[fractal/fractal-next] components: Add LabelWithWidgets



commit 1c777a21231f9606232315994961b4bd45ff2825
Author: Julian Sparber <julian sparber net>
Date:   Wed May 26 11:04:14 2021 +0200

    components: Add LabelWithWidgets
    
    This is a Label that places other widgets at the position
    where "<widget>" is found inside the label string. The user can also set
    a custom placeholder if needed.

 po/POTFILES.in                       |   1 +
 src/components/label_with_widgets.rs | 361 +++++++++++++++++++++++++++++++++++
 src/components/mod.rs                |   2 +
 src/meson.build                      |   1 +
 4 files changed, 365 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 18323249..89fe36a3 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -29,6 +29,7 @@ data/resources/ui/window.ui
 # Rust files
 src/application.rs
 src/components/context_menu_bin.rs
+src/components/label_with_widgets.rs
 src/components/mod.rs
 src/components/spinner_button.rs
 src/components/pill.rs
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
new file mode 100644
index 00000000..2bced280
--- /dev/null
+++ b/src/components/label_with_widgets.rs
@@ -0,0 +1,361 @@
+use gtk::pango;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, glib::clone};
+use std::cmp::max;
+
+const DEFAULT_PLACEHOLDER: &str = "<widget>";
+const PANGO_SCALE: i32 = 1024;
+const OBJECT_REPLACEMENT_CHARACTER: &str = "\u{FFFC}";
+fn pango_pixels(d: i32) -> i32 {
+    (d + 512) >> 10
+}
+
+mod imp {
+    use super::*;
+    use std::cell::RefCell;
+
+    #[derive(Debug)]
+    pub struct LabelWithWidgets {
+        pub widgets: RefCell<Vec<gtk::Widget>>,
+        pub widgets_sizes: RefCell<Vec<(i32, i32)>>,
+        pub label: gtk::Label,
+        pub placeholder: RefCell<Option<String>>,
+        pub text: RefCell<Option<String>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for LabelWithWidgets {
+        const NAME: &'static str = "LabelWithWidgets";
+        type Type = super::LabelWithWidgets;
+        type ParentType = gtk::Widget;
+        type Interfaces = (gtk::Buildable,);
+
+        fn new() -> Self {
+            Self {
+                label: gtk::LabelBuilder::new().wrap(true).build(),
+                widgets: Default::default(),
+                widgets_sizes: Default::default(),
+                placeholder: Default::default(),
+                text: Default::default(),
+            }
+        }
+    }
+
+    impl ObjectImpl for LabelWithWidgets {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_string(
+                        "label",
+                        "Label",
+                        "The label",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_string(
+                        "placeholder",
+                        "Placeholder",
+                        "The placeholder that is replaced with widgets",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "label" => obj.set_label(value.get().unwrap()),
+                "placeholder" => obj.set_placeholder(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "label" => obj.label().to_value(),
+                "placeholder" => obj.placeholder().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+            self.label.set_parent(obj);
+            self.label.connect_notify_local(
+                Some("label"),
+                clone!(@weak obj => move |_, _| {
+                    obj.invalidate_child_widgets();
+                }),
+            );
+        }
+
+        fn dispose(&self, _obj: &Self::Type) {
+            self.label.unparent();
+            for widget in self.widgets.borrow().iter() {
+                widget.unparent();
+            }
+        }
+    }
+
+    impl WidgetImpl for LabelWithWidgets {
+        fn measure(
+            &self,
+            _widget: &Self::Type,
+            orientation: gtk::Orientation,
+            for_size: i32,
+        ) -> (i32, i32, i32, i32) {
+            let (mut minimum, mut natural, mut minimum_baseline, mut natural_baseline) =
+                if self.label.should_layout() {
+                    self.label.measure(orientation, for_size)
+                } else {
+                    (-1, -1, -1, -1)
+                };
+
+            for child in self.widgets.borrow().iter() {
+                if self.label.should_layout() {
+                    let (child_min, child_nat, child_min_baseline, child_nat_baseline) =
+                        child.measure(orientation, for_size);
+
+                    minimum = max(minimum, child_min);
+                    natural = max(natural, child_nat);
+
+                    if child_min_baseline > -1 {
+                        minimum_baseline = max(minimum_baseline, child_min_baseline);
+                    }
+                    if child_nat_baseline > -1 {
+                        natural_baseline = max(natural_baseline, child_nat_baseline);
+                    }
+                }
+            }
+            (minimum, natural, minimum_baseline, natural_baseline)
+        }
+
+        fn size_allocate(&self, widget: &Self::Type, width: i32, height: i32, baseline: i32) {
+            // The order of the widget allocation is important.
+            widget.allocate_shapes();
+            self.label.allocate(width, height, baseline, None);
+            widget.allocate_children();
+        }
+
+        fn request_mode(&self, _widget: &Self::Type) -> gtk::SizeRequestMode {
+            self.label.request_mode()
+        }
+    }
+
+    impl BuildableImpl for LabelWithWidgets {
+        fn add_child(
+            &self,
+            buildable: &Self::Type,
+            builder: &gtk::Builder,
+            child: &glib::Object,
+            type_: Option<&str>,
+        ) {
+            if let Some(child) = child.downcast_ref::<gtk::Widget>() {
+                buildable.append_child(child);
+            } else {
+                self.parent_add_child(buildable, builder, child, type_)
+            }
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct LabelWithWidgets(ObjectSubclass<imp::LabelWithWidgets>)
+        @extends gtk::Widget, @implements gtk::Accessible, gtk::Buildable;
+}
+/// A Label that can have multiple widgets placed inside the text.
+///
+/// By default the string "<widget>" will be used as location to place the child
+/// widgets. You can set your own placeholder if you need.
+impl LabelWithWidgets {
+    pub fn new<P: IsA<gtk::Widget>>(label: &str, widgets: Vec<P>) -> Self {
+        let obj: Self =
+            glib::Object::new(&[("label", &label)]).expect("Failed to create LabelWithWidgets");
+        // FIXME: use a property for widgets
+        obj.set_widgets(widgets);
+        obj
+    }
+
+    pub fn append_child<P: IsA<gtk::Widget>>(&self, child: &P) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        priv_.widgets.borrow_mut().push(child.clone().upcast());
+        child.set_parent(self);
+        self.invalidate_child_widgets();
+    }
+
+    pub fn set_widgets<P: IsA<gtk::Widget>>(&self, widgets: Vec<P>) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+
+        priv_.widgets.borrow_mut().clear();
+        priv_
+            .widgets
+            .borrow_mut()
+            .append(&mut widgets.into_iter().map(|w| w.upcast()).collect());
+
+        for child in priv_.widgets.borrow().iter() {
+            child.set_parent(self);
+        }
+        self.invalidate_child_widgets();
+    }
+
+    pub fn widgets(&self) -> Vec<gtk::Widget> {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        priv_.widgets.borrow().to_owned()
+    }
+
+    pub fn set_label(&self, label: Option<String>) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+
+        if priv_.text.borrow().as_ref() == label.as_ref() {
+            return;
+        }
+
+        if let Some(ref label) = label {
+            let placeholder = priv_.placeholder.borrow();
+            let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
+            let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
+            priv_.label.set_text(&label);
+        }
+
+        priv_.text.replace(label);
+        self.invalidate_child_widgets();
+        self.notify("label");
+    }
+
+    pub fn label(&self) -> Option<String> {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        priv_.text.borrow().to_owned()
+    }
+
+    pub fn set_placeholder(&self, placeholder: Option<String>) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+
+        if priv_.placeholder.borrow().as_ref() == placeholder.as_ref() {
+            return;
+        }
+
+        if let Some(text) = &*priv_.text.borrow() {
+            let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
+            let label = text.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
+            priv_.label.set_text(&label);
+        }
+
+        priv_.placeholder.replace(placeholder);
+        self.invalidate_child_widgets();
+        self.notify("placeholder");
+    }
+
+    pub fn placeholder(&self) -> Option<String> {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        priv_.placeholder.borrow().to_owned()
+    }
+
+    fn invalidate_child_widgets(&self) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        priv_.widgets_sizes.borrow_mut().clear();
+        self.queue_resize();
+    }
+
+    fn allocate_shapes(&self) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        let mut widgets_sizes = priv_.widgets_sizes.borrow_mut();
+
+        let mut child_size_changed = false;
+        for (i, child) in priv_.widgets.borrow().iter().enumerate() {
+            let (_, natural_size) = child.preferred_size();
+            let width = natural_size.width;
+            let height = natural_size.height;
+            if let Some((old_width, old_height)) = widgets_sizes.get(i) {
+                if old_width != &width || old_height != &height {
+                    let _ = std::mem::replace(&mut widgets_sizes[i], (width, height));
+                    child_size_changed = true;
+                }
+            } else {
+                widgets_sizes.insert(i, (width, height));
+                child_size_changed = true;
+            }
+        }
+
+        if !child_size_changed {
+            return;
+        }
+
+        let attrs = pango::AttrList::new();
+        for (i, (start_index, _)) in priv_
+            .label
+            .text()
+            .as_str()
+            .match_indices(OBJECT_REPLACEMENT_CHARACTER)
+            .enumerate()
+        {
+            if let Some((width, height)) = widgets_sizes.get(i) {
+                let logical_rect = pango::Rectangle::new(
+                    0,
+                    -(height - (height / 4)) * PANGO_SCALE,
+                    width * PANGO_SCALE,
+                    height * PANGO_SCALE,
+                );
+
+                let mut shape = pango::Attribute::new_shape(&logical_rect, &logical_rect);
+                shape.set_start_index(start_index as u32);
+                shape.set_end_index((start_index + OBJECT_REPLACEMENT_CHARACTER.len()) as u32);
+                attrs.insert(shape);
+            } else {
+                break;
+            }
+        }
+        priv_.label.set_attributes(Some(&attrs));
+    }
+
+    fn allocate_children(&self) {
+        let priv_ = imp::LabelWithWidgets::from_instance(self);
+        let widgets = priv_.widgets.borrow();
+        let widgets_sizes = priv_.widgets_sizes.borrow();
+
+        let mut run_iter = priv_.label.layout().iter().unwrap();
+        let mut i = 0;
+        loop {
+            if let Some(run) = run_iter.run_readonly() {
+                if run
+                    .item()
+                    .analysis()
+                    .extra_attrs()
+                    .iter()
+                    .find(|attr| attr.type_() == pango::AttrType::Shape)
+                    .is_some()
+                {
+                    if let Some(widget) = widgets.get(i) {
+                        let (width, height) = widgets_sizes[i];
+                        let (_, extents) = run_iter.run_extents();
+
+                        let (offset_x, offset_y) = priv_.label.layout_offsets();
+                        let allocation = gtk::Allocation {
+                            x: pango_pixels(extents.x) + offset_x,
+                            y: pango_pixels(extents.y) + offset_y,
+                            width,
+                            height,
+                        };
+                        widget.size_allocate(&allocation, -1);
+                        i += 1;
+                    } else {
+                        break;
+                    }
+                }
+            }
+            if !run_iter.next_run() {
+                break;
+            }
+        }
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 1db7ca05..04674049 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,7 +1,9 @@
 mod context_menu_bin;
+mod label_with_widgets;
 mod pill;
 mod spinner_button;
 
 pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinImpl};
+pub use self::label_with_widgets::LabelWithWidgets;
 pub use self::pill::Pill;
 pub use self::spinner_button::SpinnerButton;
diff --git a/src/meson.build b/src/meson.build
index a14a4c88..2fd00aca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -21,6 +21,7 @@ run_command(
 sources = files(
   'application.rs',
   'components/context_menu_bin.rs',
+  'components/label_with_widgets.rs',
   'components/mod.rs',
   'components/pill.rs',
   'components/spinner_button.rs',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]