Skip to main content

frus_widgets/
keyed.rs

1//! [`Keyed`]: a **transparent** wrapper that gives a widget a **stable identity**,
2//! through a key, independent of its position among its siblings.
3//!
4//! Without a key, identity is positional: removing an item from the middle of a list
5//! shifts the identity of everything after it, and their retained state — hover, focus,
6//! caret, animations, the leaving fade — jumps. Wrapping each item in a `Keyed`, keyed
7//! on its stable domain id, fixes that.
8//!
9//! It is the reference's `KeyedSubtree` — a key around a subtree and nothing else — and
10//! [`Keyed::wrap`] is that widget's own constructor for the case where somebody else's
11//! children are being keyed in bulk.
12
13use std::hash::{Hash, Hasher};
14
15use crate::widget::Widget;
16
17/// Wraps a widget in a stable identity key, delegating everything else.
18pub struct Keyed<Msg = crate::callback::Callback> {
19    key: u64,
20    inner: Box<dyn Widget<Msg>>,
21}
22
23impl<Msg> Keyed<Msg> {
24    /// Wraps `inner` with the key `key`, which may be any hashable type.
25    pub fn new(key: impl Hash, inner: impl Widget<Msg> + 'static) -> Self {
26        let mut hasher = std::collections::hash_map::DefaultHasher::new();
27        key.hash(&mut hasher);
28        Self {
29            key: hasher.finish(),
30            inner: Box::new(inner),
31        }
32    }
33
34    /// Keeps the key `inner` **already has**, and falls back to `index` when it has
35    /// none — the reference's `KeyedSubtree.wrap`.
36    ///
37    /// It is for keying somebody else's children: a helper that wraps every item of a
38    /// list it was handed cannot use [`Keyed::new`], because that would overwrite the
39    /// keys the caller set on the items they cared about. A position is a poor identity,
40    /// and it is still better than none for the items nobody named.
41    pub fn wrap(index: usize, inner: impl Widget<Msg> + 'static) -> Self {
42        match inner.key() {
43            Some(key) => Self {
44                key,
45                inner: Box::new(inner),
46            },
47            None => Self::new(index, inner),
48        }
49    }
50
51    /// A key is an identity, not a box: the child's own, unchanged.
52    fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
53        base
54    }
55}
56
57crate::transparent::forward_transparent!(Keyed {
58    /// The one thing a `Keyed` does not delegate: the identity it exists to give.
59    fn key(&self) -> Option<u64> {
60        Some(self.key)
61    }
62
63    /// Forwarded: a key is not a place — `Keyed(Positioned(…))` must keep its pins.
64    fn positioned(&self) -> Option<crate::positioned::Positioning> {
65        self.inner.positioned()
66    }
67
68    /// And the one it does — a key says nothing about a theme.
69    fn theme_override(
70        &self,
71        inherited: &crate::theme::Theme,
72    ) -> Option<Box<crate::theme::Theme>> {
73        self.inner.theme_override(inherited)
74    }
75
76    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
77    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
78        self.inner.media_override(inherited)
79    }
80    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
81        self.inner.scaffold_override()
82    }
83    /// Forwarded: a key is not a form, and a keyed form is still one (milestone 512).
84    fn autofill_group(&self) -> bool {
85        self.inner.autofill_group()
86    }
87});
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::widget::ReorderAxis;
93    use crate::{Container, Text};
94
95    /// A reorderable that is **not** the default in either respect: dragged vertically,
96    /// and a drop target only.
97    struct Card;
98
99    impl Widget<()> for Card {
100        fn style(&self) -> frus_layout::Style {
101            frus_layout::Style::default()
102        }
103        fn children(&self) -> &[Box<dyn Widget<()>>] {
104            &[]
105        }
106        fn paint(
107            &self,
108            _bounds: frus_core::Rect,
109            _status: crate::interaction::Status,
110            _theme: &crate::theme::Theme,
111            _scene: &mut frus_core::Scene,
112        ) {
113        }
114        fn on_click(&self) -> Option<()> {
115            None
116        }
117        fn reorder_axis(&self) -> ReorderAxis {
118            ReorderAxis::Vertical
119        }
120        fn reorder_draggable(&self) -> bool {
121            false
122        }
123    }
124
125    /// Found by the forwarding test in [`crate::transparent`], not by a device: the
126    /// hand-written version of this wrapper forwarded `reorder_index` and `on_reorder`
127    /// and stopped there, so a keyed card in a board dragged along the wrong axis and a
128    /// drop-only slot could be lifted.
129    #[test]
130    fn a_keyed_reorderable_keeps_its_axis_and_its_refusal_to_be_lifted() {
131        let keyed = Keyed::new(1u64, Card);
132        assert_eq!(Widget::<()>::reorder_axis(&keyed), ReorderAxis::Vertical);
133        assert!(!Widget::<()>::reorder_draggable(&keyed));
134    }
135
136    /// The structural questions decide **how the content is laid out**, so a
137    /// transparent wrapper must pass them through. Answering them for itself made a
138    /// keyed stack lay its layers out in flow instead of on top of one another — found
139    /// on a device, wrapping a swipeable row in `keyed(...)`.
140    #[test]
141    fn structural_questions_pass_through() {
142        let stack = crate::Stack::<()>::new()
143            .width(100.0)
144            .height(50.0)
145            .layer(Container::new())
146            .layer(Container::new());
147        assert!(Widget::<()>::stack(&stack));
148        assert!(
149            Widget::<()>::stack(&Keyed::new(1u64, stack)),
150            "a keyed stack is still a stack"
151        );
152
153        let spinner = crate::CircularProgressIndicator::new();
154        assert!(Widget::<()>::continuous(&spinner));
155        assert!(Widget::<()>::continuous(&Keyed::new(2u64, spinner)));
156    }
157
158    #[test]
159    fn reports_key_and_delegates() {
160        let inner = Container::<()>::new().width(40.0).child(Text::new("x"));
161        let keyed = Keyed::new(99u64, inner);
162        // It returns a key.
163        assert!(Widget::<()>::key(&keyed).is_some());
164        // It delegates the children; the Container has one.
165        assert_eq!(Widget::<()>::children(&keyed).len(), 1);
166    }
167
168    #[test]
169    fn same_key_same_hash() {
170        let a: Keyed<()> = Keyed::new("todo-3", Container::new());
171        let b: Keyed<()> = Keyed::new("todo-3", Container::new());
172        assert_eq!(Widget::<()>::key(&a), Widget::<()>::key(&b));
173    }
174
175    /// Wrapping in bulk keeps the keys somebody already chose, and only names the ones
176    /// nobody did.
177    #[test]
178    fn wrapping_keeps_a_key_the_child_already_had() {
179        let named: Keyed<()> = Keyed::new("row-7", Text::new("seven"));
180        let key = Widget::<()>::key(&named).expect("a key of its own");
181        let wrapped = Keyed::wrap(3, named);
182        assert_eq!(
183            Widget::<()>::key(&wrapped),
184            Some(key),
185            "the position did not overwrite the name"
186        );
187
188        let plain: Keyed<()> = Keyed::wrap(3, Text::new("nobody named me"));
189        let other: Keyed<()> = Keyed::wrap(4, Text::new("nobody named me"));
190        assert_ne!(
191            Widget::<()>::key(&plain),
192            Widget::<()>::key(&other),
193            "and an unnamed child is at least distinguished by where it is"
194        );
195    }
196}