cosmic/widget/segmented_button/model/
mod.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4mod builder;
5pub use self::builder::{BuilderEntity, ModelBuilder};
6
7mod entity;
8pub use self::entity::EntityMut;
9
10mod selection;
11pub use self::selection::{MultiSelect, Selectable, SingleSelect};
12
13use crate::widget::Icon;
14use crate::widget::segmented_button::InsertPosition;
15use slotmap::{SecondaryMap, SlotMap};
16use std::any::{Any, TypeId};
17use std::borrow::Cow;
18use std::collections::{HashMap, VecDeque};
19
20slotmap::new_key_type! {
21    /// A unique ID for an item in the [`Model`].
22    pub struct Entity;
23}
24
25#[derive(Clone, Debug)]
26pub struct Settings {
27    pub enabled: bool,
28    pub closable: bool,
29}
30
31impl Default for Settings {
32    fn default() -> Self {
33        Self {
34            enabled: true,
35            closable: false,
36        }
37    }
38}
39
40/// A model for single-select button selection.
41pub type SingleSelectModel = Model<SingleSelect>;
42
43/// Single-select variant of an [`EntityMut`].
44pub type SingleSelectEntityMut<'a> = EntityMut<'a, SingleSelect>;
45
46/// A model for multi-select button selection.
47pub type MultiSelectModel = Model<MultiSelect>;
48
49/// Multi-select variant of an [`EntityMut`].
50pub type MultiSelectEntityMut<'a> = EntityMut<'a, MultiSelect>;
51
52/// The portion of the model used only by the application.
53#[derive(Debug, Default)]
54pub(super) struct Storage(HashMap<TypeId, SecondaryMap<Entity, Box<dyn Any>>>);
55
56/// The model held by the application, containing the unique IDs and data of each inserted item.
57#[derive(Default)]
58pub struct Model<SelectionMode: Default> {
59    /// The content used for drawing segmented items.
60    pub(super) items: SlotMap<Entity, Settings>,
61
62    /// Divider optionally-defined for each item.
63    pub(super) divider_aboves: SecondaryMap<Entity, bool>,
64
65    /// Icons optionally-defined for each item.
66    pub(super) icons: SecondaryMap<Entity, Icon>,
67
68    /// Indent optionally-defined for each item.
69    pub(super) indents: SecondaryMap<Entity, u16>,
70
71    /// Text optionally-defined for each item.
72    pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
73
74    /// Order which the items will be displayed.
75    pub(super) order: VecDeque<Entity>,
76
77    /// Manages selections
78    pub(super) selection: SelectionMode,
79
80    /// Data managed by the application.
81    pub(super) storage: Storage,
82}
83
84impl<SelectionMode: Default> Model<SelectionMode>
85where
86    Self: Selectable,
87{
88    /// Activates the item in the model.
89    ///
90    /// ```ignore
91    /// model.activate(id);
92    /// ```
93    #[inline]
94    pub fn activate(&mut self, id: Entity) {
95        Selectable::activate(self, id);
96    }
97
98    /// Activates the item at the given position, returning true if it was activated.
99    #[inline]
100    pub fn activate_position(&mut self, position: u16) -> bool {
101        if let Some(entity) = self.entity_at(position) {
102            self.activate(entity);
103            return true;
104        }
105
106        false
107    }
108
109    /// Creates a builder for initializing a model.
110    ///
111    /// ```ignore
112    /// let model = segmented_button::Model::builder()
113    ///     .insert(|b| b.text("Item A").activate())
114    ///     .insert(|b| b.text("Item B"))
115    ///     .insert(|b| b.text("Item C"))
116    ///     .build();
117    /// ```
118    #[must_use]
119    #[inline]
120    pub fn builder() -> ModelBuilder<SelectionMode> {
121        ModelBuilder::default()
122    }
123
124    /// Removes all items from the model.
125    ///
126    /// Any IDs held elsewhere by the application will no longer be usable with the map.
127    /// The generation is incremented on removal, so the stale IDs will return `None` for
128    /// any attempt to get values from the map.
129    ///
130    /// ```ignore
131    /// model.clear();
132    /// ```
133    #[inline]
134    pub fn clear(&mut self) {
135        for entity in self.order.clone() {
136            self.remove(entity);
137        }
138    }
139
140    /// Shows or hides the item's close button.
141    #[inline]
142    pub fn closable_set(&mut self, id: Entity, closable: bool) {
143        if let Some(settings) = self.items.get_mut(id) {
144            settings.closable = closable;
145        }
146    }
147
148    /// Check if an item exists in the map.
149    ///
150    /// ```ignore
151    /// if model.contains_item(id) {
152    ///     println!("ID is still valid");
153    /// }
154    /// ```
155    #[inline]
156    pub fn contains_item(&self, id: Entity) -> bool {
157        self.items.contains_key(id)
158    }
159
160    /// Get an immutable reference to data associated with an item.
161    ///
162    /// ```ignore
163    /// if let Some(data) = model.data::<String>(id) {
164    ///     println!("found string on {:?}: {}", id, data);
165    /// }
166    /// ```
167    pub fn data<Data: 'static>(&self, id: Entity) -> Option<&Data> {
168        self.storage
169            .0
170            .get(&TypeId::of::<Data>())
171            .and_then(|storage| storage.get(id))
172            .and_then(|data| data.downcast_ref())
173    }
174
175    /// Get a mutable reference to data associated with an item.
176    pub fn data_mut<Data: 'static>(&mut self, id: Entity) -> Option<&mut Data> {
177        self.storage
178            .0
179            .get_mut(&TypeId::of::<Data>())
180            .and_then(|storage| storage.get_mut(id))
181            .and_then(|data| data.downcast_mut())
182    }
183
184    /// Associates data with the item.
185    ///
186    /// There may only be one data component per Rust type.
187    ///
188    /// ```ignore
189    /// model.data_set::<String>(id, String::from("custom string"));
190    /// ```
191    pub fn data_set<Data: 'static>(&mut self, id: Entity, data: Data) {
192        if self.contains_item(id) {
193            self.storage
194                .0
195                .entry(TypeId::of::<Data>())
196                .or_default()
197                .insert(id, Box::new(data));
198        }
199    }
200
201    /// Removes a specific data type from the item.
202    ///
203    /// ```ignore
204    /// model.data.remove::<String>(id);
205    /// ```
206    pub fn data_remove<Data: 'static>(&mut self, id: Entity) {
207        self.storage
208            .0
209            .get_mut(&TypeId::of::<Data>())
210            .and_then(|storage| storage.remove(id));
211    }
212
213    #[inline]
214    pub fn divider_above(&self, id: Entity) -> Option<bool> {
215        self.divider_aboves.get(id).copied()
216    }
217
218    pub fn divider_above_set(&mut self, id: Entity, divider_above: bool) -> Option<bool> {
219        if !self.contains_item(id) {
220            return None;
221        }
222
223        self.divider_aboves.insert(id, divider_above)
224    }
225
226    #[inline]
227    pub fn divider_above_remove(&mut self, id: Entity) -> Option<bool> {
228        self.divider_aboves.remove(id)
229    }
230
231    /// Enable or disable an item.
232    ///
233    /// ```ignore
234    /// model.enable(id, true);
235    /// ```
236    #[inline]
237    pub fn enable(&mut self, id: Entity, enable: bool) {
238        if let Some(e) = self.items.get_mut(id) {
239            e.enabled = enable;
240        }
241    }
242
243    /// Get the item that is located at a given position.
244    #[must_use]
245    #[inline]
246    pub fn entity_at(&mut self, position: u16) -> Option<Entity> {
247        self.order.get(position as usize).copied()
248    }
249
250    /// Immutable reference to the icon associated with the item.
251    ///
252    /// ```ignore
253    /// if let Some(icon) = model.icon(id) {
254    ///     println!("has icon: {:?}", icon);
255    /// }
256    /// ```
257    #[inline]
258    pub fn icon(&self, id: Entity) -> Option<&Icon> {
259        self.icons.get(id)
260    }
261
262    /// Sets a new icon for an item.
263    ///
264    /// ```ignore
265    /// if let Some(old_icon) = model.icon_set(IconSource::from("new-icon")) {
266    ///     println!("previously had icon: {:?}", old_icon);
267    /// }
268    /// ```
269    #[inline]
270    pub fn icon_set(&mut self, id: Entity, icon: Icon) -> Option<Icon> {
271        if !self.contains_item(id) {
272            return None;
273        }
274
275        self.icons.insert(id, icon)
276    }
277
278    /// Removes the icon from an item.
279    ///
280    /// ```ignore
281    /// if let Some(old_icon) = model.icon_remove(id) {
282    ///     println!("previously had icon: {:?}", old_icon);
283    /// }
284    #[inline]
285    pub fn icon_remove(&mut self, id: Entity) -> Option<Icon> {
286        self.icons.remove(id)
287    }
288
289    /// Inserts a new item in the model.
290    ///
291    /// ```ignore
292    /// let id = model.insert().text("Item A").icon("custom-icon").id();
293    /// ```
294    #[must_use]
295    #[inline]
296    pub fn insert(&mut self) -> EntityMut<'_, SelectionMode> {
297        let id = self.items.insert(Settings::default());
298        self.order.push_back(id);
299        EntityMut { model: self, id }
300    }
301
302    /// Check if the given ID is the active ID.
303    #[must_use]
304    #[inline]
305    pub fn is_active(&self, id: Entity) -> bool {
306        <Self as Selectable>::is_active(self, id)
307    }
308
309    /// Whether the item should contain a close button.
310    #[must_use]
311    #[inline]
312    pub fn is_closable(&self, id: Entity) -> bool {
313        self.items.get(id).map(|e| e.closable).unwrap_or_default()
314    }
315
316    /// Check if the item is enabled.
317    ///
318    /// ```ignore
319    /// if model.is_enabled(id) {
320    ///     if let Some(text) = model.text(id) {
321    ///         println!("{text} is enabled");
322    ///     }
323    /// }
324    /// ```
325    #[must_use]
326    #[inline]
327    pub fn is_enabled(&self, id: Entity) -> bool {
328        self.items.get(id).map(|e| e.enabled).unwrap_or_default()
329    }
330
331    /// Get number of items in the model.
332    #[inline]
333    pub fn len(&self) -> usize {
334        self.order.len()
335    }
336
337    /// Iterates across items in the model in the order that they are displayed.
338    pub fn iter(&self) -> impl Iterator<Item = Entity> + '_ {
339        self.order.iter().copied()
340    }
341
342    #[inline]
343    pub fn indent(&self, id: Entity) -> Option<u16> {
344        self.indents.get(id).copied()
345    }
346
347    #[inline]
348    pub fn indent_set(&mut self, id: Entity, indent: u16) -> Option<u16> {
349        if !self.contains_item(id) {
350            return None;
351        }
352
353        self.indents.insert(id, indent)
354    }
355
356    #[inline]
357    pub fn indent_remove(&mut self, id: Entity) -> Option<u16> {
358        self.indents.remove(id)
359    }
360
361    /// The position of the item in the model.
362    ///
363    /// ```ignore
364    /// if let Some(position) = model.position(id) {
365    ///     println!("found item at {}", position);
366    /// }
367    #[must_use]
368    #[inline]
369    pub fn position(&self, id: Entity) -> Option<u16> {
370        #[allow(clippy::cast_possible_truncation)]
371        self.order.iter().position(|k| *k == id).map(|v| v as u16)
372    }
373
374    /// Change the position of an item in the model.
375    ///
376    /// ```ignore
377    /// if let Some(new_position) = model.position_set(id, 0) {
378    ///     println!("placed item at {}", new_position);
379    /// }
380    /// ```
381    pub fn position_set(&mut self, id: Entity, position: u16) -> Option<usize> {
382        let index = self.position(id)?;
383
384        self.order.remove(index as usize);
385
386        let position = self.order.len().min(position as usize);
387
388        self.order.insert(position, id);
389        Some(position)
390    }
391
392    /// Swap the position of two items in the model.
393    ///
394    /// Returns false if the swap cannot be performed.
395    ///
396    /// ```ignore
397    /// if model.position_swap(first_id, second_id) {
398    ///     println!("positions swapped");
399    /// }
400    /// ```
401    pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool {
402        let Some(first_index) = self.position(first) else {
403            return false;
404        };
405
406        let Some(second_index) = self.position(second) else {
407            return false;
408        };
409
410        self.order.swap(first_index as usize, second_index as usize);
411        true
412    }
413
414    /// Reorder `dragged` relative to `target` based on the provided position.
415    ///
416    /// Returns `true` if the model changed, or `false` if the move was invalid.
417    pub fn reorder(&mut self, dragged: Entity, target: Entity, position: InsertPosition) -> bool {
418        if !self.contains_item(dragged) || !self.contains_item(target) || dragged == target {
419            return false;
420        }
421
422        let len = self.iter().count();
423        let target_pos = self.position(target).map(|pos| pos as usize).unwrap_or(len);
424        let from_pos = self
425            .position(dragged)
426            .map(|pos| pos as usize)
427            .unwrap_or(target_pos);
428        let mut insert_pos = match position {
429            InsertPosition::Before => target_pos,
430            InsertPosition::After => target_pos.saturating_add(1),
431        };
432        if from_pos < insert_pos {
433            insert_pos = insert_pos.saturating_sub(1);
434        }
435        if len > 0 {
436            insert_pos = insert_pos.min(len.saturating_sub(1));
437        }
438
439        self.position_set(dragged, insert_pos as u16);
440        self.activate(dragged);
441        true
442    }
443
444    /// Removes an item from the model.
445    ///
446    /// The generation of the slot for the ID will be incremented, so this ID will no
447    /// longer be usable with the map. Subsequent attempts to get values from the map
448    /// with this ID will return `None` and failed to assign values.
449    pub fn remove(&mut self, id: Entity) {
450        self.items.remove(id);
451        self.deactivate(id);
452
453        for storage in self.storage.0.values_mut() {
454            storage.remove(id);
455        }
456
457        if let Some(index) = self.position(id) {
458            self.order.remove(index as usize);
459        }
460    }
461
462    /// Immutable reference to the text assigned to the item.
463    ///
464    /// ```ignore
465    /// if let Some(text) = model.text(id) {
466    ///     println!("{:?} has text {text}", id);
467    /// }
468    /// ```
469    #[inline]
470    pub fn text(&self, id: Entity) -> Option<&str> {
471        self.text.get(id).map(Cow::as_ref)
472    }
473
474    /// Sets new text for an item.
475    ///
476    /// ```ignore
477    /// if let Some(old_text) = model.text_set(id, "Item B") {
478    ///     println!("{:?} had text {}", id, old_text)
479    /// }
480    /// ```
481    pub fn text_set(
482        &mut self,
483        id: Entity,
484        text: impl Into<Cow<'static, str>>,
485    ) -> Option<Cow<'_, str>> {
486        if !self.contains_item(id) {
487            return None;
488        }
489
490        self.text.insert(id, text.into())
491    }
492
493    /// Removes text from an item.
494    /// ```ignore
495    /// if let Some(old_text) = model.text_remove(id) {
496    ///     println!("{:?} had text {}", id, old_text);
497    /// }
498    #[inline]
499    pub fn text_remove(&mut self, id: Entity) -> Option<Cow<'static, str>> {
500        self.text.remove(id)
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    fn sample_model() -> (Model<SingleSelect>, Vec<Entity>) {
509        let mut ids = Vec::new();
510        let model = Model::builder()
511            .insert(|b| b.text("Tab1").with_id(|id| ids.push(id)))
512            .insert(|b| b.text("Tab2").with_id(|id| ids.push(id)))
513            .insert(|b| b.text("Tab3").with_id(|id| ids.push(id)))
514            .insert(|b| b.text("Tab4").with_id(|id| ids.push(id)))
515            .build();
516        (model, ids)
517    }
518
519    fn order_of(model: &Model<SingleSelect>) -> Vec<Entity> {
520        model.iter().collect()
521    }
522
523    #[test]
524    fn reorder_inserts_before_target() {
525        let (mut model, ids) = sample_model();
526        assert!(model.reorder(ids[3], ids[1], InsertPosition::Before));
527        assert_eq!(order_of(&model), vec![ids[0], ids[3], ids[1], ids[2]]);
528    }
529
530    #[test]
531    fn reorder_inserts_after_target() {
532        let (mut model, ids) = sample_model();
533        assert!(model.reorder(ids[0], ids[2], InsertPosition::After));
534        assert_eq!(order_of(&model), vec![ids[1], ids[2], ids[0], ids[3]]);
535    }
536
537    #[test]
538    fn reorder_rejects_invalid_entities() {
539        let (mut model, ids) = sample_model();
540        assert!(!model.reorder(ids[0], ids[0], InsertPosition::After));
541    }
542}