diff --git a/crates/ewwii/src/widgets/widget_definitions.rs b/crates/ewwii/src/widgets/widget_definitions.rs index b6b0de2..58eeabb 100644 --- a/crates/ewwii/src/widgets/widget_definitions.rs +++ b/crates/ewwii/src/widgets/widget_definitions.rs @@ -215,45 +215,85 @@ pub(super) fn build_gtk_box( children: &Vec, widget_registry: &mut WidgetRegistry, ) -> Result { - // Parse initial props to create the widget: - let orientation = props - .get("orientation") - .and_then(|v| v.clone().try_cast::()) - .map(|s| parse_orientation(&s)) - .transpose()? - .unwrap_or(gtk4::Orientation::Horizontal); - - let spacing = - props.get("spacing").and_then(|v| v.clone().try_cast::()).unwrap_or(0) as i32; - - let space_evenly = get_bool_prop(&props, "space_evenly", Some(true))?; - - let gtk_widget = gtk4::Box::new(orientation, spacing); - gtk_widget.set_homogeneous(space_evenly); + let gtk_widget = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); for child in children { let child_widget = build_gtk_widget(&WidgetInput::BorrowedNode(child), widget_registry)?; gtk_widget.append(&child_widget); } + let apply_props = |props: &Map, widget: >k4::Box| -> Result<()> { + handle_signal_or_value( + &props, + "orientation", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + if let Ok(orientation) = parse_orientation(&obj.property::("value")) { + widget.set_orientation(orientation); + } + }) + ); + }, + |value| { + if let Ok(orientation) = parse_orientation(&value) { + widget.set_orientation(orientation); + } + }, + ); + + handle_signal_or_value( + &props, + "spacing", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_spacing(i) + } + }) + ); + }, + |value| widget.set_spacing(value), + ); + + handle_signal_or_value( + &props, + "space_evenly", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_homogeneous(i) + } + }) + ); + }, + |value| widget.set_homogeneous(value), + ); + + Ok(()) + }; + + apply_props(&props, >k_widget)?; + let gtk_widget_clone = gtk_widget.clone(); - let update_fn: UpdateFn = Box::new(move |props: &Map| { - if let Some(orientation_str) = - props.get("orientation").and_then(|v| v.clone().try_cast::()) - { - if let Ok(orientation) = parse_orientation(&orientation_str) { - gtk_widget_clone.set_orientation(orientation); - } - } - - if let Some(spacing_val) = props.get("spacing").and_then(|v| v.clone().try_cast::()) { - gtk_widget_clone.set_spacing(spacing_val as i32); - } - - if let Ok(space_evenly) = get_bool_prop(props, "space_evenly", None) { - gtk_widget_clone.set_homogeneous(space_evenly); - } + let _ = apply_props(&props, >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -1677,88 +1717,229 @@ pub(super) fn build_gtk_progress( Ok(gtk_widget) } +struct ImageWdgtData { + path: String, + image_width: i32, + image_height: i32, + preserve_aspect_ratio: bool, + fill_svg: String, +} + pub(super) fn build_image( props: &Map, widget_registry: &mut WidgetRegistry, ) -> Result { let gtk_widget = gtk4::Picture::new(); - let apply_props = |props: &Map, widget: >k4::Picture| -> Result<()> { - let path = get_string_prop(&props, "path", None)?; - let image_width = get_i32_prop(&props, "image_width", Some(-1))?; - let image_height = get_i32_prop(&props, "image_height", Some(-1))?; - let preserve_aspect_ratio = get_bool_prop(&props, "preserve_aspect_ratio", Some(true))?; - let fill_svg = get_string_prop(&props, "fill_svg", Some(""))?; + let widget_state = Rc::new(RefCell::new(ImageWdgtData { + path: String::new(), + image_width: -1, + image_height: -1, + preserve_aspect_ratio: true, + fill_svg: String::new(), + })); - if !path.ends_with(".svg") && !fill_svg.is_empty() { - log::warn!("Fill attribute ignored, file is not an svg image"); - } + let apply_props = |props: &Map, widget_state: Rc>, widget: >k4::Picture| -> Result<()> { + let update_image = { + let widget = widget.clone(); + let widget_state = widget_state.clone(); + move || -> Result<()> { + let state = widget_state.borrow(); - if path.ends_with(".gif") { - let pixbuf_animation = - gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; - let iter = pixbuf_animation.iter(None); + let path = &state.path; + let image_width = state.image_width; + let image_height = state.image_height; + let preserve_aspect_ratio = state.preserve_aspect_ratio; + let fill_svg = &state.fill_svg; - let frame_pixbuf = iter.pixbuf(); - widget.set_pixbuf(Some(&frame_pixbuf)); + if !path.ends_with(".svg") && !fill_svg.is_empty() { + log::warn!("Fill attribute ignored, file is not an svg image"); + } - let widget_clone = widget.clone(); + if path.ends_with(".gif") { + let pixbuf_animation = + gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; + let iter = pixbuf_animation.iter(None); - if let Some(delay) = iter.delay_time() { - glib::timeout_add_local(delay, move || { - let now = std::time::SystemTime::now(); + let frame_pixbuf = iter.pixbuf(); + widget.set_pixbuf(Some(&frame_pixbuf)); - if iter.advance(now) { - let frame_pixbuf = iter.pixbuf(); - widget_clone.set_pixbuf(Some(&frame_pixbuf)); + let widget_clone = widget.clone(); + + if let Some(delay) = iter.delay_time() { + glib::timeout_add_local(delay, move || { + let now = std::time::SystemTime::now(); + + if iter.advance(now) { + let frame_pixbuf = iter.pixbuf(); + widget_clone.set_pixbuf(Some(&frame_pixbuf)); + } + + glib::ControlFlow::Continue + }); } - - glib::ControlFlow::Continue - }); - } - } else { - let pixbuf; - // populate the pixel buffer - if path.ends_with(".svg") && !fill_svg.is_empty() { - let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?; - // The fastest way to add/change fill color - let svg_data = if svg_data.contains("fill=") { - let reg = regex::Regex::new(r#"fill="[^"]*""#)?; - reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg)) } else { - let reg = regex::Regex::new(r", - )?; - stream.close(None::<>k4::gio::Cancellable>)?; - } else { - pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_file_at_scale( - std::path::PathBuf::from(path), - image_width, - image_height, - preserve_aspect_ratio, - )?; + let pixbuf; + // populate the pixel buffer + if path.ends_with(".svg") && !fill_svg.is_empty() { + let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?; + // The fastest way to add/change fill color + let svg_data = if svg_data.contains("fill=") { + let reg = regex::Regex::new(r#"fill="[^"]*""#)?; + reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg)) + } else { + let reg = regex::Regex::new(r", + )?; + stream.close(None::<>k4::gio::Cancellable>)?; + } else { + pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_file_at_scale( + std::path::PathBuf::from(path), + image_width, + image_height, + preserve_aspect_ratio, + )?; + } + widget.set_pixbuf(Some(&pixbuf)); + } + Ok(()) } - widget.set_pixbuf(Some(&pixbuf)); - } + }; + + handle_signal_or_value( + &props, + "path", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().path = value; + let _ = update_image(); + }) + ); + }, + |value| { + widget_state.borrow_mut().path = value; + }, + ); + + handle_signal_or_value( + &props, + "fill_svg", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().fill_svg = value; + let _ = update_image(); + }) + ); + }, + |value| { + widget_state.borrow_mut().fill_svg = value; + }, + ); + + handle_signal_or_value( + &props, + "image_height", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().image_height = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().image_height = value; + }, + ); + + handle_signal_or_value( + &props, + "image_width", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().image_width = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().image_width = value; + }, + ); + + handle_signal_or_value( + &props, + "preserve_aspect_ratio", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().preserve_aspect_ratio = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().preserve_aspect_ratio = value; + }, + ); Ok(()) }; - apply_props(&props, >k_widget)?; + apply_props(&props, widget_state.clone(), >k_widget)?; let gtk_widget_clone = gtk_widget.clone(); let update_fn: UpdateFn = Box::new(move |props: &Map| { - let _ = apply_props(props, >k_widget_clone); + let _ = apply_props(props, widget_state.clone(), >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -1782,82 +1963,235 @@ pub(super) fn build_image( pub(super) fn build_icon(props: &Map, widget_registry: &mut WidgetRegistry) -> Result { let gtk_widget = gtk4::Image::new(); - let apply_props = |props: &Map, widget: >k4::Image| -> Result<()> { - let path = get_string_prop(&props, "path", None)?; - let image_width = get_i32_prop(&props, "image_width", Some(-1))?; - let image_height = get_i32_prop(&props, "image_height", Some(-1))?; - let preserve_aspect_ratio = get_bool_prop(&props, "preserve_aspect_ratio", Some(true))?; - let fill_svg = get_string_prop(&props, "fill_svg", Some(""))?; + let widget_state = Rc::new(RefCell::new(ImageWdgtData { + path: String::new(), + image_width: -1, + image_height: -1, + preserve_aspect_ratio: true, + fill_svg: String::new(), + })); - if !path.ends_with(".svg") && !fill_svg.is_empty() { - log::warn!("Fill attribute ignored, file is not an svg image"); - } + let apply_props = |props: &Map, widget_state: Rc>, widget: >k4::Image| -> Result<()> { + let update_image = { + let widget = widget.clone(); + let widget_state = widget_state.clone(); + move || -> Result<()> { + let state = widget_state.borrow(); - if path.ends_with(".gif") { - let pixbuf_animation = - gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; - let iter = pixbuf_animation.iter(None); + let path = &state.path; + let image_width = state.image_width; + let image_height = state.image_height; + let preserve_aspect_ratio = state.preserve_aspect_ratio; + let fill_svg = &state.fill_svg; - let frame_pixbuf = iter.pixbuf(); - widget.set_from_pixbuf(Some(&frame_pixbuf)); + if !path.ends_with(".svg") && !fill_svg.is_empty() { + log::warn!("Fill attribute ignored, file is not an svg image"); + } - let widget_clone = widget.clone(); + if path.ends_with(".gif") { + let pixbuf_animation = + gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; + let iter = pixbuf_animation.iter(None); - if let Some(delay) = iter.delay_time() { - glib::timeout_add_local(delay, move || { let frame_pixbuf = iter.pixbuf(); - widget_clone.set_from_pixbuf(Some(&frame_pixbuf)); + widget.set_from_pixbuf(Some(&frame_pixbuf)); - glib::ControlFlow::Continue - }); - } - } else { - let pixbuf; - // populate the pixel buffer - if path.ends_with(".svg") && !fill_svg.is_empty() { - let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?; - // The fastest way to add/change fill color - let svg_data = if svg_data.contains("fill=") { - let reg = regex::Regex::new(r#"fill="[^"]*""#)?; - reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg)) + let widget_clone = widget.clone(); + + if let Some(delay) = iter.delay_time() { + glib::timeout_add_local(delay, move || { + let now = std::time::SystemTime::now(); + + if iter.advance(now) { + let frame_pixbuf = iter.pixbuf(); + widget_clone.set_from_pixbuf(Some(&frame_pixbuf)); + } + + glib::ControlFlow::Continue + }); + } } else { - let reg = regex::Regex::new(r", - )?; - stream.close(None::<>k4::gio::Cancellable>)?; - } else { - pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_file_at_scale( - std::path::PathBuf::from(path), - image_width, - image_height, - preserve_aspect_ratio, - )?; + let pixbuf; + // populate the pixel buffer + if path.ends_with(".svg") && !fill_svg.is_empty() { + let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?; + // The fastest way to add/change fill color + let svg_data = if svg_data.contains("fill=") { + let reg = regex::Regex::new(r#"fill="[^"]*""#)?; + reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg)) + } else { + let reg = regex::Regex::new(r", + )?; + stream.close(None::<>k4::gio::Cancellable>)?; + } else { + pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_file_at_scale( + std::path::PathBuf::from(path), + image_width, + image_height, + preserve_aspect_ratio, + )?; + } + widget.set_from_pixbuf(Some(&pixbuf)); + } + Ok(()) } - widget.set_from_pixbuf(Some(&pixbuf)); - } + }; - if let Ok(icon_name) = get_string_prop(&props, "icon", None) { - widget.set_icon_name(Some(&icon_name)); - } + handle_signal_or_value( + &props, + "path", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().path = value; + let _ = update_image(); + }) + ); + }, + |value| { + widget_state.borrow_mut().path = value; + }, + ); + + handle_signal_or_value( + &props, + "fill_svg", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().fill_svg = value; + let _ = update_image(); + }) + ); + }, + |value| { + widget_state.borrow_mut().fill_svg = value; + }, + ); + + handle_signal_or_value( + &props, + "image_height", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().image_height = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().image_height = value; + }, + ); + + handle_signal_or_value( + &props, + "image_width", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().image_width = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().image_width = value; + }, + ); + + handle_signal_or_value( + &props, + "preserve_aspect_ratio", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_image = update_image.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().preserve_aspect_ratio = i; + let _ = update_image(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().preserve_aspect_ratio = value; + }, + ); + + handle_signal_or_value( + &props, + "icon", + |p, k| get_string_prop(p, k, None), + |signal| { + let signal_widget = signal.data; + let widget = widget.clone(); + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget.set_icon_name(Some(&value)); + }) + ); + }, + |value| { + widget.set_icon_name(Some(&value)); + }, + ); Ok(()) }; - apply_props(&props, >k_widget)?; + apply_props(&props, widget_state.clone(), >k_widget)?; let gtk_widget_clone = gtk_widget.clone(); let update_fn: UpdateFn = Box::new(move |props: &Map| { - let _ = apply_props(props, >k_widget_clone); + let _ = apply_props(props, widget_state.clone(), >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -1867,7 +2201,7 @@ pub(super) fn build_icon(props: &Map, widget_registry: &mut WidgetRegistry) -> R } }); - let id = hash_props_and_type(&props, "Image"); + let id = hash_props_and_type(&props, "Icon"); widget_registry .widgets @@ -2092,105 +2426,460 @@ pub(super) fn build_gtk_button( Ok(gtk_widget) } +struct LabelWdgtState { + truncate: bool, + limit_width: i32, + truncate_left: bool, + show_truncated: bool, + unindent: bool, + text: Option, + markup: Option, +} + pub(super) fn build_gtk_label( props: &Map, widget_registry: &mut WidgetRegistry, ) -> Result { let gtk_widget = gtk4::Label::new(None); - let apply_props = |props: &Map, widget: >k4::Label| -> Result<()> { - let truncate = get_bool_prop(&props, "truncate", Some(false))?; - let limit_width = get_i32_prop(&props, "limit_width", Some(i32::MAX))?; - let truncate_left = get_bool_prop(&props, "truncate_left", Some(false))?; - let show_truncated = get_bool_prop(&props, "show_truncated", Some(true))?; - let unindent = get_bool_prop(&props, "unindent", Some(true))?; + let widget_state = Rc::new(RefCell::new(LabelWdgtState { + truncate: false, + limit_width: i32::MAX, + truncate_left: false, + show_truncated: true, + unindent: true, + text: None, + markup: None, + })); - let has_text = props.get("text").is_some(); - let has_markup = props.get("markup").is_some(); + let apply_props = |props: &Map, widget_state: Rc>, widget: >k4::Label| -> Result<()> { + let update_label = { + let widget = widget.clone(); + let widget_state = widget_state.clone(); + move || -> Result<()> { + let state = widget_state.borrow(); - if has_text && has_markup { - bail!("Cannot set both 'text' and 'markup' for a label"); - } else if has_text { - let text = get_string_prop(&props, "text", None)?; - let t = if show_truncated { - if limit_width == i32::MAX { - widget.set_max_width_chars(-1); - } else { - widget.set_max_width_chars(limit_width); - } - apply_ellipsize_settings( - &widget, - truncate, - limit_width, - truncate_left, - show_truncated, - ); - text - } else { - widget.set_ellipsize(pango::EllipsizeMode::None); + let truncate = state.truncate; + let limit_width = state.limit_width; + let truncate_left = state.truncate_left; + let show_truncated = state.show_truncated; + let unindent = state.unindent; - let limit_width = limit_width as usize; - let char_count = text.chars().count(); - if char_count > limit_width { - if truncate_left { - text.chars().skip(char_count - limit_width).collect() + let has_text = state.text.is_some(); + let has_markup = state.markup.is_some(); + + if has_text && has_markup { + bail!("Cannot set both 'text' and 'markup' for a label"); + } else if has_text { + let text = state.text.clone().unwrap(); + let t = if show_truncated { + if limit_width == i32::MAX { + widget.set_max_width_chars(-1); + } else { + widget.set_max_width_chars(limit_width); + } + apply_ellipsize_settings( + &widget, + truncate, + limit_width, + truncate_left, + show_truncated, + ); + text } else { - text.chars().take(limit_width).collect() - } + widget.set_ellipsize(pango::EllipsizeMode::None); + + let limit_width = limit_width as usize; + let char_count = text.chars().count(); + if char_count > limit_width { + if truncate_left { + text.chars().skip(char_count - limit_width).collect() + } else { + text.chars().take(limit_width).collect() + } + } else { + text + } + }; + + let unescaped = + unescape::unescape(&t).ok_or_else(|| anyhow!("Failed to unescape..."))?; + let final_text = if unindent { util::unindent(&unescaped) } else { unescaped }; + widget.set_text(&final_text); + } else if has_markup { + let markup = state.markup.as_ref().unwrap(); + apply_ellipsize_settings(&widget, truncate, limit_width, truncate_left, show_truncated); + widget.set_markup(markup); } else { - text + bail!("Either 'text' or 'markup' must be set"); } - }; - let unescaped = - unescape::unescape(&t).ok_or_else(|| anyhow!("Failed to unescape..."))?; - let final_text = if unindent { util::unindent(&unescaped) } else { unescaped }; - widget.set_text(&final_text); - } else if has_markup { - let markup = get_string_prop(&props, "markup", None)?; - apply_ellipsize_settings(&widget, truncate, limit_width, truncate_left, show_truncated); - widget.set_markup(&markup); - } else { - bail!("Either 'text' or 'markup' must be set"); - } + Ok(()) + } + }; - if let Ok(wrap) = get_bool_prop(&props, "wrap", Some(false)) { - widget.set_wrap(wrap); - } + handle_signal_or_value( + &props, + "text", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().text = Some(value); + let _ = update_label(); + }) + ); + }, + |value| { + widget_state.borrow_mut().text = Some(value); + }, + ); + + handle_signal_or_value( + &props, + "markup", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget_state.borrow_mut().markup = Some(value); + let _ = update_label(); + }) + ); + }, + |value| { + widget_state.borrow_mut().markup = Some(value); + }, + ); + + handle_signal_or_value( + &props, + "truncate", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().truncate = i; + let _ = update_label(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().truncate = value; + }, + ); + + handle_signal_or_value( + &props, + "limit_width", + |p, k| get_i32_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().limit_width = i; + let _ = update_label(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().limit_width = value; + }, + ); + + handle_signal_or_value( + &props, + "truncate_left", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().truncate_left = i; + let _ = update_label(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().truncate_left = value; + }, + ); + + handle_signal_or_value( + &props, + "show_truncated", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().show_truncated = i; + let _ = update_label(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().show_truncated = value; + }, + ); + + handle_signal_or_value( + &props, + "unindent", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget_state = widget_state.clone(); + let update_label = update_label.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget_state.borrow_mut().unindent = i; + let _ = update_label(); + } + }) + ); + }, + |value| { + widget_state.borrow_mut().unindent = value; + }, + ); + + handle_signal_or_value( + &props, + "wrap", + |p, k| get_bool_prop(p, k, Some(false)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_wrap(i); + } + }) + ); + }, + |value| { + widget.set_wrap(value); + }, + ); // if let Ok(angle) = get_f64_prop(&props, "angle", Some(0.0)) { // widget.set_angle(angle); // } - let gravity = get_string_prop(&props, "gravity", Some("south"))?; - widget.pango_context().set_base_gravity(parse_gravity(&gravity)?); + handle_signal_or_value( + &props, + "gravity", + |p, k| get_string_prop(p, k, Some("south")), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + let gravity = match parse_gravity(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse gravity: {}", e); + return; + } + }; + widget.pango_context().set_base_gravity(gravity); + }) + ); + }, + |value| { + let gravity = match parse_gravity(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse gravity: {}", e); + return; + } + }; + widget.pango_context().set_base_gravity(gravity); + }, + ); - if let Ok(xalign) = get_f64_prop(&props, "xalign", Some(0.5)) { - widget.set_xalign(xalign as f32); - } + handle_signal_or_value( + &props, + "xalign", + |p, k| get_f64_prop(p, k, Some(0.5)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_xalign(i); + } + }) + ); + }, + |value| widget.set_xalign(value as f32), + ); - if let Ok(yalign) = get_f64_prop(&props, "yalign", Some(0.5)) { - widget.set_yalign(yalign as f32); - } + handle_signal_or_value( + &props, + "yalign", + |p, k| get_f64_prop(p, k, Some(0.5)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_yalign(i); + } + }) + ); + }, + |value| widget.set_yalign(value as f32), + ); - let justify = get_string_prop(&props, "justify", Some("left"))?; - widget.set_justify(parse_justification(&justify)?); + handle_signal_or_value( + &props, + "justify", + |p, k| get_string_prop(p, k, Some("left")), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + let justify = match parse_justification(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse justification: {}", e); + return; + } + }; + widget.set_justify(justify); + }) + ); + }, + |value| { + let justify = match parse_justification(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse justification: {}", e); + return; + } + }; + widget.set_justify(justify); + }, + ); - let wrap_mode = get_string_prop(&props, "wrap_mode", Some("word"))?; - widget.set_wrap_mode(parse_wrap_mode(&wrap_mode)?); + handle_signal_or_value( + &props, + "wrap_mode", + |p, k| get_string_prop(p, k, Some("word")), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + let wrap_mode = match parse_wrap_mode(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse wrap mode: {}", e); + return; + } + }; + widget.set_wrap_mode(wrap_mode); + }) + ); + }, + |value| { + let wrap_mode = match parse_wrap_mode(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse wrap mode: {}", e); + return; + } + }; + widget.set_wrap_mode(wrap_mode); + }, + ); - if let Ok(lines) = get_i32_prop(&props, "lines", Some(-1)) { - widget.set_lines(lines); - } + handle_signal_or_value( + &props, + "lines", + |p, k| get_i32_prop(p, k, Some(-1)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_lines(i); + } + }) + ); + }, + |value| widget.set_lines(value), + ); + + update_label()?; Ok(()) }; - apply_props(&props, >k_widget)?; + apply_props(&props, widget_state.clone(), >k_widget)?; let gtk_widget_clone = gtk_widget.clone(); let update_fn: UpdateFn = Box::new(move |props: &Map| { - let _ = apply_props(props, >k_widget_clone); + let _ = apply_props(props, widget_state.clone(), >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -2211,48 +2900,141 @@ pub(super) fn build_gtk_label( Ok(gtk_widget) } +struct InputCtrlData { + onchange_cmd: String, + onaccept_cmd: String, + cmd_timeout: Duration, +} + pub(super) fn build_gtk_input( props: &Map, widget_registry: &mut WidgetRegistry, ) -> Result { let gtk_widget = gtk4::Entry::new(); - let apply_props = |props: &Map, widget: >k4::Entry| -> Result<()> { - if let Ok(value) = get_string_prop(&props, "value", None) { - widget.set_text(&value); - } + let controller_data = Rc::new(RefCell::new(InputCtrlData { + onchange_cmd: String::new(), + onaccept_cmd: String::new(), + cmd_timeout: Duration::from_millis(200) + })); - let timeout = get_duration_prop(&props, "timeout", Some(Duration::from_millis(200)))?; + gtk_widget.connect_changed(glib::clone!(#[strong] controller_data, move |widget| { + let controller = controller_data.borrow(); + run_command(controller.cmd_timeout, &controller.onchange_cmd, &[widget.text().to_string()]); + })); - if let Ok(onchange) = get_string_prop(&props, "onchange", None) { - connect_signal_handler!( - widget, - widget.connect_changed(move |widget| { - run_command(timeout, &onchange, &[widget.text().to_string()]); - }) - ); - } + gtk_widget.connect_activate(glib::clone!(#[strong] controller_data, move |widget| { + let controller = controller_data.borrow(); + run_command(controller.cmd_timeout, &controller.onaccept_cmd, &[widget.text().to_string()]); + })); - if let Ok(onaccept) = get_string_prop(&props, "onaccept", None) { - connect_signal_handler!( - widget, - widget.connect_activate(move |widget| { - run_command(timeout, &onaccept, &[widget.text().to_string()]); - }) - ); - } + let apply_props = |props: &Map, controller_data: Rc>, widget: >k4::Entry| -> Result<()> { + handle_signal_or_value( + &props, + "value", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + widget.set_text(&value); + }) + ); + }, + |value| widget.set_text(&value), + ); - let password: bool = get_bool_prop(&props, "password", Some(false))?; - widget.set_visibility(!password); + handle_signal_or_value( + &props, + "timeout", + |p, k| get_duration_prop(p, k, Some(Duration::from_millis(200))), + |signal| { + let controller_data = Rc::clone(&controller_data); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(dur) = parse_duration_str(&value) { + controller_data.borrow_mut().cmd_timeout = dur; + } else { + log::error!("Invalid duration string: {}", value); + } + }) + ); + }, + |value| { + controller_data.borrow_mut().cmd_timeout = value; + }, + ); + + handle_signal_or_value( + &props, + "onchange", + |p, k| get_string_prop(p, k, None), + |signal| { + let controller_data = controller_data.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + controller_data.borrow_mut().onchange_cmd = value + }) + ); + }, + |value| controller_data.borrow_mut().onchange_cmd = value, + ); + + handle_signal_or_value( + &props, + "onaccept", + |p, k| get_string_prop(p, k, None), + |signal| { + let controller_data = controller_data.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + controller_data.borrow_mut().onaccept_cmd = value + }) + ); + }, + |value| controller_data.borrow_mut().onaccept_cmd = value, + ); + + handle_signal_or_value( + &props, + "password", + |p, k| get_bool_prop(p, k, Some(false)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_visibility(!i); + } + }) + ); + }, + |value| widget.set_visibility(!value), + ); Ok(()) }; - apply_props(&props, >k_widget)?; + apply_props(&props, controller_data.clone(), >k_widget)?; let gtk_widget_clone = gtk_widget.clone(); let update_fn: UpdateFn = Box::new(move |props: &Map| { - let _ = apply_props(props, >k_widget_clone); + let _ = apply_props(props, controller_data.clone(), >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -2273,35 +3055,112 @@ pub(super) fn build_gtk_input( Ok(gtk_widget) } +struct CalendarCtrlData { + onclick_cmd: String, + cmd_timeout: Duration +} + pub(super) fn build_gtk_calendar( props: &Map, widget_registry: &mut WidgetRegistry, ) -> Result { let gtk_widget = gtk4::Calendar::new(); - let apply_props = |props: &Map, widget: >k4::Calendar| -> Result<()> { + let controller_data = Rc::new(RefCell::new(CalendarCtrlData { + onclick_cmd: String::new(), + cmd_timeout: Duration::from_millis(200), + })); + + gtk_widget.connect_day_selected(glib::clone!(#[strong] controller_data, move |w| { + let controller = controller_data.borrow(); + run_command(controller.cmd_timeout, &controller.onclick_cmd, &[w.day(), w.month(), w.year()]) + })); + + let apply_props = |props: &Map, controller_data: Rc>, widget: >k4::Calendar| -> Result<()> { // day - the selected day - if let Ok(day) = get_f64_prop(&props, "day", None) { - if !(1f64..=31f64).contains(&day) { - log::warn!("Calendar day is not a number between 1 and 31"); - } else { - widget.set_day(day as i32) - } - } + handle_signal_or_value( + &props, + "day", + |p, k| get_f64_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + if !(1f64..=31f64).contains(&i) { + log::warn!("Calendar day is not a number between 1 and 31"); + } else { + widget.set_day(i as i32) + } + } + }) + ); + }, + |day| { + if !(1f64..=31f64).contains(&day) { + log::warn!("Calendar day is not a number between 1 and 31"); + } else { + widget.set_day(day as i32) + } + }, + ); // month - the selected month - if let Ok(month) = get_f64_prop(&props, "month", None) { - if !(1f64..=12f64).contains(&month) { - log::warn!("Calendar month is not a number between 1 and 12"); - } else { - widget.set_month(month as i32 - 1) - } - } + handle_signal_or_value( + &props, + "month", + |p, k| get_f64_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + if !(1f64..=12f64).contains(&i) { + log::warn!("Calendar month is not a number between 1 and 12"); + } else { + widget.set_month(i as i32 - 1) + } + } + }) + ); + }, + |month| { + if !(1f64..=12f64).contains(&month) { + log::warn!("Calendar month is not a number between 1 and 12"); + } else { + widget.set_month(month as i32 - 1) + } + }, + ); // year - the selected year - if let Ok(year) = get_f64_prop(&props, "year", None) { - widget.set_year(year as i32) - } + handle_signal_or_value( + &props, + "year", + |p, k| get_f64_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_year(i as i32) + } + }) + ); + }, + |year| { + widget.set_year(year as i32) + }, + ); // // show-details - show details // if let Ok(show_details) = get_bool_prop(&props, "show_details", None) { @@ -2309,40 +3168,127 @@ pub(super) fn build_gtk_calendar( // } // show-heading - show heading line - if let Ok(show_heading) = get_bool_prop(&props, "show_heading", None) { - widget.set_show_heading(show_heading) - } + handle_signal_or_value( + &props, + "show_heading", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_show_heading(i) + } + }) + ); + }, + |show_heading| { + widget.set_show_heading(show_heading) + }, + ); // show-day-names - show names of days - if let Ok(show_day_names) = get_bool_prop(&props, "show_day_names", None) { - widget.set_show_day_names(show_day_names) - } + handle_signal_or_value( + &props, + "show_day_names", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_show_day_names(i) + } + }) + ); + }, + |show_day_names| { + widget.set_show_day_names(show_day_names) + }, + ); // show-week-numbers - show week numbers - if let Ok(show_week_numbers) = get_bool_prop(&props, "show_week_numbers", None) { - widget.set_show_week_numbers(show_week_numbers) - } + handle_signal_or_value( + &props, + "show_week_numbers", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_show_week_numbers(i) + } + }) + ); + }, + |show_week_numbers| { + widget.set_show_week_numbers(show_week_numbers) + }, + ); // timeout - timeout of the command. Default: "200ms" - let timeout = get_duration_prop(&props, "timeout", Some(Duration::from_millis(200)))?; + handle_signal_or_value( + &props, + "timeout", + |p, k| get_duration_prop(p, k, Some(Duration::from_millis(200))), + |signal| { + let controller_data = Rc::clone(&controller_data); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(dur) = parse_duration_str(&value) { + controller_data.borrow_mut().cmd_timeout = dur; + } else { + log::error!("Invalid duration string: {}", value); + } + }) + ); + }, + |value| { + controller_data.borrow_mut().cmd_timeout = value; + }, + ); // onclick - command to run when the user selects a date. The `{0}` placeholder will be replaced by the selected day, `{1}` will be replaced by the month, and `{2}` by the year. - if let Ok(onclick) = get_string_prop(&props, "onclick", None) { - connect_signal_handler!( - widget, - widget.connect_day_selected(move |w| { - run_command(timeout, &onclick, &[w.day(), w.month(), w.year()]) - }) - ); - } + handle_signal_or_value( + &props, + "onclick", + |p, k| get_string_prop(p, k, None), + |signal| { + let controller_data = controller_data.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + controller_data.borrow_mut().onclick_cmd = value; + }) + ); + }, + |value| { + controller_data.borrow_mut().onclick_cmd = value; + }, + ); Ok(()) }; - apply_props(&props, >k_widget)?; + apply_props(&props, controller_data.clone(), >k_widget)?; let gtk_widget_clone = gtk_widget.clone(); let update_fn: UpdateFn = Box::new(move |props: &Map| { - let _ = apply_props(props, >k_widget_clone); + let _ = apply_props(props, controller_data.clone(), >k_widget_clone); // now re-apply generic widget attrs if let Err(err) = @@ -3130,22 +4076,162 @@ pub(super) fn build_gtk_scale( // Reusable closure for applying props let apply_props = |props: &Map, widget: >k4::Scale, scale_dat: Rc>| -> Result<()> { - widget.set_inverted(get_bool_prop(props, "flipped", Some(false))?); + handle_signal_or_value( + &props, + "orientation", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + if let Ok(orientation) = parse_orientation(&obj.property::("value")) { + widget.set_orientation(orientation); + } + }) + ); + }, + |value| { + if let Ok(orientation) = parse_orientation(&value) { + widget.set_orientation(orientation); + } + }, + ); - if let Ok(marks) = get_string_prop(props, "marks", None) { - widget.clear_marks(); - for mark in marks.split(',') { - widget.add_mark(mark.trim().parse()?, gtk4::PositionType::Bottom, None); - } - } + handle_signal_or_value( + &props, + "flipped", + |p, k| get_bool_prop(p, k, Some(false)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_inverted(i); + } + }) + ); + }, + |value| widget.set_inverted(value), + ); - widget.set_draw_value(get_bool_prop(props, "draw_value", Some(false))?); + handle_signal_or_value( + &props, + "value_pos", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let marks = obj.property::("value"); + widget.clear_marks(); + for mark in marks.split(',') { + let value = match mark.trim().parse() { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse mark: {}", e); + return; + } + }; + widget.add_mark(value, gtk4::PositionType::Bottom, None); + } + }) + ); + }, + |marks| { + widget.clear_marks(); + for mark in marks.split(',') { + let value = match mark.trim().parse() { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse mark: {}", e); + return; + } + }; + widget.add_mark(value, gtk4::PositionType::Bottom, None); + } + }, + ); - if let Ok(value_pos) = get_string_prop(props, "value_pos", None) { - widget.set_value_pos(parse_position_type(&value_pos)?); - } + handle_signal_or_value( + &props, + "draw_value", + |p, k| get_bool_prop(p, k, Some(false)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_draw_value(i); + } + }) + ); + }, + |value| widget.set_draw_value(value), + ); - widget.set_round_digits(get_i32_prop(props, "round_digits", Some(0))?); + handle_signal_or_value( + &props, + "value_pos", + |p, k| get_string_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value_pos = obj.property::("value"); + let value = match parse_position_type(&value_pos) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse position type: {}", e); + return; + } + }; + widget.set_value_pos(value); + }) + ); + }, + |value_pos| { + let value = match parse_position_type(&value_pos) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to parse position type: {}", e); + return; + } + }; + widget.set_value_pos(value); + }, + ); + + handle_signal_or_value( + &props, + "round_digits", + |p, k| get_i32_prop(p, k, Some(0)), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_round_digits(i); + } + }) + ); + }, + |value| widget.set_round_digits(value), + ); resolve_range_attrs(props, widget.upcast_ref::(), scale_dat)?; Ok(()) @@ -3186,17 +4272,83 @@ pub(super) fn build_gtk_scrolledwindow( let gtk_widget = gtk4::ScrolledWindow::new(); let apply_props = |props: &Map, widget: >k4::ScrolledWindow| -> Result<()> { - let hscroll = get_bool_prop(&props, "hscroll", Some(true))?; - let vscroll = get_bool_prop(&props, "vscroll", Some(true))?; + handle_double_signal_or_value( + &props, + "hscroll", + "vscroll", + |p, k, _| get_bool_prop(p, k, Some(true)), + |p, k, _| get_bool_prop(p, k, Some(true)), + |signal_h, signal_v| { + let widget = widget.clone(); + let props = props.clone(); - widget.set_policy( - if hscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, - if vscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + if let Some(signal) = signal_h { + let widget = widget.clone(); + let props = props.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let hscroll = obj.property::("value"); + let vscroll = get_bool_prop(&props, "vscroll", Some(true)) + .ok() + .unwrap_or(true); + widget.set_policy( + if hscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + if vscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + ); + }) + ); + } + + if let Some(signal) = signal_v { + let widget = widget.clone(); + let props = props.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let vscroll = obj.property::("value"); + let hscroll = get_bool_prop(&props, "hscroll", Some(true)) + .ok() + .unwrap_or(true); + widget.set_policy( + if hscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + if vscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + ); + }) + ); + } + }, + |h_opt, v_opt| { + let hscroll = h_opt.unwrap_or(true); + let vscroll = v_opt.unwrap_or(true); + widget.set_policy( + if hscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + if vscroll { gtk4::PolicyType::Automatic } else { gtk4::PolicyType::Never }, + ); + } ); - if let Ok(natural_height_bool) = get_bool_prop(&props, "propagate_natural_height", None) { - widget.set_propagate_natural_height(natural_height_bool); - } + handle_signal_or_value( + &props, + "propagate_natural_height", + |p, k| get_bool_prop(p, k, None), + |signal| { + let widget = widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + widget.set_propagate_natural_height(i) + } + }) + ); + }, + |value| widget.set_propagate_natural_height(value), + ); Ok(()) }; @@ -3601,20 +4753,78 @@ pub(super) fn resolve_range_attrs( // We keep track of the last value that has been set via gtk_widget.set_value (by a change in the value property). // We do this so we can detect if the new value came from a scripted change or from a user input from within the value_changed handler // and only run on_change when it's caused by manual user input - if let Ok(value) = get_f64_prop(&props, "value", None) { - if !range_dat.borrow().is_being_dragged { - range_dat.borrow_mut().last_set_value = Some(value); - gtk_widget.set_value(value); - } - } + handle_signal_or_value( + &props, + "value", + |p, k| get_f64_prop(p, k, None), + |signal| { + let range_dat = range_dat.clone(); + let gtk_widget = gtk_widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + if !range_dat.borrow().is_being_dragged { + range_dat.borrow_mut().last_set_value = Some(i); + gtk_widget.set_value(i); + } + } + }) + ); + }, + |value| { + if !range_dat.borrow().is_being_dragged { + range_dat.borrow_mut().last_set_value = Some(value); + gtk_widget.set_value(value); + } + }, + ); - if let Ok(min) = get_f64_prop(&props, "min", None) { - gtk_widget.adjustment().set_lower(min) - } + handle_signal_or_value( + &props, + "min", + |p, k| get_f64_prop(p, k, None), + |signal| { + let gtk_widget = gtk_widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + gtk_widget.adjustment().set_lower(i) + } + }) + ); + }, + |value| { + gtk_widget.adjustment().set_lower(value) + }, + ); - if let Ok(max) = get_f64_prop(&props, "max", None) { - gtk_widget.adjustment().set_upper(max) - } + handle_signal_or_value( + &props, + "max", + |p, k| get_f64_prop(p, k, None), + |signal| { + let gtk_widget = gtk_widget.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(i) = value.parse::() { + gtk_widget.adjustment().set_upper(i) + } + }) + ); + }, + |value| { + gtk_widget.adjustment().set_upper(value) + }, + ); let onchange = get_string_prop(&props, "onchange", None).ok(); let timeout = get_duration_prop(&props, "timeout", Some(Duration::from_millis(200)))?; @@ -3624,5 +4834,49 @@ pub(super) fn resolve_range_attrs( range_dat.borrow_mut().cmd_timeout = timeout; } + handle_signal_or_value( + &props, + "onchange", + |p, k| get_string_prop(p, k, None), + |signal| { + let range_dat = range_dat.clone(); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + range_dat.borrow_mut().onchange_cmd = value; + }) + ); + }, + |value| { + range_dat.borrow_mut().onchange_cmd = value; + }, + ); + + handle_signal_or_value( + &props, + "timeout", + |p, k| get_duration_prop(p, k, Some(Duration::from_millis(200))), + |signal| { + let range_dat = Rc::clone(&range_dat); + let signal_widget = signal.data; + connect_signal_handler!( + signal_widget, + signal_widget.connect_notify_local(Some("value"), move |obj, _| { + let value = obj.property::("value"); + if let Ok(dur) = parse_duration_str(&value) { + range_dat.borrow_mut().cmd_timeout = dur; + } else { + log::error!("Invalid duration string: {}", value); + } + }) + ); + }, + |value| { + range_dat.borrow_mut().cmd_timeout = value; + }, + ); + Ok(()) }