Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/clipped_test.rs
StratoSDK / crates / strato-ui-core / src / elements / clipped_test.rs
1use super::*;
2use crate::{
3 elements::{
4 ChildAnchor, ConstrainedBox, DispatchEventResult, EventHandler, Hoverable,
5 MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Rect,
6 Stack, ZIndex,
7 },
8 platform::WindowStyle,
9 App, AppContext, Entity, Event, Presenter, TypedActionView, ViewContext, WindowInvalidation,
10};
11use pathfinder_geometry::vector::vec2f;
12use std::{
13 cell::RefCell,
14 collections::{HashMap, HashSet},
15 rc::Rc,
16};
17 
18#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
19enum ElementIdentifier {
20 BottomStack,
21 TopStackBase,
22 TopStackOverlay,
23 Hoverable,
24}
25 
26#[derive(Default)]
27struct View {
28 // Maps identifier to number of mouse down events
29 mouse_downs: HashMap<ElementIdentifier, usize>,
30 mouse_state: MouseStateHandle,
31}
32 
33pub fn init(app: &mut AppContext) {
34 app.add_action("clipped_test:mouse_down", View::mouse_down);
35}
36 
37impl View {
38 fn mouse_down(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
39 log::info!("Recording mouse_down on element {identifier:?}");
40 let entry = self.mouse_downs.entry(*identifier).or_insert(0);
41 *entry += 1;
42 true
43 }
44}
45 
46impl Entity for View {
47 type Event = ();
48}
49 
50impl crate::core::View for View {
51 fn ui_name() -> &'static str {
52 "clipped_test_view"
53 }
54 
55 // The element tree looks like the following:
56 // - Scene
57 // - Stack
58 // - Bottom Stack
59 // - Base (This acts as the base layer for the scene)
60 // - BottomStack
61 // - Top Stack
62 // - TopStackBase (Bottom)
63 // - TopStackOverlay (Top)
64 //
65 // --------------------------------
66 // | TopStackBase | Hoverable | |
67 // | (Clipped) | | | |
68 // | --|---------- |
69 // | | |
70 // | -----------|------- |
71 // | | Overlap | | |
72 // |----------------- | |
73 // | | | |
74 // | | | |
75 // | |TopStackOverlay | |
76 // | ------------------- |
77 // | |
78 // | |
79 // |-------------- |
80 // | | |
81 // | | |
82 // | | |
83 // | | |
84 // |BottomStack | |
85 // --------------------------------
86 fn render(&self, _: &AppContext) -> Box<dyn Element> {
87 let mut bottom_stack = Stack::new();
88 
89 bottom_stack.add_child(
90 ConstrainedBox::new(Rect::new().finish())
91 .with_height(100.)
92 .with_width(50.)
93 .finish(),
94 );
95 bottom_stack.add_positioned_child(
96 EventHandler::new(
97 ConstrainedBox::new(Rect::new().finish())
98 .with_height(25.)
99 .with_width(25.)
100 .finish(),
101 )
102 .on_left_mouse_down(|evt, _, _| {
103 evt.dispatch_action("clipped_test:mouse_down", ElementIdentifier::BottomStack);
104 DispatchEventResult::StopPropagation
105 })
106 .finish(),
107 OffsetPositioning::offset_from_parent(
108 vec2f(0., 75.),
109 ParentOffsetBounds::ParentByPosition,
110 ParentAnchor::TopLeft,
111 ChildAnchor::TopLeft,
112 ),
113 );
114 
115 let mut top_stack = Stack::new();
116 
117 top_stack.add_positioned_child(
118 EventHandler::new(
119 ConstrainedBox::new(Rect::new().finish())
120 .with_height(25.)
121 .with_width(25.)
122 .finish(),
123 )
124 .on_left_mouse_down(|evt, _, _| {
125 evt.dispatch_action("clipped_test:mouse_down", ElementIdentifier::TopStackBase);
126 DispatchEventResult::StopPropagation
127 })
128 .finish(),
129 OffsetPositioning::offset_from_parent(
130 vec2f(0., 0.),
131 ParentOffsetBounds::ParentByPosition,
132 ParentAnchor::TopLeft,
133 ChildAnchor::TopLeft,
134 ),
135 );
136 
137 top_stack.add_positioned_child(
138 EventHandler::new(
139 ConstrainedBox::new(Rect::new().finish())
140 .with_height(25.)
141 .with_width(25.)
142 .finish(),
143 )
144 .on_left_mouse_down(|evt, _, _| {
145 evt.dispatch_action(
146 "clipped_test:mouse_down",
147 ElementIdentifier::TopStackOverlay,
148 );
149 DispatchEventResult::StopPropagation
150 })
151 .finish(),
152 OffsetPositioning::offset_from_parent(
153 vec2f(15., 15.),
154 ParentOffsetBounds::ParentByPosition,
155 ParentAnchor::TopLeft,
156 ChildAnchor::TopLeft,
157 ),
158 );
159 
160 top_stack.add_positioned_child(
161 Hoverable::new(self.mouse_state.clone(), |_| {
162 ConstrainedBox::new(Rect::new().finish())
163 .with_height(20.)
164 .with_height(8.)
165 .finish()
166 })
167 .on_click(|evt, _, _| {
168 evt.dispatch_action("clipped_test:mouse_down", ElementIdentifier::Hoverable);
169 })
170 .finish(),
171 OffsetPositioning::offset_from_parent(
172 vec2f(15., 0.),
173 ParentOffsetBounds::ParentByPosition,
174 ParentAnchor::TopLeft,
175 ChildAnchor::TopLeft,
176 ),
177 );
178 
179 let mut stack = Stack::new();
180 stack.add_child(bottom_stack.finish());
181 stack.add_child(Clipped::sized(top_stack.finish(), Vector2F::new(25., 25.)).finish());
182 
183 // Force the Stack to take up the full size of the window by pulling
184 // the minimum size constraint up to the size of the window.
185 ConstrainedBox::new(stack.finish())
186 .with_min_width(f32::MAX)
187 .with_min_height(f32::MAX)
188 .finish()
189 }
190}
191 
192impl TypedActionView for View {
193 type Action = ();
194}
195 
196#[test]
197fn test_clipped_element_click_handling() {
198 App::test((), |mut app| async move {
199 let app = &mut app;
200 app.update(init);
201 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
202 
203 let mut presenter = Presenter::new(window_id);
204 
205 let mut updated = HashSet::new();
206 updated.insert(app.root_view_id(window_id).unwrap());
207 let invalidation = WindowInvalidation {
208 updated,
209 ..Default::default()
210 };
211 
212 app.update(move |ctx| {
213 presenter.invalidate(invalidation, ctx);
214 let scene = presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
215 assert_eq!(scene.z_index(), ZIndex::new(0));
216 assert_eq!(scene.layer_count(), 9);
217 let presenter = Rc::new(RefCell::new(presenter));
218 
219 // Click on the bottom stack. This should work because the bottom
220 // stack is not clipped.
221 ctx.simulate_window_event(
222 Event::LeftMouseDown {
223 position: vec2f(10., 90.),
224 modifiers: Default::default(),
225 click_count: 1,
226 is_first_mouse: false,
227 },
228 window_id,
229 presenter.clone(),
230 );
231 
232 // Click on the top stack base. This should work because the
233 // click is within the clipped range of the top stack.
234 ctx.simulate_window_event(
235 Event::LeftMouseDown {
236 position: vec2f(10., 10.),
237 modifiers: Default::default(),
238 click_count: 1,
239 is_first_mouse: false,
240 },
241 window_id,
242 presenter.clone(),
243 );
244 
245 // Click on the overlap between top stack base and top stack overlay.
246 // This should work because the click is still within the clipped
247 // range of the top stack.
248 ctx.simulate_window_event(
249 Event::LeftMouseDown {
250 position: vec2f(20., 20.),
251 modifiers: Default::default(),
252 click_count: 1,
253 is_first_mouse: false,
254 },
255 window_id,
256 presenter.clone(),
257 );
258 
259 // Click on the part of top stack overlay not overlapping with base.
260 // This should not work because it is outside of the clip bound of the
261 // base stack.
262 ctx.simulate_window_event(
263 Event::LeftMouseDown {
264 position: vec2f(30., 30.),
265 modifiers: Default::default(),
266 click_count: 1,
267 is_first_mouse: false,
268 },
269 window_id,
270 presenter.clone(),
271 );
272 
273 // Click on the overlap between hoverable and top stack overlay.
274 // This should work because the click is still within the clipped
275 // range of the top stack.
276 // Note Hoverable needs both mouse down and mouse up to fire the
277 // on click event.
278 ctx.simulate_window_event(
279 Event::LeftMouseDown {
280 position: vec2f(20., 5.),
281 modifiers: Default::default(),
282 click_count: 1,
283 is_first_mouse: false,
284 },
285 window_id,
286 presenter.clone(),
287 );
288 
289 ctx.simulate_window_event(
290 Event::LeftMouseUp {
291 position: vec2f(20., 5.),
292 modifiers: Default::default(),
293 },
294 window_id,
295 presenter.clone(),
296 );
297 
298 // Click on the part of hoverable not overlapping with base.
299 // This should not work because it is outside of the clip bound of the
300 // base stack.
301 ctx.simulate_window_event(
302 Event::LeftMouseDown {
303 position: vec2f(30., 5.),
304 modifiers: Default::default(),
305 click_count: 1,
306 is_first_mouse: false,
307 },
308 window_id,
309 presenter.clone(),
310 );
311 
312 ctx.simulate_window_event(
313 Event::LeftMouseUp {
314 position: vec2f(30., 5.),
315 modifiers: Default::default(),
316 },
317 window_id,
318 presenter,
319 );
320 });
321 
322 view.read(app, |view, _| {
323 assert_eq!(
324 1,
325 *view
326 .mouse_downs
327 .get(&ElementIdentifier::BottomStack)
328 .unwrap()
329 );
330 assert_eq!(
331 1,
332 *view
333 .mouse_downs
334 .get(&ElementIdentifier::TopStackBase)
335 .unwrap()
336 );
337 assert_eq!(
338 1,
339 *view
340 .mouse_downs
341 .get(&ElementIdentifier::TopStackOverlay)
342 .unwrap()
343 );
344 assert_eq!(
345 1,
346 *view.mouse_downs.get(&ElementIdentifier::Hoverable).unwrap()
347 );
348 });
349 });
350}
351