makepad_widgets/
adaptive_view.rs

1use std::collections::HashMap;
2
3use crate::{
4    makepad_derive_widget::*, makepad_draw::*, widget::*, widget_match_event::WidgetMatchEvent,
5    WindowAction,
6};
7
8live_design! {
9    link widgets;
10    use link::widgets::*;
11    use link::theme::*;
12
13    pub AdaptiveViewBase = {{AdaptiveView}} {}
14    pub AdaptiveView = <AdaptiveViewBase> {
15        width: Fill, height: Fill
16    
17        Mobile = <View> {}
18        Desktop = <View> {}
19    }
20}
21
22/// A widget that adapts its content based on the current context.
23///
24/// `AdaptiveView` allows you to define different layouts for various conditions, like display context,
25/// parent size or platform variations, (e.g., desktop vs. mobile) and automatically switches
26/// between them based on a selector function.
27///
28/// Optionally retains unused variants to preserve their state
29///
30/// # Example
31///
32/// ```rust
33
34/// live_design! {
35///     // ...
36///     adaptive = <AdaptiveView> {
37///         Desktop = <CustomView> {
38///             label =  { text: "Desktop View" } // override specific values of the same widget
39///         }
40///         Mobile = <CustomView> {
41///             label =  { text: "Mobile View" }
42///         }
43///     }
44///  // ...
45/// }
46///
47/// fn setup_adaptive_view(cx: &mut Cx) {;
48///     self.adaptive_view(id!(adaptive)).set_variant_selector(|cx, parent_size| {
49///         if cx.display_context.screen_size.x >= 1280.0 {
50///             live_id!(Desktop)
51///         } else {
52///             live_id!(Mobile)
53///         }
54///     });
55/// }
56/// ```
57///
58/// In this example, the `AdaptiveView` switches between Desktop and Mobile layouts
59/// based on the screen width. The `set_variant_selector` method allows you to define
60/// custom logic for choosing the appropriate layout variant.
61///
62/// `AdaptiveView` implements a default variant selector based on the screen width for different
63/// device layouts (Currently `Desktop` and `Mobile`). You can override this through the `set_variant_selector` method.
64///
65/// Check out [VariantSelector] for more information on how to define custom selectors, and what information is available to them.
66#[derive(Live, LiveRegisterWidget, WidgetRef)]
67pub struct AdaptiveView {
68    #[rust]
69    area: Area,
70
71    /// This widget's walk, it should always match the walk of the active widget.
72    #[walk]
73    walk: Walk,
74
75    /// Wether to retain the widget variant state when it goes unused.
76    /// While it avoids creating new widgets and keeps their state, be mindful of the memory usage and potential memory leaks.
77    #[live]
78    retain_unused_variants: bool,
79
80    /// A map of previously active widgets that are not currently being displayed.
81    /// Only used when `retain_unused_variants` is true.
82    #[rust]
83    previously_active_widgets: HashMap<LiveId, WidgetVariant>,
84
85    /// A map of templates that are used to create the active widget.
86    #[rust]
87    templates: ComponentMap<LiveId, LivePtr>,
88
89    /// The active widget that is currently being displayed.
90    #[rust]
91    active_widget: Option<WidgetVariant>,
92
93    /// The current variant selector that determines which template to use.
94    #[rust]
95    variant_selector: Option<Box<VariantSelector>>,
96
97    /// A flag to reapply the selector on the next draw call.
98    #[rust]
99    should_reapply_selector: bool,
100
101    /// Whether the AdaptiveView has non-default templates.
102    /// Used to determine if we should create a default widget.
103    /// When there are no custom templates, the user of this AdaptiveView is likely not
104    /// setting up a custom selector, so we should create a default widget.
105    #[rust]
106    has_custom_templates: bool,
107}
108
109pub struct WidgetVariant {
110    pub template_id: LiveId,
111    pub widget_ref: WidgetRef,
112}
113
114impl WidgetNode for AdaptiveView {
115    fn walk(&mut self, cx: &mut Cx) -> Walk {
116        if let Some(active_widget) = self.active_widget.as_ref() {
117            active_widget.widget_ref.walk(cx)
118        } else {
119            // No active widget found, returning a default walk.
120            self.walk
121        }
122    }
123
124    fn area(&self) -> Area {
125        self.area
126    }
127
128    fn redraw(&mut self, cx: &mut Cx) {
129        self.area.redraw(cx);
130    }
131
132    fn find_widgets(&self, path: &[LiveId], cached: WidgetCache, results: &mut WidgetSet) {
133        if let Some(active_widget) = self.active_widget.as_ref() {
134            active_widget.widget_ref.find_widgets(path, cached, results);
135        }
136    }
137
138    fn uid_to_widget(&self, uid: WidgetUid) -> WidgetRef {
139        if let Some(active_widget) = self.active_widget.as_ref() {
140            active_widget.widget_ref.uid_to_widget(uid)
141        } else {
142            WidgetRef::empty()
143        }
144    }
145}
146
147impl LiveHook for AdaptiveView {
148    fn before_apply(
149        &mut self,
150        _cx: &mut Cx,
151        apply: &mut Apply,
152        _index: usize,
153        _nodes: &[LiveNode],
154    ) {
155        if let ApplyFrom::UpdateFromDoc { .. } = apply.from {
156            self.templates.clear();
157        }
158    }
159
160    fn after_apply_from(&mut self, cx: &mut Cx, apply: &mut Apply) {
161        // Do not override the current selector if we are updating from the doc
162        if let ApplyFrom::UpdateFromDoc { .. } = apply.from {
163            return;
164        };
165
166        // If there are no custom templates, create a default widget with the default variant Desktop
167        // This is needed so that methods that run before drawing (find_widgets, walk) have something to work with
168        if !self.has_custom_templates {
169            let template = self.templates.get(&live_id!(Desktop)).unwrap();
170            let widget_ref = WidgetRef::new_from_ptr(cx, Some(*template));
171            self.active_widget = Some(WidgetVariant {
172                template_id: live_id!(Desktop),
173                widget_ref: widget_ref.clone(),
174            });
175        }
176        self.set_default_variant_selector();
177    }
178
179    fn apply_value_instance(
180        &mut self,
181        cx: &mut Cx,
182        apply: &mut Apply,
183        index: usize,
184        nodes: &[LiveNode],
185    ) -> usize {
186        if nodes[index].is_instance_prop() {
187            if let Some(live_ptr) = apply.from.to_live_ptr(cx, index) {
188                let id = nodes[index].id;
189                self.templates.insert(id, live_ptr);
190
191                if id != live_id!(Desktop) && id != live_id!(Mobile) {
192                    self.has_custom_templates = true;
193                }
194
195                if let Some(widget_variant) = self.active_widget.as_mut() {
196                    if widget_variant.template_id == id {
197                        widget_variant.widget_ref.apply(cx, apply, index, nodes);
198                    }
199                }
200            }
201        } else {
202            cx.apply_error_no_matching_field(live_error_origin!(), index, nodes);
203        }
204        nodes.skip_node(index)
205    }
206}
207
208impl Widget for AdaptiveView {
209    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
210        self.widget_match_event(cx, event, scope);
211        if let Some(active_widget) = self.active_widget.as_mut() {
212            active_widget.widget_ref.handle_event(cx, event, scope);
213        }
214    }
215
216    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
217        if self.should_reapply_selector {
218            let parent_size = cx.peek_walk_turtle(walk).size;
219            self.apply_selector(cx, &parent_size);
220        }
221
222        if let Some(active_widget) = self.active_widget.as_mut() {
223            active_widget.widget_ref.draw_walk(cx, scope, walk)?;
224        }
225
226        DrawStep::done()
227    }
228}
229
230impl WidgetMatchEvent for AdaptiveView {
231    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) {
232        for action in actions {
233            // Handle window geom change events, this is triggered at startup and on window resize.
234            if let WindowAction::WindowGeomChange(ce) = action.as_widget_action().cast() {
235                let event_id = cx.event_id();
236
237                // Skip if the display context was already updated on this event
238                if cx.display_context.updated_on_event_id == event_id { return }
239                // Update the current context if the screen size has changed
240                if cx.display_context.screen_size != ce.new_geom.inner_size {
241                    cx.display_context.updated_on_event_id = event_id;
242                    cx.display_context.screen_size = ce.new_geom.inner_size;
243
244                    self.should_reapply_selector = true;
245                }
246
247                cx.redraw_all();
248            }
249        }
250    }
251}
252
253impl AdaptiveView {
254    /// Apply the variant selector to determine which template to use.
255    fn apply_selector(&mut self, cx: &mut Cx, parent_size: &DVec2) {
256        let Some(variant_selector) = self.variant_selector.as_mut() else {
257            return;
258        };
259
260        let template_id = variant_selector(cx, parent_size);
261
262        // If the selector resulted in a widget that is already active, do nothing
263        if let Some(active_widget) = self.active_widget.as_mut() {
264            if active_widget.template_id == template_id {
265                return;
266            }
267        }
268
269        // If the selector resulted in a widget that was previously active, restore it
270        if self.retain_unused_variants && self.previously_active_widgets.contains_key(&template_id)
271        {
272            let widget_variant = self.previously_active_widgets.remove(&template_id).unwrap();
273
274            self.walk = widget_variant.widget_ref.walk(cx);
275            self.active_widget = Some(widget_variant);
276            return;
277        }
278
279        // Invalidate widget query caches when changing the active variant.
280        // Parent views need to rebuild their widget queries since the widget
281        // hierarchy has changed. We use the event system to ensure all views
282        // process this invalidation in the next event cycle.
283        cx.widget_query_invalidation_event = Some(cx.event_id());
284
285        // Otherwise create a new widget from the template
286        let template = self.templates.get(&template_id).unwrap();
287        let widget_ref = WidgetRef::new_from_ptr(cx, Some(*template));
288
289        // Update this widget's walk to match the walk of the active widget,
290        // this ensures that the new widget is not affected by `Fill` or `Fit` constraints from this parent.
291        self.walk = widget_ref.walk(cx);
292
293        if let Some(active_widget) = self.active_widget.take() {
294            if self.retain_unused_variants {
295                self.previously_active_widgets
296                    .insert(active_widget.template_id, active_widget);
297            }
298        }
299
300        self.active_widget = Some(WidgetVariant {
301            template_id,
302            widget_ref,
303        });
304    }
305
306    /// Set a variant selector for this widget.
307    /// The selector is a closure that takes a `DisplayContext` and returns a `LiveId`, corresponding to the template to use.
308    pub fn set_variant_selector(
309        &mut self,
310        selector: impl FnMut(&mut Cx, &DVec2) -> LiveId + 'static,
311    ) {
312        self.variant_selector = Some(Box::new(selector));
313        self.should_reapply_selector = true;
314    }
315
316    pub fn set_default_variant_selector(&mut self) {
317        // TODO(Julian): setup a more comprehensive default
318        self.set_variant_selector(|cx, _parent_size| {
319            if cx.display_context.is_desktop() {
320                live_id!(Desktop)
321            } else {
322                live_id!(Mobile)
323            }
324        });
325    }
326}
327
328impl AdaptiveViewRef {
329    /// Set a variant selector for this widget.
330    /// The selector is a closure that takes a `DisplayContext` and returns a `LiveId`, corresponding to the template to use.
331    pub fn set_variant_selector(
332        &self,
333        selector: impl FnMut(&mut Cx, &DVec2) -> LiveId + 'static,
334    ) {
335        let Some(mut inner) = self.borrow_mut() else {
336            return;
337        };
338        inner.set_variant_selector(selector);
339    }
340}
341
342/// A closure that returns a `LiveId` corresponding to the template to use.
343pub type VariantSelector = dyn FnMut(&mut Cx, &ParentSize) -> LiveId;
344
345/// The size of the parent obtained from running `cx.peek_walk_turtle(walk)` before the widget is drawn.
346type ParentSize = DVec2;