fractal/utils/grouping_list_model/
mod.rs1use std::{cmp::Ordering, fmt, ops::RangeInclusive};
2
3use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
4
5mod group;
6#[cfg(test)]
7mod tests;
8
9pub(crate) use self::group::GroupingListGroup;
10use crate::utils::BoundObject;
11
12pub(crate) type GroupFn = dyn Fn(&glib::Object, &glib::Object) -> bool;
18
19mod imp {
20 use std::{
21 cell::{OnceCell, RefCell},
22 collections::{HashSet, VecDeque},
23 };
24
25 use super::*;
26
27 #[derive(Default, glib::Properties)]
28 #[properties(wrapper_type = super::GroupingListModel)]
29 pub struct GroupingListModel {
30 #[property(get, set = Self::set_model, explicit_notify, nullable)]
32 model: BoundObject<gio::ListModel>,
33 pub(super) group_fn: OnceCell<Box<GroupFn>>,
35 items: RefCell<VecDeque<GroupingListItem>>,
37 }
38
39 impl fmt::Debug for GroupingListModel {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.debug_struct("GroupingListModel")
42 .field("model", &self.model)
43 .field("items", &self.items)
44 .finish_non_exhaustive()
45 }
46 }
47
48 #[glib::object_subclass]
49 impl ObjectSubclass for GroupingListModel {
50 const NAME: &'static str = "GroupingListModel";
51 type Type = super::GroupingListModel;
52 type Interfaces = (gio::ListModel,);
53 }
54
55 #[glib::derived_properties]
56 impl ObjectImpl for GroupingListModel {}
57
58 impl ListModelImpl for GroupingListModel {
59 fn item_type(&self) -> glib::Type {
60 glib::Object::static_type()
61 }
62
63 fn n_items(&self) -> u32 {
64 self.items.borrow().len() as u32
65 }
66
67 fn item(&self, position: u32) -> Option<glib::Object> {
68 let model = self.model.obj()?;
69 self.items
70 .borrow()
71 .get(position as usize)
72 .and_then(|item| match item {
73 GroupingListItem::Singleton(position) => model.item(*position),
74 GroupingListItem::Group(obj) => Some(obj.clone().upcast()),
75 })
76 }
77 }
78
79 impl GroupingListModel {
80 fn group_fn(&self) -> &GroupFn {
82 self.group_fn.get().expect("group Fn should be initialized")
83 }
84
85 fn set_model(&self, model: Option<gio::ListModel>) {
87 let prev_model = self.model.obj();
88
89 if prev_model == model {
90 return;
91 }
92
93 self.model.disconnect_signals();
94
95 if let Some(model) = model {
96 let items_changed_handler = model.connect_items_changed(clone!(
97 #[weak(rename_to = imp)]
98 self,
99 move |model, position, removed, added| {
100 imp.items_changed(model, position, removed, added);
101 }
102 ));
103
104 self.model.set(model.clone(), vec![items_changed_handler]);
105
106 let removed = prev_model.map(|model| model.n_items()).unwrap_or_default();
107 self.items_changed(&model, 0, removed, model.n_items());
108 } else {
109 let removed = self.n_items();
110 self.items.borrow_mut().clear();
111 self.obj().items_changed(0, removed, 0);
112 }
113
114 self.obj().notify_model();
115 }
116
117 fn model_position_to_index(&self, position: u32) -> Option<usize> {
120 for (index, item) in self.items.borrow().iter().enumerate() {
121 if item.contains(position) {
122 return Some(index);
123 }
124
125 if item.end() > position {
128 return None;
129 }
130 }
131
132 None
133 }
134
135 #[allow(clippy::too_many_lines)]
137 fn items_changed(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
138 if removed == 0 && added == 0 {
139 return;
141 }
142
143 let index_before_changes = position
146 .checked_sub(1)
147 .and_then(|position| self.model_position_to_index(position));
148
149 let mut replaced_list_items = HashSet::new();
150
151 let mut list_items_removed =
152 self.items_removed(position, removed, index_before_changes);
153 let mut list_items_added = self.items_added(
154 model,
155 position,
156 added,
157 &mut replaced_list_items,
158 index_before_changes,
159 );
160
161 let position_after_changes = position + added;
162 let mut index_after_changes = self.model_position_to_index(position_after_changes);
163
164 if let Some(index_after_changes) =
167 index_after_changes.as_mut().filter(|index| **index > 0)
168 {
169 let mut items = self.items.borrow_mut();
170
171 let previous_item_in_other_list_item = !items
172 .get(*index_after_changes)
173 .expect("list item index should be valid")
174 .contains(position_after_changes - 1);
175
176 if previous_item_in_other_list_item {
177 let item_after_changes = model
178 .item(position_after_changes)
179 .expect("item position should be valid");
180 let previous_item = model
181 .item(position_after_changes - 1)
182 .expect("item position should be valid");
183
184 if self.group_fn()(&item_after_changes, &previous_item) {
185 *index_after_changes -= 1;
187
188 let (removed_list_item, list_item_to_merge_into) = if index_before_changes
189 .is_some_and(|index| index == *index_after_changes)
190 {
191 let list_item_after_changes = items
193 .remove(*index_after_changes + 1)
194 .expect("list item index should be valid");
195 let list_item_before_changes = items
196 .get_mut(*index_after_changes)
197 .expect("list item index should be valid");
198 (list_item_after_changes, list_item_before_changes)
199 } else {
200 let previous_list_item = items
202 .remove(*index_after_changes)
203 .expect("list item index should be valid");
204 let list_item_after_changes = items
205 .get_mut(*index_after_changes)
206 .expect("list item index should be valid");
207 (previous_list_item, list_item_after_changes)
208 };
209
210 let list_item_replacement = list_item_to_merge_into.add(
211 removed_list_item.start(),
212 removed_list_item.len(),
213 model,
214 );
215
216 if let Some(replacement) = list_item_replacement {
217 *list_item_to_merge_into = replacement;
218 replaced_list_items.insert(*index_after_changes);
219 }
220
221 if let Some(added) = list_items_added.checked_sub(1) {
222 list_items_added = added;
223 } else {
224 list_items_removed += 1;
225 }
226 }
227 }
228 }
229
230 let obj = self.obj();
231
232 if list_items_removed > 0 || list_items_added > 0 {
233 let index_at_changes = index_before_changes
234 .map(|index| index + 1)
235 .unwrap_or_default();
236
237 self.items
240 .borrow()
241 .range(index_at_changes..index_at_changes + list_items_added)
242 .for_each(|list_item| match list_item {
243 GroupingListItem::Singleton(_) => {}
244 GroupingListItem::Group(group) => group.drop_batch(),
245 });
246
247 obj.items_changed(
248 index_at_changes as u32,
249 list_items_removed as u32,
250 list_items_added as u32,
251 );
252 }
253
254 for index in index_before_changes.into_iter().chain(index_after_changes) {
256 let mut items = self.items.borrow_mut();
257 let item = items
258 .get_mut(index)
259 .expect("list item index should be valid");
260
261 if matches!(item, GroupingListItem::Group(_)) && item.len() == 1 {
262 *item = GroupingListItem::Singleton(item.start());
263 replaced_list_items.insert(index);
264 }
265 }
266
267 for index in replaced_list_items {
268 obj.items_changed(index as u32, 1, 1);
269 }
270
271 let groups = {
274 let items = self.items.borrow();
275
276 let first_possible_group_with_changes_index =
277 index_before_changes.unwrap_or_default();
278 let after_last_possible_group_with_changes_index =
279 (first_possible_group_with_changes_index + list_items_added + 2)
280 .min(items.len());
281
282 items
283 .range(
284 first_possible_group_with_changes_index
285 ..after_last_possible_group_with_changes_index,
286 )
287 .filter_map(|list_item| match list_item {
288 GroupingListItem::Singleton(_) => None,
289 GroupingListItem::Group(group) => group.has_batch().then(|| group.clone()),
290 })
291 .collect::<Vec<_>>()
292 };
293
294 for group in groups {
295 group.process_batch();
296 }
297 }
298
299 fn items_removed(
304 &self,
305 position: u32,
306 removed: u32,
307 index_before_changes: Option<usize>,
308 ) -> usize {
309 if removed == 0 {
310 return 0;
312 }
313
314 let index_after_changes = position
317 .checked_add(removed)
318 .and_then(|position| self.model_position_to_index(position));
319
320 let mut items = self.items.borrow_mut();
321
322 if let Some(index_before_changes) = index_before_changes.filter(|index| {
325 index_after_changes.is_none_or(|index_after_changes| index_after_changes > *index)
326 }) {
327 items
328 .get_mut(index_before_changes)
329 .expect("list item index should be valid")
330 .handle_removal(position, removed);
331 }
332
333 if let Some(index_after_changes) = index_after_changes {
335 items
336 .range_mut(index_after_changes..)
337 .for_each(|list_item| list_item.handle_removal(position, removed));
338 }
339
340 let max_index = items.len() - 1;
342
343 let removal_start = if let Some(index_before_changes) = index_before_changes {
344 index_before_changes
347 .checked_add(1)
348 .filter(|index| *index <= max_index)
349 } else {
350 Some(0)
352 };
353
354 let removal_end = if let Some(index_after_changes) = index_after_changes {
355 index_after_changes.checked_sub(1)
358 } else {
359 Some(max_index)
361 };
362
363 let Some((removal_start, removal_end)) = removal_start
365 .zip(removal_end)
366 .filter(|(removal_start, removal_end)| removal_start <= removal_end)
367 else {
368 return 0;
369 };
370
371 let is_at_items_start = removal_start == 0;
372 let is_at_items_end = removal_end == items.len().saturating_sub(1);
373
374 if is_at_items_start && is_at_items_end {
376 items.clear();
378 } else if is_at_items_end {
379 items.truncate(removal_start);
381 } else {
382 for i in (removal_start..=removal_end).rev() {
384 items.remove(i);
385 }
386 }
387
388 removal_end - removal_start + 1
389 }
390
391 fn items_added(
395 &self,
396 model: &gio::ListModel,
397 position: u32,
398 added: u32,
399 replaced_list_items: &mut HashSet<usize>,
400 index_before_changes: Option<usize>,
401 ) -> usize {
402 if added == 0 {
403 return 0;
405 }
406
407 let mut list_items_added = 0;
408
409 let position_before = position.checked_sub(1);
410 let mut previous_item_and_index =
413 position_before.and_then(|position| model.item(position).zip(index_before_changes));
414
415 let group_fn = self.group_fn();
416 let mut items = self.items.borrow_mut();
417
418 for current_position in position..position + added {
419 let item = model
420 .item(current_position)
421 .expect("item position should be valid");
422
423 if let Some((previous_item, previous_index)) = &mut previous_item_and_index {
424 let previous_list_item = items
425 .get_mut(*previous_index)
426 .expect("list item index should be valid");
427
428 if group_fn(&item, previous_item) {
429 let list_item_replacement =
431 previous_list_item.add(current_position, 1, model);
432
433 if let Some(replacement) = list_item_replacement {
434 *previous_list_item = replacement;
435
436 if current_position == position {
437 replaced_list_items.insert(*previous_index);
440 }
441 }
442
443 *previous_item = item;
445
446 continue;
447 } else if previous_list_item.contains(current_position) {
448 let end_list_item = previous_list_item.split(current_position);
450
451 items.insert(*previous_index + 1, end_list_item);
452 list_items_added += 1;
453 }
454 }
455
456 let index = previous_item_and_index
458 .take()
459 .map(|(_, index)| index + 1)
460 .unwrap_or_default();
461 items.insert(index, GroupingListItem::Singleton(current_position));
462 list_items_added += 1;
463
464 previous_item_and_index = Some((item, index));
465 }
466
467 let (_, last_index_with_changes) =
468 previous_item_and_index.expect("there should have been at least one addition");
469 let index_after_changes = last_index_with_changes + 1;
470
471 if index_after_changes < items.len() {
473 items
474 .range_mut(index_after_changes..)
475 .for_each(|list_item| list_item.handle_addition(position, added));
476 }
477
478 list_items_added
479 }
480 }
481}
482
483glib::wrapper! {
484 pub struct GroupingListModel(ObjectSubclass<imp::GroupingListModel>)
486 @implements gio::ListModel;
487}
488
489impl GroupingListModel {
490 pub fn new<GroupFn>(group_fn: GroupFn) -> Self
493 where
494 GroupFn: Fn(&glib::Object, &glib::Object) -> bool + 'static,
495 {
496 let obj = glib::Object::new::<Self>();
497 let _ = obj.imp().group_fn.set(Box::new(group_fn));
499 obj
500 }
501}
502
503#[derive(Debug, Clone)]
505enum GroupingListItem {
506 Singleton(u32),
508
509 Group(GroupingListGroup),
511}
512
513impl GroupingListItem {
514 fn with_range(range: RangeInclusive<u32>, model: &gio::ListModel) -> Self {
516 if range.start() == range.end() {
517 Self::Singleton(*range.start())
518 } else {
519 Self::Group(GroupingListGroup::new(model, range))
520 }
521 }
522
523 fn contains(&self, position: u32) -> bool {
525 match self {
526 Self::Singleton(pos) => *pos == position,
527 Self::Group(group) => group.contains(position),
528 }
529 }
530
531 fn start(&self) -> u32 {
533 match self {
534 Self::Singleton(position) => *position,
535 Self::Group(group) => group.start(),
536 }
537 }
538
539 fn end(&self) -> u32 {
541 match self {
542 Self::Singleton(position) => *position,
543 Self::Group(group) => group.end(),
544 }
545 }
546
547 fn len(&self) -> u32 {
549 match self {
550 Self::Singleton(_) => 1,
551 Self::Group(group) => {
552 let (start, end) = group.bounds();
553 end - start + 1
554 }
555 }
556 }
557
558 fn handle_removal(&mut self, position: u32, removed: u32) {
562 match self {
563 Self::Singleton(pos) => match position.cmp(pos) {
564 Ordering::Less => *pos -= removed,
565 Ordering::Equal => panic!("should not remove list item"),
566 Ordering::Greater => {}
567 },
568 Self::Group(group) => group.handle_removal(position, removed),
569 }
570 }
571
572 fn handle_addition(&mut self, position: u32, added: u32) {
574 match self {
575 Self::Singleton(pos) => {
576 if position <= *pos {
577 *pos += added;
578 }
579 }
580 Self::Group(group) => group.handle_addition(position, added),
581 }
582 }
583
584 fn add(&self, position: u32, added: u32, model: &gio::ListModel) -> Option<Self> {
590 debug_assert!(
591 (position + added) >= self.start() || position <= self.end().saturating_add(1),
592 "items to add should be contiguous"
593 );
594
595 match self {
596 Self::Singleton(pos) => {
597 let start = position.min(*pos);
598 let end = start + added;
599 Some(Self::with_range(start..=end, model))
600 }
601 Self::Group(group) => {
602 group.add(position, added);
603 None
604 }
605 }
606 }
607
608 fn split(&self, position: u32) -> Self {
616 match self {
617 Self::Singleton(_) => panic!("singleton cannot be split"),
618 Self::Group(group) => {
619 let model = group.model().expect("model should be initialized");
620 let end = group.end();
621
622 group.handle_removal(position, end - position + 1);
623
624 Self::with_range(position..=end, &model)
625 }
626 }
627 }
628}