makepad_widgets/
cached_widget.rs

1use std::collections::HashMap;
2
3use crate::{
4    makepad_derive_widget::*,
5    makepad_draw::*,
6    widget::*,
7};
8
9live_design! {
10    link widgets;
11    
12    pub CachedWidget = {{CachedWidget}} {}
13}
14
15/// A Singleton wrapper widget that caches and reuses its child widget across multiple instances.
16///
17/// `CachedWidget` is designed to optimize performance and memory usage by ensuring
18/// that only one instance of a child widget is created and shared across multiple
19/// uses in the UI. This is particularly useful for complex widgets that are used
20/// in different parts of the UI but should maintain a single state.
21///
22/// # Usage
23///
24/// In the DSL, you can use `CachedWidget` as follows:
25///
26/// ```
27/// <CachedWidget> {
28///     my_widget = <MyWidget> {}
29/// }
30/// ```
31///
32/// The child widget will be created once and cached.
33/// Subsequent uses of this `CachedWidget` with the same child id (`mid_widget`) will reuse the cached instance.
34/// Note that only one child is supported per `CachedWidget`.
35/// 
36/// CachedWidget supports Makepad's widget finding mechanism, allowing child widgets to be located as expected.
37///
38/// # Implementation Details
39///
40/// - Uses a global `WidgetWrapperCache` to store cached widgets
41/// - Handles widget creation and caching in the `after_apply` hook
42/// - Delegates most widget operations (like event handling and drawing) to the cached child widget
43///
44/// # Note
45///
46/// While `CachedWidget` can significantly improve performance for complex, frequently used widgets,
47/// it should be used judiciously. Overuse of caching can lead to unexpected behavior if not managed properly.
48#[derive(Live, LiveRegisterWidget, WidgetRef)]
49pub struct CachedWidget {
50    #[walk]
51    walk: Walk,
52
53    /// The ID of the child widget template
54    #[rust]
55    template_id: LiveId,
56
57    /// The cached child widget template
58    #[rust]
59    template: Option<LivePtr>,
60
61    /// The cached child widget instance
62    #[rust]
63    widget: Option<WidgetRef>,
64}
65
66impl LiveHook for CachedWidget {
67    fn before_apply(
68        &mut self,
69        _cx: &mut Cx,
70        apply: &mut Apply,
71        _index: usize,
72        _nodes: &[LiveNode],
73    ) {
74        if let ApplyFrom::UpdateFromDoc { .. } = apply.from {
75            self.template = None;
76        }
77    }
78
79    /// Handles the application of instance properties to this CachedWidget.
80    ///
81    /// In the case of `CachedWidget` This method is responsible for setting up the template 
82    /// for the child widget, and applying any changes to an existing widget instance.
83    fn apply_value_instance(
84        &mut self,
85        cx: &mut Cx,
86        apply: &mut Apply,
87        index: usize,
88        nodes: &[LiveNode],
89    ) -> usize {
90        if nodes[index].is_instance_prop() {
91            if let Some(live_ptr) = apply.from.to_live_ptr(cx, index) {
92                if self.template.is_some() {
93                    nodes.skip_node(index);
94                    error!("CachedWidget only supports one child widget, skipping additional instances");
95                }
96                let id = nodes[index].id;
97                self.template_id = id;
98                self.template = Some(live_ptr);
99
100                if let Some(widget) = &mut self.widget {
101                    widget.apply(cx, apply, index, nodes);
102                }
103            }
104        } else {
105            cx.apply_error_no_matching_field(live_error_origin!(), index, nodes);
106        }
107        nodes.skip_node(index)
108    }
109
110    /// Handles the creation or retrieval of the cached widget after applying changes.
111    ///
112    /// This method is called after all properties have been applied to the widget.
113    /// It ensures that the child widget is properly cached and retrieved from the global cache.
114    fn after_apply(&mut self, cx: &mut Cx, _apply: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
115        // Ensure the global widget cache exists
116        if !cx.has_global::<WidgetWrapperCache>() {
117            cx.set_global(WidgetWrapperCache::default())
118        }
119
120        if self.widget.is_none() {
121            // Try to retrieve the widget from the global cache
122            if let Some(widget) = cx
123                .get_global::<WidgetWrapperCache>()
124                .map
125                .get_mut(&self.template_id)
126            {
127                self.widget = Some(widget.clone());
128            } else {
129                // If not in cache, create a new widget and add it to the cache
130                let widget = WidgetRef::new_from_ptr(cx, self.template);
131                cx.get_global::<WidgetWrapperCache>()
132                    .map
133                    .insert(self.template_id, widget.clone());
134                self.widget = Some(widget);
135            }
136        }
137    }
138}
139
140impl WidgetNode for CachedWidget {
141    fn walk(&mut self, cx: &mut Cx) -> Walk {
142        if let Some(widget) = &self.widget {
143            widget.walk(cx)
144        } else {
145            self.walk
146        }
147    }
148    fn area(&self) -> Area {
149        if let Some(widget) = &self.widget {
150            widget.area()
151        } else {
152            Area::default()
153        }
154    }
155
156    fn redraw(&mut self, cx: &mut Cx) {
157        if let Some(widget) = &self.widget {
158            widget.redraw(cx);
159        }
160    }
161
162    // Searches for widgets within this CachedWidget based on the given path.
163    fn find_widgets(&self, path: &[LiveId], cached: WidgetCache, results: &mut WidgetSet) {
164        let Some(widget) = self.widget.as_ref() else { return };
165        if self.template_id == path[0] {
166            if path.len() == 1 {
167                // If the child widget is the target widget, add it to the results
168                results.push(widget.clone());
169            } else {
170                // If not, continue searching in the child widget
171                widget.find_widgets(&path[1..], cached, results);
172            }
173        }
174    }
175
176    fn uid_to_widget(&self, uid: WidgetUid) -> WidgetRef {
177        if let Some(widget) = &self.widget {
178            return widget.uid_to_widget(uid);
179        }
180        WidgetRef::empty()
181    }
182}
183
184impl Widget for CachedWidget {
185    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
186        if let Some(widget) = &self.widget {
187            widget.handle_event(cx, event, scope);
188        }
189    }
190
191    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
192        if let Some(widget) = &self.widget {
193            return widget.draw_walk(cx, scope, walk);
194        }
195
196        DrawStep::done()
197    }
198}
199
200impl CachedWidget {}
201
202#[derive(Default)]
203pub struct WidgetWrapperCache {
204    map: HashMap<LiveId, WidgetRef>,
205}