fractal/utils/
fixed_selection.rs1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2
3use crate::utils::BoundObject;
4
5pub(crate) type EquivalentObjectFn = dyn Fn(&glib::Object, &glib::Object) -> bool;
7
8mod imp {
9 use std::{
10 cell::{Cell, RefCell},
11 fmt,
12 marker::PhantomData,
13 };
14
15 use super::*;
16
17 #[derive(glib::Properties)]
18 #[properties(wrapper_type = super::FixedSelection)]
19 pub struct FixedSelection {
20 #[property(get, set = Self::set_model, explicit_notify, nullable)]
22 model: BoundObject<gio::ListModel>,
23 pub(super) item_equivalence_fn: RefCell<Option<Box<EquivalentObjectFn>>>,
33 #[property(get, set = Self::set_selected, explicit_notify, default = gtk::INVALID_LIST_POSITION)]
35 selected: Cell<u32>,
36 #[property(get, set = Self::set_selected_item, explicit_notify, nullable)]
38 selected_item: RefCell<Option<glib::Object>>,
39 #[property(get = Self::is_empty)]
41 is_empty: PhantomData<bool>,
42 }
43
44 impl fmt::Debug for FixedSelection {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_struct("FixedSelection")
47 .field("model", &self.model)
48 .field("selected", &self.selected)
49 .field("selected_item", &self.selected_item)
50 .finish_non_exhaustive()
51 }
52 }
53
54 impl Default for FixedSelection {
55 fn default() -> Self {
56 Self {
57 model: Default::default(),
58 item_equivalence_fn: Default::default(),
59 selected: Cell::new(gtk::INVALID_LIST_POSITION),
60 selected_item: Default::default(),
61 is_empty: Default::default(),
62 }
63 }
64 }
65
66 #[glib::object_subclass]
67 impl ObjectSubclass for FixedSelection {
68 const NAME: &'static str = "FixedSelection";
69 type Type = super::FixedSelection;
70 type Interfaces = (gio::ListModel, gtk::SelectionModel);
71 }
72
73 #[glib::derived_properties]
74 impl ObjectImpl for FixedSelection {}
75
76 impl ListModelImpl for FixedSelection {
77 fn item_type(&self) -> glib::Type {
78 glib::Object::static_type()
79 }
80
81 fn n_items(&self) -> u32 {
82 self.model.obj().map(|m| m.n_items()).unwrap_or_default()
83 }
84
85 fn item(&self, position: u32) -> Option<glib::Object> {
86 self.model.obj()?.item(position)
87 }
88 }
89
90 impl SelectionModelImpl for FixedSelection {
91 fn selection_in_range(&self, _position: u32, _n_items: u32) -> gtk::Bitset {
92 let bitset = gtk::Bitset::new_empty();
93 let selected = self.selected.get();
94
95 if selected != gtk::INVALID_LIST_POSITION {
96 bitset.add(selected);
97 }
98
99 bitset
100 }
101
102 fn is_selected(&self, position: u32) -> bool {
103 self.selected.get() == position
104 }
105 }
106
107 impl FixedSelection {
108 fn set_model(&self, model: Option<gio::ListModel>) {
110 let prev_model = self.model.obj();
111
112 if prev_model == model {
113 return;
114 }
115
116 let prev_n_items = prev_model
117 .as_ref()
118 .map(ListModelExt::n_items)
119 .unwrap_or_default();
120 let n_items = model
121 .as_ref()
122 .map(ListModelExt::n_items)
123 .unwrap_or_default();
124
125 self.model.disconnect_signals();
126
127 let obj = self.obj();
128 let _guard = obj.freeze_notify();
129
130 if let Some(model) = model {
131 let items_changed_handler = model.connect_items_changed(clone!(
132 #[weak(rename_to = imp)]
133 self,
134 move |m, p, r, a| {
135 imp.items_changed_cb(m, p, r, a);
136 }
137 ));
138
139 self.model.set(model, vec![items_changed_handler]);
140 }
141
142 if self.selected.get() != gtk::INVALID_LIST_POSITION {
143 self.selected.replace(gtk::INVALID_LIST_POSITION);
144 obj.notify_selected();
145 }
146 if self.selected_item.borrow().is_some() {
147 self.selected_item.replace(None);
148 obj.notify_selected_item();
149 }
150
151 if prev_n_items > 0 || n_items > 0 {
152 obj.items_changed(0, prev_n_items, n_items);
153 }
154 if (prev_n_items > 0 && n_items == 0) || (prev_n_items == 0 && n_items > 0) {
155 obj.notify_is_empty();
156 }
157
158 obj.notify_model();
159 }
160
161 fn set_selected(&self, position: u32) {
163 let prev_selected = self.selected.get();
164 if prev_selected == position {
165 return;
166 }
167
168 let selected_item = self.model.obj().and_then(|m| m.item(position));
169
170 let selected = if selected_item.is_none() {
171 gtk::INVALID_LIST_POSITION
172 } else {
173 position
174 };
175
176 if prev_selected == selected {
177 return;
178 }
179 let obj = self.obj();
180
181 self.selected.replace(selected);
182 self.selected_item.replace(selected_item);
183
184 if prev_selected == gtk::INVALID_LIST_POSITION {
185 obj.selection_changed(selected, 1);
186 } else if selected == gtk::INVALID_LIST_POSITION {
187 obj.selection_changed(prev_selected, 1);
188 } else if selected < prev_selected {
189 obj.selection_changed(selected, prev_selected - selected + 1);
190 } else {
191 obj.selection_changed(prev_selected, selected - prev_selected + 1);
192 }
193
194 obj.notify_selected();
195 obj.notify_selected_item();
196 }
197
198 fn set_selected_item(&self, item: Option<glib::Object>) {
200 if *self.selected_item.borrow() == item {
201 return;
202 }
203 let obj = self.obj();
204
205 let prev_selected = self.selected.get();
206 let mut selected = gtk::INVALID_LIST_POSITION;
207
208 if item.is_some()
209 && let Some(model) = self.model.obj()
210 {
211 for i in 0..model.n_items() {
212 let current_item = model.item(i);
213 if current_item == item {
214 selected = i;
215 break;
216 }
217 }
218 }
219
220 self.selected_item.replace(item);
221
222 if prev_selected != selected {
223 self.selected.replace(selected);
224
225 if prev_selected == gtk::INVALID_LIST_POSITION {
226 obj.selection_changed(selected, 1);
227 } else if selected == gtk::INVALID_LIST_POSITION {
228 obj.selection_changed(prev_selected, 1);
229 } else if selected < prev_selected {
230 obj.selection_changed(selected, prev_selected - selected + 1);
231 } else {
232 obj.selection_changed(prev_selected, selected - prev_selected + 1);
233 }
234 obj.notify_selected();
235 }
236
237 obj.notify_selected_item();
238 }
239
240 fn is_empty(&self) -> bool {
242 self.model.obj().is_none_or(|model| model.n_items() == 0)
243 }
244
245 fn items_changed_cb(
247 &self,
248 model: &gio::ListModel,
249 position: u32,
250 removed: u32,
251 added: u32,
252 ) {
253 let obj = self.obj();
254 let _guard = obj.freeze_notify();
255
256 let selected = self.selected.get();
257 let selected_item = self.selected_item.borrow().clone();
258
259 if selected_item.is_none() || selected < position {
260 } else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
262 self.selected.set(selected + added - removed);
263 obj.notify_selected();
264 } else {
265 let mut found = false;
266 let item_equivalence_fn = self.item_equivalence_fn.borrow();
267
268 for i in position..(position + added) {
269 let item = model.item(i);
270
271 if item.as_ref().zip(selected_item.as_ref()).is_some_and(
272 |(item, selected_item)| {
273 if let Some(item_equivalence_fn) = &*item_equivalence_fn {
274 item_equivalence_fn(item, selected_item)
275 } else {
276 item == selected_item
277 }
278 },
279 ) {
280 if selected != i {
281 self.selected.set(i);
283 obj.notify_selected();
284 }
285
286 if item != selected_item {
287 self.selected_item.replace(item);
289 obj.notify_selected_item();
290 }
291
292 found = true;
293 break;
294 }
295 }
296
297 if !found {
298 self.selected.set(gtk::INVALID_LIST_POSITION);
300 obj.notify_selected();
301 }
302 }
303
304 obj.items_changed(position, removed, added);
305
306 let n_items = model.n_items();
307 if n_items == 0 || (removed == 0 && n_items == added) {
308 obj.notify_is_empty();
309 }
310 }
311 }
312}
313
314glib::wrapper! {
315 pub struct FixedSelection(ObjectSubclass<imp::FixedSelection>)
318 @implements gio::ListModel, gtk::SelectionModel;
319}
320
321impl FixedSelection {
322 pub fn new(model: Option<&impl IsA<gio::ListModel>>) -> Self {
324 glib::Object::builder().property("model", model).build()
325 }
326
327 pub(crate) fn set_item_equivalence_fn(
337 &self,
338 equivalence_fn: impl Fn(&glib::Object, &glib::Object) -> bool + 'static,
339 ) {
340 self.imp()
341 .item_equivalence_fn
342 .replace(Some(Box::new(equivalence_fn)));
343 }
344}
345
346impl Default for FixedSelection {
347 fn default() -> Self {
348 Self::new(None::<&gio::ListModel>)
349 }
350}