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/event_handler_test.rs
StratoSDK / crates / strato-ui-core / src / elements / event_handler_test.rs
1use super::*;
2use crate::{
3 elements::{
4 ChildAnchor, ConstrainedBox, OffsetPositioning, ParentAnchor, ParentElement,
5 ParentOffsetBounds, Rect, Stack,
6 },
7 platform::WindowStyle,
8 App, AppContext, Entity, EntityId, Presenter, TypedActionView, ViewContext, WindowInvalidation,
9};
10use pathfinder_geometry::vector::vec2f;
11use std::{
12 collections::{HashMap, HashSet},
13 rc::Rc,
14};
15 
16#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
17enum ElementIdentifier {
18 Base,
19 Inset,
20 Overlay,
21}
22 
23#[derive(Default)]
24struct View {
25 // Maps identifier to number of mouse down events
26 mouse_downs: HashMap<ElementIdentifier, usize>,
27 mouse_ins: HashMap<ElementIdentifier, usize>,
28 mouse_in_behavior: MouseInBehavior,
29}
30 
31pub fn init(app: &mut AppContext) {
32 app.add_action("event_handler_test:mouse_down", View::mouse_down);
33 app.add_action("event_handler_test:mouse_in", View::mouse_in);
34}
35 
36impl View {
37 fn mouse_down(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
38 let entry = self.mouse_downs.entry(*identifier).or_insert(0);
39 *entry += 1;
40 true
41 }
42 
43 fn mouse_in(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
44 let entry = self.mouse_ins.entry(*identifier).or_insert(0);
45 *entry += 1;
46 true
47 }
48}
49 
50impl Entity for View {
51 type Event = ();
52}
53 
54impl View {
55 fn new(mouse_in_behavior: MouseInBehavior) -> Self {
56 Self {
57 mouse_in_behavior,
58 ..Default::default()
59 }
60 }
61}
62 
63impl crate::core::View for View {
64 fn ui_name() -> &'static str {
65 "event_handler_test_view"
66 }
67 
68 fn render(&self, _: &AppContext) -> Box<dyn Element> {
69 let mut inner_stack = Stack::new();
70 inner_stack.add_child(
71 ConstrainedBox::new(Rect::new().finish())
72 .with_height(100.)
73 .with_width(100.)
74 .finish(),
75 );
76 inner_stack.add_positioned_child(
77 EventHandler::new(
78 ConstrainedBox::new(Rect::new().finish())
79 .with_height(25.)
80 .with_width(25.)
81 .finish(),
82 )
83 .on_left_mouse_down(|evt, _, _| {
84 evt.dispatch_action("event_handler_test:mouse_down", ElementIdentifier::Inset);
85 DispatchEventResult::StopPropagation
86 })
87 .on_mouse_in(
88 |evt, _, _| {
89 evt.dispatch_action("event_handler_test:mouse_in", ElementIdentifier::Inset);
90 DispatchEventResult::StopPropagation
91 },
92 Some(self.mouse_in_behavior),
93 )
94 .finish(),
95 OffsetPositioning::offset_from_parent(
96 vec2f(0., 75.),
97 ParentOffsetBounds::ParentByPosition,
98 ParentAnchor::TopLeft,
99 ChildAnchor::TopLeft,
100 ),
101 );
102 
103 let mut stack = Stack::new();
104 stack.add_child(
105 EventHandler::new(inner_stack.finish())
106 .on_left_mouse_down(|evt, _, _| {
107 evt.dispatch_action("event_handler_test:mouse_down", ElementIdentifier::Base);
108 DispatchEventResult::StopPropagation
109 })
110 .on_mouse_in(
111 |evt, _, _| {
112 evt.dispatch_action("event_handler_test:mouse_in", ElementIdentifier::Base);
113 DispatchEventResult::StopPropagation
114 },
115 Some(self.mouse_in_behavior),
116 )
117 .finish(),
118 );
119 stack.add_positioned_child(
120 EventHandler::new(
121 ConstrainedBox::new(Rect::new().finish())
122 .with_height(25.)
123 .with_width(25.)
124 .finish(),
125 )
126 .on_left_mouse_down(|evt, _, _| {
127 evt.dispatch_action("event_handler_test:mouse_down", ElementIdentifier::Overlay);
128 DispatchEventResult::StopPropagation
129 })
130 .on_mouse_in(
131 |evt, _, _| {
132 evt.dispatch_action("event_handler_test:mouse_in", ElementIdentifier::Overlay);
133 DispatchEventResult::StopPropagation
134 },
135 Some(self.mouse_in_behavior),
136 )
137 .finish(),
138 OffsetPositioning::offset_from_parent(
139 vec2f(75., 0.),
140 ParentOffsetBounds::ParentByPosition,
141 ParentAnchor::TopLeft,
142 ChildAnchor::TopLeft,
143 ),
144 );
145 
146 stack.finish()
147 }
148}
149 
150impl TypedActionView for View {
151 type Action = ();
152}
153 
154#[test]
155fn test_layered_click_handling() {
156 App::test((), |mut app| async move {
157 let app = &mut app;
158 app.update(init);
159 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
160 
161 let mut presenter = Presenter::new(window_id);
162 
163 let mut updated = HashSet::new();
164 updated.insert(app.root_view_id(window_id).unwrap());
165 let invalidation = WindowInvalidation {
166 updated,
167 ..Default::default()
168 };
169 
170 app.update(move |ctx| {
171 presenter.invalidate(invalidation, ctx);
172 let scene = presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
173 assert_eq!(scene.z_index(), ZIndex::new(0));
174 assert_eq!(scene.layer_count(), 5);
175 let presenter = Rc::new(RefCell::new(presenter));
176 
177 // Click on the overlay
178 ctx.simulate_window_event(
179 Event::LeftMouseDown {
180 position: vec2f(90., 10.),
181 modifiers: Default::default(),
182 click_count: 1,
183 is_first_mouse: false,
184 },
185 window_id,
186 presenter.clone(),
187 );
188 
189 // Click on the inset
190 ctx.simulate_window_event(
191 Event::LeftMouseDown {
192 position: vec2f(10., 90.),
193 modifiers: Default::default(),
194 click_count: 1,
195 is_first_mouse: false,
196 },
197 window_id,
198 presenter.clone(),
199 );
200 
201 // Click on the top-left area of the base
202 ctx.simulate_window_event(
203 Event::LeftMouseDown {
204 position: vec2f(10., 10.),
205 modifiers: Default::default(),
206 click_count: 1,
207 is_first_mouse: false,
208 },
209 window_id,
210 presenter.clone(),
211 );
212 
213 // Click on the bottom-right area of the base
214 ctx.simulate_window_event(
215 Event::LeftMouseDown {
216 position: vec2f(90., 90.),
217 modifiers: Default::default(),
218 click_count: 1,
219 is_first_mouse: false,
220 },
221 window_id,
222 presenter,
223 );
224 });
225 
226 view.read(app, |view, _| {
227 assert_eq!(
228 1,
229 *view.mouse_downs.get(&ElementIdentifier::Overlay).unwrap()
230 );
231 assert_eq!(1, *view.mouse_downs.get(&ElementIdentifier::Inset).unwrap());
232 assert_eq!(2, *view.mouse_downs.get(&ElementIdentifier::Base).unwrap());
233 });
234 });
235}
236 
237#[test]
238fn test_default_mouse_in_behavior() {
239 App::test((), |mut app| async move {
240 let app = &mut app;
241 app.update(init);
242 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
243 
244 let mut presenter = Presenter::new(window_id);
245 
246 let mut updated = HashSet::new();
247 updated.insert(app.root_view_id(window_id).unwrap());
248 let invalidation = WindowInvalidation {
249 updated,
250 ..Default::default()
251 };
252 
253 app.update(move |ctx| {
254 presenter.invalidate(invalidation, ctx);
255 let scene = presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
256 assert_eq!(scene.z_index(), ZIndex::new(0));
257 assert_eq!(scene.layer_count(), 5);
258 let presenter = Rc::new(RefCell::new(presenter));
259 
260 // Non-synthetic move over the overlay
261 ctx.simulate_window_event(
262 Event::MouseMoved {
263 position: vec2f(90., 10.),
264 cmd: false,
265 shift: false,
266 is_synthetic: false,
267 },
268 window_id,
269 presenter.clone(),
270 );
271 
272 // Non-synthetic move over the inset
273 ctx.simulate_window_event(
274 Event::MouseMoved {
275 position: vec2f(10., 90.),
276 cmd: false,
277 shift: false,
278 is_synthetic: false,
279 },
280 window_id,
281 presenter.clone(),
282 );
283 
284 // Non-synthetic move over top left the base
285 ctx.simulate_window_event(
286 Event::MouseMoved {
287 position: vec2f(10., 10.),
288 cmd: false,
289 shift: false,
290 is_synthetic: false,
291 },
292 window_id,
293 presenter.clone(),
294 );
295 
296 // Non-synthetic move over the bottom right of base
297 ctx.simulate_window_event(
298 Event::MouseMoved {
299 position: vec2f(90., 90.),
300 cmd: false,
301 shift: false,
302 is_synthetic: false,
303 },
304 window_id,
305 presenter.clone(),
306 );
307 });
308 
309 view.read(app, |view, _| {
310 assert_eq!(1, *view.mouse_ins.get(&ElementIdentifier::Overlay).unwrap());
311 assert_eq!(1, *view.mouse_ins.get(&ElementIdentifier::Inset).unwrap());
312 // Only 2 events should be fired because 1) the inset is a child of the base
313 // and doesn't propagate events to its parent 2) the overlay event is not propagated
314 // to the base.
315 assert_eq!(2, *view.mouse_ins.get(&ElementIdentifier::Base).unwrap());
316 });
317 });
318}
319 
320#[test]
321fn test_mouse_in_behavior_dont_fire_on_synthetic_events() {
322 App::test((), |mut app| async move {
323 let app = &mut app;
324 app.update(init);
325 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
326 View::new(MouseInBehavior {
327 fire_on_synthetic_events: false,
328 fire_when_covered: true,
329 })
330 });
331 
332 let mut presenter = Presenter::new(window_id);
333 
334 let mut updated = HashSet::new();
335 updated.insert(app.root_view_id(window_id).unwrap());
336 let invalidation = WindowInvalidation {
337 updated,
338 ..Default::default()
339 };
340 
341 app.update(move |ctx| {
342 presenter.invalidate(invalidation, ctx);
343 let scene = presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
344 assert_eq!(scene.z_index(), ZIndex::new(0));
345 assert_eq!(scene.layer_count(), 5);
346 let presenter = Rc::new(RefCell::new(presenter));
347 
348 // Non-synthetic move over the overlay
349 ctx.simulate_window_event(
350 Event::MouseMoved {
351 position: vec2f(90., 10.),
352 cmd: false,
353 shift: false,
354 is_synthetic: true,
355 },
356 window_id,
357 presenter.clone(),
358 );
359 });
360 
361 view.read(app, |view, _| {
362 assert_eq!(
363 0,
364 *view
365 .mouse_ins
366 .get(&ElementIdentifier::Overlay)
367 .unwrap_or(&0)
368 );
369 assert_eq!(
370 0,
371 *view.mouse_ins.get(&ElementIdentifier::Inset).unwrap_or(&0)
372 );
373 assert_eq!(
374 0,
375 *view.mouse_ins.get(&ElementIdentifier::Base).unwrap_or(&0)
376 );
377 });
378 });
379}
380 
381#[test]
382fn test_mouse_in_behavior_dont_fire_when_covered() {
383 App::test((), |mut app| async move {
384 let app = &mut app;
385 app.update(init);
386 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
387 View::new(MouseInBehavior {
388 fire_on_synthetic_events: true,
389 fire_when_covered: false,
390 })
391 });
392 
393 let mut presenter = Presenter::new(window_id);
394 
395 let mut updated = HashSet::new();
396 updated.insert(app.root_view_id(window_id).unwrap());
397 let invalidation = WindowInvalidation {
398 updated,
399 ..Default::default()
400 };
401 
402 app.update(move |ctx| {
403 presenter.invalidate(invalidation, ctx);
404 let scene = presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
405 assert_eq!(scene.z_index(), ZIndex::new(0));
406 assert_eq!(scene.layer_count(), 5);
407 let presenter = Rc::new(RefCell::new(presenter));
408 
409 // Non-synthetic move over the overlay
410 ctx.simulate_window_event(
411 Event::MouseMoved {
412 position: vec2f(90., 10.),
413 cmd: false,
414 shift: false,
415 is_synthetic: false,
416 },
417 window_id,
418 presenter.clone(),
419 );
420 
421 // Non-synthetic move over the inset
422 ctx.simulate_window_event(
423 Event::MouseMoved {
424 position: vec2f(10., 90.),
425 cmd: false,
426 shift: false,
427 is_synthetic: false,
428 },
429 window_id,
430 presenter.clone(),
431 );
432 
433 // Non-synthetic move over top left the base
434 ctx.simulate_window_event(
435 Event::MouseMoved {
436 position: vec2f(10., 10.),
437 cmd: false,
438 shift: false,
439 is_synthetic: false,
440 },
441 window_id,
442 presenter.clone(),
443 );
444 
445 // Non-synthetic move over the bottom right of base
446 ctx.simulate_window_event(
447 Event::MouseMoved {
448 position: vec2f(90., 90.),
449 cmd: false,
450 shift: false,
451 is_synthetic: false,
452 },
453 window_id,
454 presenter.clone(),
455 );
456 });
457 
458 view.read(app, |view, _| {
459 assert_eq!(1, *view.mouse_ins.get(&ElementIdentifier::Overlay).unwrap());
460 assert_eq!(1, *view.mouse_ins.get(&ElementIdentifier::Inset).unwrap());
461 assert_eq!(2, *view.mouse_ins.get(&ElementIdentifier::Base).unwrap());
462 });
463 });
464}
465 
466/// For testing event propagation
467#[derive(Debug)]
468enum PropagationViewAction {
469 MouseDown(ElementIdentifier),
470}
471 
472#[derive(Default)]
473struct PropagationView {
474 // Maps identifier to number of mouse down events
475 mouse_downs: HashMap<ElementIdentifier, usize>,
476 allow_propagation: bool,
477}
478 
479impl PropagationView {
480 fn mouse_down(&mut self, identifier: &ElementIdentifier) -> bool {
481 let entry = self.mouse_downs.entry(*identifier).or_insert(0);
482 *entry += 1;
483 true
484 }
485 
486 fn set_propagation(&mut self, allow_propagation: bool, ctx: &mut ViewContext<Self>) {
487 self.allow_propagation = allow_propagation;
488 ctx.notify();
489 }
490}
491 
492impl Entity for PropagationView {
493 type Event = ();
494}
495 
496impl crate::core::View for PropagationView {
497 fn ui_name() -> &'static str {
498 "event_handler_test_propagation_view"
499 }
500 
501 fn render(&self, _: &AppContext) -> Box<dyn Element> {
502 let allow_propagation = self.allow_propagation;
503 
504 let handler = EventHandler::new(
505 ConstrainedBox::new(Rect::new().finish())
506 .with_height(100.)
507 .with_width(100.)
508 .finish(),
509 )
510 .on_left_mouse_down(move |evt, _, _| {
511 evt.dispatch_typed_action(PropagationViewAction::MouseDown(ElementIdentifier::Inset));
512 if allow_propagation {
513 DispatchEventResult::PropagateToParent
514 } else {
515 DispatchEventResult::StopPropagation
516 }
517 })
518 .finish();
519 
520 EventHandler::new(handler)
521 .on_left_mouse_down(|evt, _, _| {
522 evt.dispatch_typed_action(PropagationViewAction::MouseDown(
523 ElementIdentifier::Base,
524 ));
525 DispatchEventResult::StopPropagation
526 })
527 .finish()
528 }
529}
530 
531impl TypedActionView for PropagationView {
532 type Action = PropagationViewAction;
533 
534 fn handle_action(&mut self, action: &Self::Action, _: &mut ViewContext<Self>) {
535 match action {
536 PropagationViewAction::MouseDown(identifier) => {
537 self.mouse_down(identifier);
538 }
539 }
540 }
541}
542 
543fn invalidate_and_rebuild_scene(
544 presenter: &Rc<RefCell<Presenter>>,
545 root_view_id: EntityId,
546 ctx: &mut AppContext,
547) {
548 let mut updated = HashSet::new();
549 updated.insert(root_view_id);
550 let invalidation = WindowInvalidation {
551 updated,
552 ..Default::default()
553 };
554 presenter.borrow_mut().invalidate(invalidation, ctx);
555 presenter
556 .borrow_mut()
557 .build_scene(vec2f(100., 100.), 1., None, ctx);
558}
559 
560#[test]
561fn test_event_propagation() {
562 App::test((), |mut app| async move {
563 let (window_id, view) =
564 app.add_window(WindowStyle::NotStealFocus, |_| PropagationView::default());
565 
566 let root_view_id = view.id();
567 app.update(move |ctx| {
568 invalidate_and_rebuild_scene(
569 &ctx.presenter(window_id).expect("Window should exist"),
570 root_view_id,
571 ctx,
572 );
573 
574 // Click on the inset with propagation disabled
575 ctx.simulate_window_event(
576 Event::LeftMouseDown {
577 position: vec2f(90., 10.),
578 modifiers: Default::default(),
579 click_count: 1,
580 is_first_mouse: false,
581 },
582 window_id,
583 ctx.presenter(window_id)
584 .expect("window should exist")
585 .clone(),
586 );
587 });
588 
589 view.read(&app, |view, _| {
590 assert_eq!(1, *view.mouse_downs.get(&ElementIdentifier::Inset).unwrap());
591 assert_eq!(view.mouse_downs.get(&ElementIdentifier::Base), None);
592 });
593 
594 // Allow propagation
595 view.update(&mut app, |view, ctx| {
596 view.set_propagation(true, ctx);
597 });
598 
599 app.update(move |ctx| {
600 // Click on the inset with propagation enabled
601 ctx.simulate_window_event(
602 Event::LeftMouseDown {
603 position: vec2f(90., 10.),
604 modifiers: Default::default(),
605 click_count: 1,
606 is_first_mouse: false,
607 },
608 window_id,
609 ctx.presenter(window_id)
610 .expect("window should exist")
611 .clone(),
612 );
613 });
614 
615 // Both the inset and the base should have received the even
616 view.read(&app, |view, _| {
617 assert_eq!(2, *view.mouse_downs.get(&ElementIdentifier::Inset).unwrap());
618 assert_eq!(1, *view.mouse_downs.get(&ElementIdentifier::Base).unwrap());
619 });
620 })
621}
622