summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/ui/gtk.rs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui/gtk.rs')
-rw-r--r--toolkit/crashreporter/client/app/src/ui/gtk.rs841
1 files changed, 841 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/ui/gtk.rs b/toolkit/crashreporter/client/app/src/ui/gtk.rs
new file mode 100644
index 0000000000..a76f99b0bd
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/gtk.rs
@@ -0,0 +1,841 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::model::{self, Alignment, Application, Element};
+use crate::std::{
+ cell::RefCell,
+ ffi::{c_char, CString},
+ rc::Rc,
+ sync::atomic::{AtomicBool, Ordering::Relaxed},
+};
+use crate::{
+ data::{Event, Property, Synchronized},
+ std,
+};
+
+/// Create a `std::ffi::CStr` directly from a literal string.
+///
+/// The argument is an `expr` rather than `literal` so that other macros can be used (such as
+/// `stringify!`).
+macro_rules! cstr {
+ ( $str:expr ) => {
+ #[allow(unused_unsafe)]
+ unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($str, "\0").as_bytes()) }
+ .as_ptr()
+ };
+}
+
+/// A GTK+ UI implementation.
+#[derive(Default)]
+pub struct UI {
+ running: AtomicBool,
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ unsafe {
+ let stream = gtk::g_memory_input_stream_new_from_data(
+ super::icon::PNG_DATA.as_ptr() as _,
+ // unwrap() because the PNG_DATA length will be well within gssize limits (32-bit
+ // at the smallest).
+ super::icon::PNG_DATA.len().try_into().unwrap(),
+ None,
+ );
+ let icon_pixbuf =
+ gtk::gdk_pixbuf_new_from_stream(stream, std::ptr::null_mut(), std::ptr::null_mut());
+ gtk::g_object_unref(stream as _);
+
+ gtk::gtk_window_set_default_icon(icon_pixbuf);
+
+ let app_ptr = gtk::gtk_application_new(
+ std::ptr::null(),
+ gtk::GApplicationFlags_G_APPLICATION_FLAGS_NONE,
+ );
+ gtk::g_signal_connect_data(
+ app_ptr as *mut _,
+ cstr!("activate"),
+ Some(std::mem::transmute(
+ render_application
+ as for<'a> unsafe extern "C" fn(*mut gtk::GtkApplication, &'a Application),
+ )),
+ &app as *const Application as *mut Application as _,
+ None,
+ 0,
+ );
+ self.running.store(true, Relaxed);
+ gtk::g_application_run(app_ptr as *mut gtk::GApplication, 0, std::ptr::null_mut());
+ self.running.store(false, Relaxed);
+ gtk::g_object_unref(app_ptr as *mut _);
+ gtk::g_object_unref(icon_pixbuf as _);
+ }
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ if !self.running.load(Relaxed) {
+ log::debug!("ignoring `invoke` as main loop isn't running");
+ return;
+ }
+ type BoxedData = Option<model::InvokeFn>;
+
+ unsafe extern "C" fn call(ptr: gtk::gpointer) -> gtk::gboolean {
+ let f: &mut BoxedData = ToPointer::from_ptr(ptr as _);
+ f.take().unwrap()();
+ false.into()
+ }
+
+ unsafe extern "C" fn drop(ptr: gtk::gpointer) {
+ let _: Box<BoxedData> = ToPointer::from_ptr(ptr as _);
+ }
+
+ let data: Box<BoxedData> = Box::new(Some(f));
+
+ unsafe {
+ let main_context = gtk::g_main_context_default();
+ gtk::g_main_context_invoke_full(
+ main_context,
+ 0, // G_PRIORITY_DEFAULT
+ Some(call as unsafe extern "C" fn(gtk::gpointer) -> gtk::gboolean),
+ data.to_ptr() as _,
+ Some(drop as unsafe extern "C" fn(gtk::gpointer)),
+ );
+ }
+ }
+}
+
+/// Types that can be converted to and from a pointer.
+///
+/// These types must be sized to avoid fat pointers (i.e., the pointers must be FFI-compatible, the
+/// same size as usize).
+trait ToPointer: Sized {
+ fn to_ptr(self) -> *mut ();
+ /// # Safety
+ /// The caller must ensure that the pointer was created as the result of `to_ptr` on the same
+ /// or a compatible type, and that the data is still valid.
+ unsafe fn from_ptr(ptr: *mut ()) -> Self;
+}
+
+/// Types that can be attached to a GLib object to be dropped when the widget is dropped.
+trait DropWithObject: Sized {
+ fn drop_with_object(self, object: *mut gtk::GObject);
+ fn drop_with_widget(self, widget: *mut gtk::GtkWidget) {
+ self.drop_with_object(widget as *mut _);
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char);
+}
+
+impl<T: ToPointer> DropWithObject for T {
+ fn drop_with_object(self, object: *mut gtk::GObject) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(
+ ptr: gtk::gpointer,
+ _object: *mut gtk::GObject,
+ ) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe { gtk::g_object_weak_ref(object, Some(free_ptr::<T>), self.to_ptr() as *mut _) };
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(ptr: gtk::gpointer) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe {
+ gtk::g_object_set_data_full(object, key, self.to_ptr() as *mut _, Some(free_ptr::<T>))
+ };
+ }
+}
+
+impl ToPointer for CString {
+ fn to_ptr(self) -> *mut () {
+ self.into_raw() as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ CString::from_raw(ptr as *mut c_char)
+ }
+}
+
+impl<T> ToPointer for Rc<T> {
+ fn to_ptr(self) -> *mut () {
+ Rc::into_raw(self) as *mut T as *mut ()
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Rc::from_raw(ptr as *mut T as *const T)
+ }
+}
+
+impl<T> ToPointer for Box<T> {
+ fn to_ptr(self) -> *mut () {
+ Box::into_raw(self) as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Box::from_raw(ptr as _)
+ }
+}
+
+impl<T: Sized> ToPointer for &mut T {
+ fn to_ptr(self) -> *mut () {
+ self as *mut T as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ &mut *(ptr as *mut T)
+ }
+}
+
+/// Connect a GTK+ object signal to a function, providing an additional context value (by
+/// reference).
+macro_rules! connect_signal {
+ ( object $object:expr ; with $with:expr ;
+ signal $name:ident ($target:ident : &$type:ty $(, $argname:ident : $argtype:ty )* ) $( -> $ret:ty )? $body:block
+ ) => {{
+ unsafe extern "C" fn $name($($argname : $argtype ,)* $target: &$type) $( -> $ret )? $body
+ #[allow(unused_unsafe)]
+ unsafe {
+ gtk::g_signal_connect_data(
+ $object as *mut _,
+ cstr!(stringify!($name)),
+ Some(std::mem::transmute(
+ $name
+ as for<'a> unsafe extern "C" fn(
+ $($argtype,)*
+ &'a $type,
+ ) $( -> $ret )?,
+ )),
+ $with as *const $type as *mut $type as _,
+ None,
+ 0,
+ );
+ }
+ }};
+}
+
+/// Bind a read only (from the renderer perspective) property to a widget.
+///
+/// The `set` function is called initially and when the property value changes.
+macro_rules! property_read_only {
+ ( property $property:expr ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ v.on_change(move |$name| $setbody);
+ }
+ Property::ReadOnly(_) => (),
+ }
+ }};
+}
+
+/// Bind a read/write property to a widget signal.
+///
+/// This currently only allows signals which are of the form
+/// `void(*)(SomeGtkObject*, gpointer user_data)`.
+macro_rules! property_with_signal {
+ ( object $object:expr ; property $property:expr ; signal $signame:ident ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ fn get( $getobj:ident : $gettype:ty ) -> $result:ty $getbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ let changing = Rc::new(RefCell::new(false));
+ struct SignalData {
+ changing: Rc<RefCell<bool>>,
+ value: Synchronized<$result>,
+ }
+ let signal_data = Box::new(SignalData {
+ changing: changing.clone(),
+ value: v.clone(),
+ });
+ v.on_change(move |$name| {
+ if !*changing.borrow() {
+ *changing.borrow_mut() = true;
+ $setbody;
+ *changing.borrow_mut() = false;
+ }
+ });
+ connect_signal! {
+ object $object;
+ with signal_data.as_ref();
+ signal $signame(signal_data: &SignalData, $getobj: $gettype) {
+ let new_value = (|| $getbody)();
+ if !*signal_data.changing.borrow() {
+ *signal_data.changing.borrow_mut() = true;
+ *signal_data.value.borrow_mut() = new_value;
+ *signal_data.changing.borrow_mut() = false;
+ }
+ }
+ }
+ signal_data.drop_with_object($object as _);
+ }
+ Property::ReadOnly(v) => {
+ v.register(move |target: &mut $result| {
+ let $getobj: $gettype = $object as _;
+ *target = (|| $getbody)();
+ });
+ }
+ }
+ }};
+}
+
+unsafe extern "C" fn render_application(app_ptr: *mut gtk::GtkApplication, app: &Application) {
+ unsafe {
+ gtk::gtk_widget_set_default_direction(if app.rtl {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_RTL
+ } else {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_LTR
+ });
+ }
+ for window in &app.windows {
+ let window_ptr = render_window(&window.element_type);
+ let style = &window.style;
+
+ // Set size before applying style (since the style will set the visibility and show the
+ // window). Note that we take the size request as an initial size here.
+ //
+ // `gtk_window_set_default_size` doesn't work; it resizes to the size request of the inner
+ // labels (instead of wrapping them) since it doesn't know how small they should be (that's
+ // dictated by the window size!).
+ unsafe {
+ gtk::gtk_window_resize(
+ window_ptr as _,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+ }
+
+ apply_style(window_ptr, style);
+ unsafe {
+ gtk::gtk_application_add_window(app_ptr, window_ptr as *mut _);
+ }
+ }
+}
+
+fn render(element: &Element) -> Option<*mut gtk::GtkWidget> {
+ let widget = render_element_type(&element.element_type)?;
+ apply_style(widget, &element.style);
+ Some(widget)
+}
+
+fn apply_style(widget: *mut gtk::GtkWidget, style: &model::ElementStyle) {
+ unsafe {
+ gtk::gtk_widget_set_halign(widget, alignment(&style.horizontal_alignment));
+ if style.horizontal_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_hexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_valign(widget, alignment(&style.vertical_alignment));
+ if style.vertical_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_vexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_size_request(
+ widget,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+
+ gtk::gtk_widget_set_margin_start(widget, style.margin.start as i32);
+ gtk::gtk_widget_set_margin_end(widget, style.margin.end as i32);
+ gtk::gtk_widget_set_margin_top(widget, style.margin.top as i32);
+ gtk::gtk_widget_set_margin_bottom(widget, style.margin.bottom as i32);
+ }
+ property_read_only! {
+ property &style.visible;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_visible(widget, new_value.clone().into());
+ };
+ }
+ }
+ property_read_only! {
+ property &style.enabled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_sensitive(widget, new_value.clone().into());
+ }
+ }
+ }
+}
+
+fn alignment(align: &Alignment) -> gtk::GtkAlign {
+ use Alignment::*;
+ match align {
+ Start => gtk::GtkAlign_GTK_ALIGN_START,
+ Center => gtk::GtkAlign_GTK_ALIGN_CENTER,
+ End => gtk::GtkAlign_GTK_ALIGN_END,
+ Fill => gtk::GtkAlign_GTK_ALIGN_FILL,
+ }
+}
+
+struct PangoAttrList {
+ list: *mut gtk::PangoAttrList,
+}
+
+impl PangoAttrList {
+ pub fn new() -> Self {
+ PangoAttrList {
+ list: unsafe { gtk::pango_attr_list_new() },
+ }
+ }
+
+ pub fn bold(&mut self) -> &mut Self {
+ unsafe {
+ gtk::pango_attr_list_insert(
+ self.list,
+ gtk::pango_attr_weight_new(gtk::PangoWeight_PANGO_WEIGHT_BOLD),
+ )
+ };
+ self
+ }
+}
+
+impl std::ops::Deref for PangoAttrList {
+ type Target = *mut gtk::PangoAttrList;
+
+ fn deref(&self) -> &Self::Target {
+ &self.list
+ }
+}
+
+impl std::ops::DerefMut for PangoAttrList {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.list
+ }
+}
+
+impl Drop for PangoAttrList {
+ fn drop(&mut self) {
+ unsafe { gtk::pango_attr_list_unref(self.list) };
+ }
+}
+
+fn render_element_type(element_type: &model::ElementType) -> Option<*mut gtk::GtkWidget> {
+ use model::ElementType::*;
+ Some(match element_type {
+ Label(model::Label { text, bold }) => {
+ let label_ptr = unsafe { gtk::gtk_label_new(std::ptr::null()) };
+ match text {
+ Property::Static(text) => {
+ let text = CString::new(text.clone()).ok()?;
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, text.as_ptr()) };
+ text.drop_with_widget(label_ptr);
+ }
+ Property::Binding(b) => {
+ let label_text = Rc::new(RefCell::new(CString::new(b.borrow().clone()).ok()?));
+ unsafe {
+ gtk::gtk_label_set_text(label_ptr as _, label_text.borrow().as_ptr())
+ };
+ let lt = label_text.clone();
+ label_text.drop_with_widget(label_ptr);
+ b.on_change(move |t| {
+ let Some(cstr) = CString::new(t.clone()).ok() else {
+ return;
+ };
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, cstr.as_ptr()) };
+ *lt.borrow_mut() = cstr;
+ });
+ }
+ Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"),
+ }
+ unsafe { gtk::gtk_label_set_line_wrap(label_ptr as _, true.into()) };
+ // This is gtk_label_set_{xalign,yalign} in gtk 3.16+
+ unsafe { gtk::gtk_misc_set_alignment(label_ptr as _, 0.0, 0.5) };
+ if *bold {
+ unsafe {
+ gtk::gtk_label_set_attributes(label_ptr as _, **PangoAttrList::new().bold())
+ };
+ }
+ label_ptr
+ }
+ HBox(model::HBox {
+ items,
+ spacing,
+ affirmative_order,
+ }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_HORIZONTAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ let items_iter: Box<dyn Iterator<Item = &Element>> = if *affirmative_order {
+ Box::new(items.iter().rev())
+ } else {
+ Box::new(items.iter())
+ };
+ for item in items_iter {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case horizontal alignment to pack into the end if appropriate
+ if item.style.horizontal_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ VBox(model::VBox { items, spacing }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_VERTICAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ for item in items {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case vertical alignment to pack into the end if appropriate
+ if item.style.vertical_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ Button(model::Button { content, click }) => {
+ let button_ptr = unsafe { gtk::gtk_button_new() };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ // Always center widgets in buttons.
+ gtk::gtk_widget_set_valign(widget, alignment(&Alignment::Center));
+ gtk::gtk_widget_set_halign(widget, alignment(&Alignment::Center));
+ gtk::gtk_container_add(button_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ connect_signal! {
+ object button_ptr;
+ with click;
+ signal clicked(event: &Event<()>, _button: *mut gtk::GtkButton) {
+ event.fire(&());
+ }
+ }
+ button_ptr
+ }
+ Checkbox(model::Checkbox { checked, label }) => {
+ let cb_ptr = match label {
+ None => unsafe { gtk::gtk_check_button_new() },
+ Some(s) => {
+ let label = CString::new(s.clone()).ok()?;
+ let cb = unsafe { gtk::gtk_check_button_new_with_label(label.as_ptr()) };
+ label.drop_with_widget(cb);
+ cb
+ }
+ };
+ property_with_signal! {
+ object cb_ptr;
+ property checked;
+ signal toggled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_toggle_button_set_active(cb_ptr as *mut _, new_value.clone().into())
+ };
+ }
+ fn get(button: *mut gtk::GtkToggleButton) -> bool {
+ unsafe {
+ gtk::gtk_toggle_button_get_active(button) == 1
+ }
+ }
+ }
+ cb_ptr
+ }
+ TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let text_ptr = unsafe { gtk::gtk_text_view_new() };
+ unsafe {
+ const GTK_WRAP_WORD_CHAR: u32 = 3;
+ gtk::gtk_text_view_set_wrap_mode(text_ptr as *mut _, GTK_WRAP_WORD_CHAR);
+ gtk::gtk_text_view_set_editable(text_ptr as *mut _, editable.clone().into());
+ gtk::gtk_text_view_set_accepts_tab(text_ptr as *mut _, false.into());
+ }
+ let buffer = unsafe { gtk::gtk_text_view_get_buffer(text_ptr as *mut _) };
+
+ struct State {
+ placeholder: Option<Placeholder>,
+ }
+
+ struct Placeholder {
+ string: CString,
+ visible: RefCell<bool>,
+ }
+
+ impl Placeholder {
+ fn focus(&self, widget: *mut gtk::GtkWidget) {
+ if *self.visible.borrow() {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), 0);
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ std::ptr::null(),
+ );
+ }
+ *self.visible.borrow_mut() = false;
+ }
+ }
+
+ fn unfocus(&self, widget: *mut gtk::GtkWidget) {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+
+ let mut end_iter = gtk::GtkTextIter::default();
+ gtk::gtk_text_buffer_get_end_iter(buffer, &mut end_iter);
+ let is_empty = gtk::gtk_text_iter_get_offset(&end_iter) == 0;
+
+ if is_empty && !*self.visible.borrow() {
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), -1);
+ let context = gtk::gtk_widget_get_style_context(widget);
+ let mut color = gtk::GdkRGBA::default();
+ gtk::gtk_style_context_get_color(
+ context,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_INSENSITIVE,
+ &mut color,
+ );
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ &color,
+ );
+ *self.visible.borrow_mut() = true;
+ }
+ }
+ }
+ }
+
+ let mut state = Box::new(State { placeholder: None });
+
+ if let Some(placeholder) = placeholder {
+ state.placeholder = Some(Placeholder {
+ string: CString::new(placeholder.clone()).ok()?,
+ visible: RefCell::new(false),
+ });
+
+ let placeholder = state.placeholder.as_ref().unwrap();
+
+ placeholder.unfocus(text_ptr);
+
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_in_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.focus(widget);
+ false.into()
+ }
+ }
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_out_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.unfocus(widget);
+ false.into()
+ }
+ }
+ }
+
+ // Attach the state so that we can access it in the changed signal.
+ // This is kind of ugly; in the future it might be a nicer developer interface to simply
+ // always use a closure as the user data (which itself can capture arbitrary things). This
+ // would move the data management from GTK to rust.
+ state.set_data(buffer as *mut _, cstr!("textview-state"));
+
+ property_with_signal! {
+ object buffer;
+ property content;
+ signal changed;
+ fn set(new_value: &String) {
+ unsafe {
+ gtk::gtk_text_buffer_set_text(buffer, new_value.as_ptr() as *const c_char, new_value.len().try_into().unwrap());
+ }
+ }
+ fn get(buffer: *mut gtk::GtkTextBuffer) -> String {
+ let state = unsafe {
+ gtk::g_object_get_data(buffer as *mut _, cstr!("textview-state"))
+ };
+ if !state.is_null() {
+ let s: &State = unsafe { &*(state as *mut State as *const State) };
+ if let Some(placeholder) = &s.placeholder {
+ if *placeholder.visible.borrow() {
+ return "".to_owned();
+ }
+ }
+ }
+ let mut start_iter = gtk::GtkTextIter::default();
+ unsafe {
+ gtk::gtk_text_buffer_get_start_iter(buffer, &mut start_iter);
+ }
+
+ let mut s = String::new();
+ loop {
+ let c = unsafe { gtk::gtk_text_iter_get_char(&start_iter) };
+ if c == 0 {
+ break;
+ }
+ // Safety:
+ // gunichar is guaranteed to be a valid unicode codepoint (if nonzero).
+ s.push(unsafe { char::from_u32_unchecked(c) });
+ unsafe {
+ gtk::gtk_text_iter_forward_char(&mut start_iter);
+ }
+ }
+ s
+ }
+ }
+ text_ptr
+ }
+ Scroll(model::Scroll { content }) => {
+ let scroll_ptr =
+ unsafe { gtk::gtk_scrolled_window_new(std::ptr::null_mut(), std::ptr::null_mut()) };
+ unsafe {
+ gtk::gtk_scrolled_window_set_policy(
+ scroll_ptr as *mut _,
+ gtk::GtkPolicyType_GTK_POLICY_NEVER,
+ gtk::GtkPolicyType_GTK_POLICY_ALWAYS,
+ );
+ gtk::gtk_scrolled_window_set_shadow_type(
+ scroll_ptr as *mut _,
+ gtk::GtkShadowType_GTK_SHADOW_IN,
+ );
+ };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ gtk::gtk_container_add(scroll_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ scroll_ptr
+ }
+ Progress(model::Progress { amount }) => {
+ let progress_ptr = unsafe { gtk::gtk_progress_bar_new() };
+ property_read_only! {
+ property amount;
+ fn set(value: &Option<f32>) {
+ match &*value {
+ Some(v) => unsafe {
+ gtk::gtk_progress_bar_set_fraction(
+ progress_ptr as *mut _,
+ v.clamp(0f32,1f32) as f64,
+ );
+ }
+ None => unsafe {
+ gtk::gtk_progress_bar_pulse(progress_ptr as *mut _);
+
+ fn auto_pulse_progress_bar(progress: *mut gtk::GtkProgressBar) {
+ unsafe extern fn pulse(progress: *mut std::ffi::c_void) -> gtk::gboolean {
+ if gtk::gtk_widget_is_visible(progress as _) == 0 {
+ false.into()
+ } else {
+ gtk::gtk_progress_bar_pulse(progress as _);
+ true.into()
+ }
+ }
+ unsafe {
+ gtk::g_timeout_add(100, Some(pulse as unsafe extern fn(*mut std::ffi::c_void) -> gtk::gboolean), progress as _);
+ }
+
+ }
+
+ connect_signal! {
+ object progress_ptr;
+ with std::ptr::null_mut();
+ signal show(_user_data: &(), progress: *mut gtk::GtkWidget) {
+ auto_pulse_progress_bar(progress as *mut _);
+ }
+ }
+ auto_pulse_progress_bar(progress_ptr as *mut _);
+ }
+ }
+ }
+ }
+ progress_ptr
+ }
+ })
+}
+
+fn render_window(
+ model::Window {
+ title,
+ content,
+ children,
+ modal,
+ close,
+ }: &model::Window,
+) -> *mut gtk::GtkWidget {
+ unsafe {
+ let window_ptr = gtk::gtk_window_new(gtk::GtkWindowType_GTK_WINDOW_TOPLEVEL);
+ if !title.is_empty() {
+ if let Some(title) = CString::new(title.clone()).ok() {
+ gtk::gtk_window_set_title(window_ptr as *mut _, title.as_ptr());
+ title.drop_with_widget(window_ptr);
+ }
+ }
+ if let Some(content) = content {
+ if let Some(widget) = render(content) {
+ gtk::gtk_container_add(window_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ for child in children {
+ let widget = render_window(&child.element_type);
+ apply_style(widget, &child.style);
+ gtk::gtk_window_set_transient_for(widget as *mut _, window_ptr as *mut _);
+ // Delete should hide the window.
+ gtk::g_signal_connect_data(
+ widget as *mut _,
+ cstr!("delete-event"),
+ Some(std::mem::transmute(
+ gtk::gtk_widget_hide_on_delete
+ as unsafe extern "C" fn(*mut gtk::GtkWidget) -> i32,
+ )),
+ std::ptr::null_mut(),
+ None,
+ 0,
+ );
+ }
+ if *modal {
+ gtk::gtk_window_set_modal(window_ptr as *mut _, true.into());
+ }
+ if let Some(close) = close {
+ close.subscribe(move |&()| gtk::gtk_window_close(window_ptr as *mut _));
+ }
+
+ window_ptr
+ }
+}