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    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
84        self.inner.scroll_behavior_override()
85    }
86    /// Forwarded: a key is not a form, and a keyed form is still one (milestone 512).
87    fn autofill_group(&self) -> bool {
88        self.inner.autofill_group()
89    }
90    /// Forwarded: the area's bar is the area's, whatever wraps it.
91    fn area_toolbar(
92        &self,
93        context: crate::ToolbarContext,
94    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
95        self.inner.area_toolbar(context)
96    }
97});
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::widget::ReorderAxis;
103    use crate::{Container, Text};
104
105    /// A reorderable that is **not** the default in either respect: dragged vertically,
106    /// and a drop target only.
107    struct Card;
108
109    impl Widget<()> for Card {
110        fn style(&self) -> frus_layout::Style {
111            frus_layout::Style::default()
112        }
113        fn children(&self) -> &[Box<dyn Widget<()>>] {
114            &[]
115        }
116        fn paint(
117            &self,
118            _bounds: frus_core::Rect,
119            _status: crate::interaction::Status,
120            _theme: &crate::theme::Theme,
121            _scene: &mut frus_core::Scene,
122        ) {
123        }
124        fn on_click(&self) -> Option<()> {
125            None
126        }
127        fn reorder_axis(&self) -> ReorderAxis {
128            ReorderAxis::Vertical
129        }
130        fn reorder_draggable(&self) -> bool {
131            false
132        }
133    }
134
135    /// Found by the forwarding test in [`crate::transparent`], not by a device: the
136    /// hand-written version of this wrapper forwarded `reorder_index` and `on_reorder`
137    /// and stopped there, so a keyed card in a board dragged along the wrong axis and a
138    /// drop-only slot could be lifted.
139    #[test]
140    fn a_keyed_reorderable_keeps_its_axis_and_its_refusal_to_be_lifted() {
141        let keyed = Keyed::new(1u64, Card);
142        assert_eq!(Widget::<()>::reorder_axis(&keyed), ReorderAxis::Vertical);
143        assert!(!Widget::<()>::reorder_draggable(&keyed));
144    }
145
146    /// The structural questions decide **how the content is laid out**, so a
147    /// transparent wrapper must pass them through. Answering them for itself made a
148    /// keyed stack lay its layers out in flow instead of on top of one another — found
149    /// on a device, wrapping a swipeable row in `keyed(...)`.
150    #[test]
151    fn structural_questions_pass_through() {
152        let stack = crate::Stack::<()>::new()
153            .width(100.0)
154            .height(50.0)
155            .layer(Container::new())
156            .layer(Container::new());
157        assert!(Widget::<()>::stack(&stack));
158        assert!(
159            Widget::<()>::stack(&Keyed::new(1u64, stack)),
160            "a keyed stack is still a stack"
161        );
162
163        let spinner = crate::CircularProgressIndicator::new();
164        assert!(Widget::<()>::continuous(&spinner));
165        assert!(Widget::<()>::continuous(&Keyed::new(2u64, spinner)));
166    }
167
168    #[test]
169    fn reports_key_and_delegates() {
170        let inner = Container::<()>::new().width(40.0).child(Text::new("x"));
171        let keyed = Keyed::new(99u64, inner);
172        // It returns a key.
173        assert!(Widget::<()>::key(&keyed).is_some());
174        // It delegates the children; the Container has one.
175        assert_eq!(Widget::<()>::children(&keyed).len(), 1);
176    }
177
178    #[test]
179    fn same_key_same_hash() {
180        let a: Keyed<()> = Keyed::new("todo-3", Container::new());
181        let b: Keyed<()> = Keyed::new("todo-3", Container::new());
182        assert_eq!(Widget::<()>::key(&a), Widget::<()>::key(&b));
183    }
184
185    /// Wrapping in bulk keeps the keys somebody already chose, and only names the ones
186    /// nobody did.
187    #[test]
188    fn wrapping_keeps_a_key_the_child_already_had() {
189        let named: Keyed<()> = Keyed::new("row-7", Text::new("seven"));
190        let key = Widget::<()>::key(&named).expect("a key of its own");
191        let wrapped = Keyed::wrap(3, named);
192        assert_eq!(
193            Widget::<()>::key(&wrapped),
194            Some(key),
195            "the position did not overwrite the name"
196        );
197
198        let plain: Keyed<()> = Keyed::wrap(3, Text::new("nobody named me"));
199        let other: Keyed<()> = Keyed::wrap(4, Text::new("nobody named me"));
200        assert_ne!(
201            Widget::<()>::key(&plain),
202            Widget::<()>::key(&other),
203            "and an unnamed child is at least distinguished by where it is"
204        );
205    }
206}