1use crate::{
4 AnyElement, App, Bounds, Context, Element, ElementId, Entity, GlobalElementId, IntoElement,
5 LayoutId, Pixels, Style, StyleRefinement, Styled, Window,
6 view::{paint_view, prepaint_cached_view},
7};
8use refineable::Refineable;
9
10pub trait EntitySlotExt<T: 'static> {
17 fn slot<F, E>(&self, id: impl Into<ElementId>, builder: F) -> SlotElement<T, F>
28 where
29 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
30 E: IntoElement + 'static;
31}
32
33impl<T: 'static> EntitySlotExt<T> for Entity<T> {
34 #[track_caller]
35 fn slot<F, E>(&self, id: impl Into<ElementId>, builder: F) -> SlotElement<T, F>
36 where
37 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
38 E: IntoElement + 'static,
39 {
40 SlotElement {
41 entity: self.clone(),
42 id: id.into(),
43 style: StyleRefinement::default(),
44 builder,
45 #[cfg(debug_assertions)]
46 source: core::panic::Location::caller(),
47 }
48 }
49}
50
51pub struct SlotElement<T: 'static, F> {
57 entity: Entity<T>,
58 id: ElementId,
59 style: StyleRefinement,
60 builder: F,
61 #[cfg(debug_assertions)]
62 source: &'static core::panic::Location<'static>,
63}
64
65impl<T: 'static, F, E> SlotElement<T, F>
66where
67 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
68 E: IntoElement + 'static,
69{
70 fn build(&self, window: &mut Window, cx: &mut App) -> AnyElement {
72 self.entity.update(cx, |state, cx| {
73 (self.builder)(state, window, cx).into_any_element()
74 })
75 }
76}
77
78impl<T: 'static, F, E> Styled for SlotElement<T, F>
79where
80 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
81 E: IntoElement + 'static,
82{
83 fn style(&mut self) -> &mut StyleRefinement {
84 &mut self.style
85 }
86}
87
88impl<T: 'static, F, E> IntoElement for SlotElement<T, F>
89where
90 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
91 E: IntoElement + 'static,
92{
93 type Element = Self;
94
95 fn into_element(self) -> Self::Element {
96 self
97 }
98}
99
100impl<T: 'static, F, E> Element for SlotElement<T, F>
101where
102 F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
103 E: IntoElement + 'static,
104{
105 type RequestLayoutState = Option<AnyElement>;
106 type PrepaintState = Option<AnyElement>;
107
108 fn id(&self) -> Option<ElementId> {
109 Some(self.id.clone())
110 }
111
112 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
113 #[cfg(debug_assertions)]
114 return Some(self.source);
115
116 #[cfg(not(debug_assertions))]
117 return None;
118 }
119
120 fn request_layout(
121 &mut self,
122 _id: Option<&GlobalElementId>,
123 window: &mut Window,
124 cx: &mut App,
125 ) -> (LayoutId, Self::RequestLayoutState) {
126 let entity_id = self.entity.entity_id();
127 window.with_rendered_view(entity_id, |window| {
128 if window.is_inspector_picking(cx) {
129 let mut element = self.build(window, cx);
132 let layout_id = element.request_layout(window, cx);
133 (layout_id, Some(element))
134 } else {
135 let mut root_style = Style::default();
136 root_style.refine(&self.style);
137 let layout_id = window.request_layout(root_style, None, cx);
138 (layout_id, None)
139 }
140 })
141 }
142
143 fn prepaint(
144 &mut self,
145 global_id: Option<&GlobalElementId>,
146 bounds: Bounds<Pixels>,
147 element: &mut Self::RequestLayoutState,
148 window: &mut Window,
149 cx: &mut App,
150 ) -> Option<AnyElement> {
151 let entity_id = self.entity.entity_id();
152 window.set_view_id(entity_id);
153 window.with_rendered_view(entity_id, |window| {
154 if let Some(mut element) = element.take() {
155 element.prepaint(window, cx);
156 return Some(element);
157 }
158
159 prepaint_cached_view(entity_id, global_id, bounds, window, cx, |window, cx| {
160 self.build(window, cx)
161 })
162 })
163 }
164
165 fn paint(
166 &mut self,
167 global_id: Option<&GlobalElementId>,
168 _bounds: Bounds<Pixels>,
169 _request_layout: &mut Self::RequestLayoutState,
170 element: &mut Self::PrepaintState,
171 window: &mut Window,
172 cx: &mut App,
173 ) {
174 paint_view(
175 self.entity.entity_id(),
176 true,
177 global_id,
178 element,
179 window,
180 cx,
181 );
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use std::{cell::Cell, rc::Rc};
188
189 use crate::{
190 AnyWindowHandle, AppContext as _, Context, Entity, EntitySlotExt as _, IntoElement,
191 ParentElement as _, Render, Styled as _, TestAppContext, Window, div, green, px,
192 };
193
194 struct Counter(usize);
195
196 struct SlotView {
197 counter: Entity<Counter>,
198 builds: Rc<Cell<usize>>,
199 }
200
201 impl Render for SlotView {
202 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
203 let builds = self.builds.clone();
204 div().size_full().child(
205 self.counter
206 .slot("counter", move |_state, _window, _cx| {
207 builds.set(builds.get() + 1);
208 div().w(px(10.)).h(px(10.)).bg(green())
209 })
210 .size_full(),
211 )
212 }
213 }
214
215 fn draw(cx: &mut TestAppContext, window: AnyWindowHandle) -> usize {
217 cx.update_window(window, |_, window, cx| {
218 window.draw(cx).clear(cx);
219 window.frame_state.rendered_frame.scene.quads.len()
220 })
221 .unwrap()
222 }
223
224 #[gpui::test]
225 fn a_slot_reuses_its_subtree_until_the_entity_notifies(cx: &mut TestAppContext) {
226 let builds = Rc::new(Cell::new(0));
227 let counter = cx.new(|_| Counter(1));
228 let window: AnyWindowHandle = cx
229 .add_window({
230 let counter = counter.clone();
231 let builds = builds.clone();
232 move |_, _| SlotView { counter, builds }
233 })
234 .into();
235
236 let first = draw(cx, window);
237 assert_eq!(builds.get(), 1, "the first frame builds the slot");
238 assert!(first > 0, "the slot's content is painted");
239
240 let second = draw(cx, window);
241 assert_eq!(builds.get(), 1, "a clean slot is not rebuilt");
242 assert_eq!(
243 second, first,
244 "the reused subtree is still painted into the scene"
245 );
246
247 counter.update(cx, |counter, cx| {
248 counter.0 += 1;
249 cx.notify();
250 });
251
252 let third = draw(cx, window);
253 assert!(builds.get() > 1, "notifying the entity rebuilds the slot");
254 assert_eq!(third, first, "the rebuilt subtree paints the same content");
255 }
256}