fractal/components/
label_with_widgets.rs

1use gtk::{glib, pango, prelude::*, subclass::prelude::*};
2
3const OBJECT_REPLACEMENT_CHARACTER: &str = "\u{FFFC}";
4
5mod imp {
6    use std::{
7        cell::{Cell, RefCell},
8        marker::PhantomData,
9    };
10
11    use super::*;
12
13    #[derive(Debug, Default, glib::Properties)]
14    #[properties(wrapper_type = super::LabelWithWidgets)]
15    pub struct LabelWithWidgets {
16        /// The child `GtkLabel`.
17        child: gtk::Label,
18        /// The widgets to display in the label.
19        widgets: RefCell<Vec<gtk::Widget>>,
20        widgets_sizes: RefCell<Vec<(i32, i32)>>,
21        /// The text of the label.
22        #[property(get)]
23        label: RefCell<Option<String>>,
24        /// Whether the label includes Pango markup.
25        #[property(get = Self::uses_markup, set = Self::set_use_markup, explicit_notify)]
26        use_markup: PhantomData<bool>,
27        /// Whether the label should be ellipsized.
28        #[property(get, set = Self::set_ellipsize, explicit_notify)]
29        ellipsize: Cell<bool>,
30        /// The alignment of the lines in the text of the label, relative to
31        /// each other.
32        #[property(get = Self::justify, set = Self::set_justify, builder(gtk::Justification::Left))]
33        justify: PhantomData<gtk::Justification>,
34    }
35
36    #[glib::object_subclass]
37    impl ObjectSubclass for LabelWithWidgets {
38        const NAME: &'static str = "LabelWithWidgets";
39        type Type = super::LabelWithWidgets;
40        type ParentType = gtk::Widget;
41    }
42
43    #[glib::derived_properties]
44    impl ObjectImpl for LabelWithWidgets {
45        fn constructed(&self) {
46            self.parent_constructed();
47            let obj = self.obj();
48
49            let child = &self.child;
50            child.set_parent(&*obj);
51            child.set_wrap(true);
52            child.set_wrap_mode(pango::WrapMode::WordChar);
53            child.set_xalign(0.0);
54            child.set_valign(gtk::Align::Start);
55        }
56
57        fn dispose(&self) {
58            self.child.unparent();
59
60            for widget in self.widgets.borrow().iter() {
61                widget.unparent();
62            }
63        }
64    }
65
66    impl WidgetImpl for LabelWithWidgets {
67        fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
68            self.allocate_shapes();
69            self.child.measure(orientation, for_size)
70        }
71
72        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
73            self.child.allocate(width, height, baseline, None);
74            self.allocate_widgets();
75        }
76
77        fn request_mode(&self) -> gtk::SizeRequestMode {
78            self.child.request_mode()
79        }
80    }
81
82    impl LabelWithWidgets {
83        /// Set the label and widgets to display.
84        pub(super) fn set_label_and_widgets<P: IsA<gtk::Widget>>(
85            &self,
86            label: String,
87            widgets: Vec<P>,
88        ) {
89            self.set_label(Some(label));
90            self.set_widgets(widgets);
91
92            self.update();
93        }
94
95        /// Set the widgets to display.
96        fn set_widgets<P: IsA<gtk::Widget>>(&self, widgets: Vec<P>) {
97            for widget in self.widgets.borrow_mut().drain(..) {
98                widget.unparent();
99            }
100
101            self.widgets
102                .borrow_mut()
103                .extend(widgets.into_iter().map(Cast::upcast));
104
105            let obj = self.obj();
106            for child in self.widgets.borrow().iter() {
107                child.set_parent(&*obj);
108            }
109        }
110
111        /// Set the text of the label.
112        fn set_label(&self, label: Option<String>) {
113            if *self.label.borrow() == label {
114                return;
115            }
116
117            self.label.replace(label);
118            self.obj().notify_label();
119        }
120
121        /// Whether the label includes Pango markup.
122        fn uses_markup(&self) -> bool {
123            self.child.uses_markup()
124        }
125
126        /// Set whether the label includes Pango markup.
127        fn set_use_markup(&self, use_markup: bool) {
128            if self.uses_markup() == use_markup {
129                return;
130            }
131
132            self.child.set_use_markup(use_markup);
133
134            self.invalidate_widgets();
135            self.obj().notify_use_markup();
136        }
137
138        /// Sets whether the text of the label should be ellipsized.
139        fn set_ellipsize(&self, ellipsize: bool) {
140            if self.ellipsize.get() == ellipsize {
141                return;
142            }
143
144            self.ellipsize.set(true);
145
146            self.update();
147            self.obj().notify_ellipsize();
148        }
149
150        /// The alignment of the lines in the text of the label, relative to
151        /// each other.
152        fn justify(&self) -> gtk::Justification {
153            self.child.justify()
154        }
155
156        /// Set the alignment of the lines in the text of the label, relative to
157        /// each other.
158        fn set_justify(&self, justify: gtk::Justification) {
159            self.child.set_justify(justify);
160        }
161
162        /// Re-compute the child widgets allocations in the Pango layout.
163        fn invalidate_widgets(&self) {
164            self.widgets_sizes.borrow_mut().clear();
165            self.allocate_shapes();
166            self.obj().queue_resize();
167        }
168
169        /// Allocate shapes in the Pango layout for the child widgets.
170        fn allocate_shapes(&self) {
171            if self.label.borrow().as_ref().is_none_or(String::is_empty) {
172                // No need to compute shapes if the label is empty.
173                return;
174            }
175
176            if self.widgets.borrow().is_empty() {
177                // There should be no attributes if there are no widgets.
178                self.child.set_attributes(None);
179                return;
180            }
181
182            let mut widgets_sizes = self.widgets_sizes.borrow_mut();
183
184            let mut child_size_changed = false;
185            for (i, child) in self.widgets.borrow().iter().enumerate() {
186                let (_, natural_size) = child.preferred_size();
187                let width = natural_size.width();
188                let height = natural_size.height();
189                if let Some((old_width, old_height)) = widgets_sizes.get(i) {
190                    if old_width != &width || old_height != &height {
191                        let _ = std::mem::replace(&mut widgets_sizes[i], (width, height));
192                        child_size_changed = true;
193                    }
194                } else {
195                    widgets_sizes.insert(i, (width, height));
196                    child_size_changed = true;
197                }
198            }
199
200            if !child_size_changed {
201                return;
202            }
203
204            let attrs = pango::AttrList::new();
205
206            for (i, (start_index, _)) in self
207                .child
208                .text()
209                .as_str()
210                .match_indices(OBJECT_REPLACEMENT_CHARACTER)
211                .enumerate()
212            {
213                if let Some((width, height)) = widgets_sizes.get(i) {
214                    let logical_rect = pango::Rectangle::new(
215                        0,
216                        -(height - (height / 4)) * pango::SCALE,
217                        width * pango::SCALE,
218                        height * pango::SCALE,
219                    );
220
221                    let mut shape = pango::AttrShape::new(&logical_rect, &logical_rect);
222                    shape.set_start_index(start_index as u32);
223                    shape.set_end_index((start_index + OBJECT_REPLACEMENT_CHARACTER.len()) as u32);
224                    attrs.insert(shape);
225                } else {
226                    break;
227                }
228            }
229
230            self.child.set_attributes(Some(&attrs));
231        }
232
233        /// Allocate the child widgets.
234        fn allocate_widgets(&self) {
235            let widgets = self.widgets.borrow();
236            let widgets_sizes = self.widgets_sizes.borrow();
237
238            let mut run_iter = self.child.layout().iter();
239            let mut i = 0;
240            loop {
241                if let Some(run) = run_iter.run_readonly() {
242                    if run
243                        .item()
244                        .analysis()
245                        .extra_attrs()
246                        .iter()
247                        .any(|attr| attr.type_() == pango::AttrType::Shape)
248                    {
249                        if let Some(widget) = widgets.get(i) {
250                            let (width, height) = widgets_sizes[i];
251                            let (_, mut extents) = run_iter.run_extents();
252                            pango::extents_to_pixels(Some(&mut extents), None);
253
254                            let (offset_x, offset_y) = self.child.layout_offsets();
255                            let allocation = gtk::Allocation::new(
256                                extents.x() + offset_x,
257                                extents.y() + offset_y,
258                                width,
259                                height,
260                            );
261                            widget.size_allocate(&allocation, -1);
262                            i += 1;
263                        } else {
264                            break;
265                        }
266                    }
267                }
268
269                if !run_iter.next_run() {
270                    // We are at the end of the Pango layout.
271                    break;
272                }
273            }
274        }
275
276        /// Update this label for the current text and child widgets.
277        fn update(&self) {
278            let old_label = self.child.text();
279            let old_ellipsize = self.child.ellipsize() == pango::EllipsizeMode::End;
280            let new_ellipsize = self.ellipsize.get();
281
282            let new_label = if let Some(label) = self.label.borrow().as_ref() {
283                let placeholder = <Self as ObjectSubclass>::Type::PLACEHOLDER;
284                let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
285
286                if new_ellipsize {
287                    if let Some(pos) = label.find('\n') {
288                        format!("{}…", &label[0..pos])
289                    } else {
290                        label
291                    }
292                } else {
293                    label
294                }
295            } else {
296                String::new()
297            };
298
299            if old_ellipsize != new_ellipsize || old_label != new_label {
300                if new_ellipsize {
301                    // Workaround: if both wrap and ellipsize are set, and there are
302                    // widgets inserted, GtkLabel reports an erroneous minimum width.
303                    self.child.set_wrap(false);
304                    self.child.set_ellipsize(pango::EllipsizeMode::End);
305                } else {
306                    self.child.set_wrap(true);
307                    self.child.set_ellipsize(pango::EllipsizeMode::None);
308                }
309
310                self.child.set_label(&new_label);
311                self.invalidate_widgets();
312            }
313        }
314    }
315}
316
317glib::wrapper! {
318    /// A Label that can have multiple widgets placed inside the text.
319    pub struct LabelWithWidgets(ObjectSubclass<imp::LabelWithWidgets>)
320        @extends gtk::Widget, @implements gtk::Accessible;
321}
322
323impl LabelWithWidgets {
324    /// The placeholder used to mark the locations of widgets in the label.
325    pub(crate) const PLACEHOLDER: &'static str = "<widget>";
326
327    /// Create an empty `LabelWithWidget`.
328    pub fn new() -> Self {
329        glib::Object::new()
330    }
331
332    /// Set the label and widgets to display.
333    pub(crate) fn set_label_and_widgets<P: IsA<gtk::Widget>>(
334        &self,
335        label: String,
336        widgets: Vec<P>,
337    ) {
338        self.imp().set_label_and_widgets(label, widgets);
339    }
340}
341
342impl Default for LabelWithWidgets {
343    fn default() -> Self {
344        Self::new()
345    }
346}