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}