fractal/components/
label_with_widgets.rs1use 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 child: gtk::Label,
18 widgets: RefCell<Vec<gtk::Widget>>,
20 widgets_sizes: RefCell<Vec<(i32, i32)>>,
21 #[property(get)]
23 label: RefCell<Option<String>>,
24 #[property(get = Self::uses_markup, set = Self::set_use_markup, explicit_notify)]
26 use_markup: PhantomData<bool>,
27 #[property(get, set = Self::set_ellipsize, explicit_notify)]
29 ellipsize: Cell<bool>,
30 #[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 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 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 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 fn uses_markup(&self) -> bool {
123 self.child.uses_markup()
124 }
125
126 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 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 fn justify(&self) -> gtk::Justification {
153 self.child.justify()
154 }
155
156 fn set_justify(&self, justify: gtk::Justification) {
159 self.child.set_justify(justify);
160 }
161
162 fn invalidate_widgets(&self) {
164 self.widgets_sizes.borrow_mut().clear();
165 self.allocate_shapes();
166 self.obj().queue_resize();
167 }
168
169 fn allocate_shapes(&self) {
171 if self.label.borrow().as_ref().is_none_or(String::is_empty) {
172 return;
174 }
175
176 if self.widgets.borrow().is_empty() {
177 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 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 break;
272 }
273 }
274 }
275
276 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 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 pub struct LabelWithWidgets(ObjectSubclass<imp::LabelWithWidgets>)
320 @extends gtk::Widget, @implements gtk::Accessible;
321}
322
323impl LabelWithWidgets {
324 pub(crate) const PLACEHOLDER: &'static str = "<widget>";
326
327 pub fn new() -> Self {
329 glib::Object::new()
330 }
331
332 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}