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