cosmic/widget/segmented_button/model/
mod.rs1mod 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 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
40pub type SingleSelectModel = Model<SingleSelect>;
42
43pub type SingleSelectEntityMut<'a> = EntityMut<'a, SingleSelect>;
45
46pub type MultiSelectModel = Model<MultiSelect>;
48
49pub type MultiSelectEntityMut<'a> = EntityMut<'a, MultiSelect>;
51
52#[derive(Debug, Default)]
54pub(super) struct Storage(HashMap<TypeId, SecondaryMap<Entity, Box<dyn Any>>>);
55
56#[derive(Default)]
58pub struct Model<SelectionMode: Default> {
59 pub(super) items: SlotMap<Entity, Settings>,
61
62 pub(super) divider_aboves: SecondaryMap<Entity, bool>,
64
65 pub(super) icons: SecondaryMap<Entity, Icon>,
67
68 pub(super) indents: SecondaryMap<Entity, u16>,
70
71 pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
73
74 pub(super) order: VecDeque<Entity>,
76
77 pub(super) selection: SelectionMode,
79
80 pub(super) storage: Storage,
82}
83
84impl<SelectionMode: Default> Model<SelectionMode>
85where
86 Self: Selectable,
87{
88 #[inline]
94 pub fn activate(&mut self, id: Entity) {
95 Selectable::activate(self, id);
96 }
97
98 #[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 #[must_use]
119 #[inline]
120 pub fn builder() -> ModelBuilder<SelectionMode> {
121 ModelBuilder::default()
122 }
123
124 #[inline]
134 pub fn clear(&mut self) {
135 for entity in self.order.clone() {
136 self.remove(entity);
137 }
138 }
139
140 #[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 #[inline]
156 pub fn contains_item(&self, id: Entity) -> bool {
157 self.items.contains_key(id)
158 }
159
160 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 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 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 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 #[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 #[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 #[inline]
258 pub fn icon(&self, id: Entity) -> Option<&Icon> {
259 self.icons.get(id)
260 }
261
262 #[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 #[inline]
285 pub fn icon_remove(&mut self, id: Entity) -> Option<Icon> {
286 self.icons.remove(id)
287 }
288
289 #[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 #[must_use]
304 #[inline]
305 pub fn is_active(&self, id: Entity) -> bool {
306 <Self as Selectable>::is_active(self, id)
307 }
308
309 #[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 #[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 #[inline]
333 pub fn len(&self) -> usize {
334 self.order.len()
335 }
336
337 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 #[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 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 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 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 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 #[inline]
470 pub fn text(&self, id: Entity) -> Option<&str> {
471 self.text.get(id).map(Cow::as_ref)
472 }
473
474 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 #[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}