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/hoverable_test.rs
StratoSDK / crates / strato-ui-core / src / elements / hoverable_test.rs
1use super::*;
2use crate::elements::DispatchEventResult;
3use crate::r#async::Timer;
4use crate::{
5 elements::{
6 ChildAnchor, ConstrainedBox, EventHandler, OffsetPositioning, ParentAnchor, ParentElement,
7 ParentOffsetBounds, Rect, Stack, Text,
8 },
9 fonts::FamilyId,
10 platform::WindowStyle,
11 App, AppContext, Entity, Event, Presenter, TypedActionView, ViewContext, WindowInvalidation,
12};
13use pathfinder_geometry::vector::vec2f;
14use std::{
15 cell::RefCell,
16 collections::{HashMap, HashSet},
17 rc::Rc,
18};
19 
20#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
21enum ElementIdentifier {
22 BottomStack,
23 HoverableElementBottomLeft,
24 HoverableElementTopRight,
25}
26 
27fn mouse_moved_event(position: Vector2F) -> Event {
28 Event::MouseMoved {
29 position,
30 cmd: false,
31 shift: false,
32 is_synthetic: false,
33 }
34}
35 
36#[derive(Default)]
37struct View {
38 // Maps identifier to number of mouse down events
39 mouse_downs: HashMap<ElementIdentifier, usize>,
40 mouse_ups: HashMap<ElementIdentifier, usize>,
41 hover_ins: HashMap<ElementIdentifier, usize>,
42 hover_outs: HashMap<ElementIdentifier, usize>,
43 bottom_mouse_state: MouseStateHandle,
44 top_mouse_state: MouseStateHandle,
45 
46 /// If [`Some`], the top-right hoverable will
47 /// have a hover-in delay with this duration.
48 hover_in_delay: Option<Duration>,
49 
50 /// If [`Some`], the top-right hoverable will
51 /// have a hover-out delay with this duration.
52 hover_out_delay: Option<Duration>,
53}
54 
55pub fn init(app: &mut AppContext) {
56 app.add_action("hoverable_test:mouse_down", View::mouse_down);
57 app.add_action("hoverable_test:mouse_up", View::mouse_up);
58 app.add_action("hoverable_test:hover_in", View::hover_in);
59 app.add_action("hoverable_test:hover_out", View::hover_out);
60}
61 
62impl View {
63 fn with_hover_in_delay(mut self, delay: Duration) -> Self {
64 self.hover_in_delay = Some(delay);
65 self
66 }
67 
68 fn with_hover_out_delay(mut self, delay: Duration) -> Self {
69 self.hover_out_delay = Some(delay);
70 self
71 }
72 
73 fn mouse_down(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
74 log::info!("Recording mouse_down on element {identifier:?}");
75 let entry = self.mouse_downs.entry(*identifier).or_insert(0);
76 *entry += 1;
77 true
78 }
79 
80 fn mouse_up(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
81 log::info!("Recording mouse_up on element {identifier:?}");
82 let entry = self.mouse_ups.entry(*identifier).or_insert(0);
83 *entry += 1;
84 true
85 }
86 
87 fn hover_in(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
88 log::info!("Recording hover in on element {identifier:?}");
89 let entry = self.hover_ins.entry(*identifier).or_insert(0);
90 *entry += 1;
91 true
92 }
93 
94 fn hover_out(&mut self, identifier: &ElementIdentifier, _: &mut ViewContext<Self>) -> bool {
95 log::info!("Recording hover out on element {identifier:?}");
96 let entry = self.hover_outs.entry(*identifier).or_insert(0);
97 *entry += 1;
98 true
99 }
100 
101 fn num_hover_in_events(&self, identifier: &ElementIdentifier) -> usize {
102 *self.hover_ins.get(identifier).unwrap_or(&0)
103 }
104 
105 fn num_hover_out_events(&self, identifier: &ElementIdentifier) -> usize {
106 *self.hover_outs.get(identifier).unwrap_or(&0)
107 }
108}
109 
110impl Entity for View {
111 type Event = ();
112}
113 
114impl crate::core::View for View {
115 fn ui_name() -> &'static str {
116 "hoverable_test_view"
117 }
118 
119 fn render(&self, _: &AppContext) -> Box<dyn Element> {
120 let mut stack = Stack::new();
121 
122 stack.add_child(
123 ConstrainedBox::new(Rect::new().finish())
124 .with_height(100.)
125 .with_width(100.)
126 .finish(),
127 );
128 stack.add_positioned_child(
129 Hoverable::new(self.bottom_mouse_state.clone(), |_| {
130 ConstrainedBox::new(Rect::new().finish())
131 .with_height(25.)
132 .with_width(25.)
133 .finish()
134 })
135 .on_click(|evt, _, _| {
136 evt.dispatch_action(
137 "hoverable_test:mouse_down",
138 ElementIdentifier::HoverableElementBottomLeft,
139 );
140 })
141 .on_hover(|hovered, evt, _, _| {
142 let action_name = if hovered {
143 "hoverable_test:hover_in"
144 } else {
145 "hoverable_test:hover_out"
146 };
147 evt.dispatch_action(action_name, ElementIdentifier::HoverableElementBottomLeft);
148 })
149 .with_cursor(Cursor::Crosshair)
150 .finish(),
151 OffsetPositioning::offset_from_parent(
152 vec2f(0., 75.),
153 ParentOffsetBounds::ParentByPosition,
154 ParentAnchor::TopLeft,
155 ChildAnchor::TopLeft,
156 ),
157 );
158 
159 let mut hoverable = Hoverable::new(self.top_mouse_state.clone(), |_| {
160 ConstrainedBox::new(Rect::new().finish())
161 .with_height(25.)
162 .with_width(25.)
163 .finish()
164 })
165 .on_hover(|hovered, evt, _, _| {
166 let action_name = if hovered {
167 "hoverable_test:hover_in"
168 } else {
169 "hoverable_test:hover_out"
170 };
171 evt.dispatch_action(action_name, ElementIdentifier::HoverableElementTopRight);
172 })
173 .with_cursor(Cursor::PointingHand);
174 
175 if let Some(delay) = self.hover_in_delay {
176 hoverable = hoverable.with_hover_in_delay(delay);
177 }
178 
179 if let Some(delay) = self.hover_out_delay {
180 hoverable = hoverable.with_hover_out_delay(delay);
181 }
182 
183 stack.add_positioned_child(
184 hoverable.finish(),
185 OffsetPositioning::offset_from_parent(
186 vec2f(75., 0.),
187 ParentOffsetBounds::ParentByPosition,
188 ParentAnchor::TopLeft,
189 ChildAnchor::TopLeft,
190 ),
191 );
192 stack.add_positioned_child(
193 ConstrainedBox::new(Rect::new().finish())
194 .with_height(70.)
195 .with_width(70.)
196 .finish(),
197 OffsetPositioning::offset_from_parent(
198 vec2f(15., 15.),
199 ParentOffsetBounds::Unbounded,
200 ParentAnchor::TopLeft,
201 ChildAnchor::TopLeft,
202 ),
203 );
204 
205 let mut scene = Stack::new();
206 scene.add_child(
207 EventHandler::new(stack.finish())
208 .on_left_mouse_down(|evt, _, _| {
209 evt.dispatch_action(
210 "hoverable_test:mouse_down",
211 ElementIdentifier::BottomStack,
212 );
213 DispatchEventResult::StopPropagation
214 })
215 .on_left_mouse_up(|evt, _, _| {
216 evt.dispatch_action("hoverable_test:mouse_up", ElementIdentifier::BottomStack);
217 DispatchEventResult::StopPropagation
218 })
219 .finish(),
220 );
221 scene.finish()
222 }
223}
224 
225impl TypedActionView for View {
226 type Action = ();
227}
228 
229#[test]
230fn test_hoverable_element_click_handling() {
231 App::test((), |mut app| async move {
232 let app = &mut app;
233 app.update(init);
234 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
235 
236 let mut presenter = Presenter::new(window_id);
237 
238 let mut updated = HashSet::new();
239 updated.insert(app.root_view_id(window_id).unwrap());
240 let invalidation = WindowInvalidation {
241 updated,
242 ..Default::default()
243 };
244 
245 app.update(move |ctx| {
246 presenter.invalidate(invalidation, ctx);
247 presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
248 let presenter = Rc::new(RefCell::new(presenter));
249 
250 // Click on the hoverable element on the bottom left corner.
251 // This event should be handled by the Hoverable as it has a
252 // click_handler.
253 ctx.simulate_window_event(
254 Event::LeftMouseDown {
255 position: vec2f(10., 90.),
256 modifiers: Default::default(),
257 click_count: 1,
258 is_first_mouse: false,
259 },
260 window_id,
261 presenter.clone(),
262 );
263 
264 // Mouse up on the hoverable element on the bottom left corner.
265 // This should trigger the click_handler and increment the mouse_down
266 // count on HoverableElement by 1.
267 ctx.simulate_window_event(
268 Event::LeftMouseUp {
269 position: vec2f(10., 90.),
270 modifiers: Default::default(),
271 },
272 window_id,
273 presenter.clone(),
274 );
275 
276 // Click on the hoverable element on the upper right corner.
277 // This event should be handled by the BottomStack instead of Hoverable
278 // as the element does not have a click_handler.
279 ctx.simulate_window_event(
280 Event::LeftMouseDown {
281 position: vec2f(90., 10.),
282 modifiers: Default::default(),
283 click_count: 1,
284 is_first_mouse: false,
285 },
286 window_id,
287 presenter.clone(),
288 );
289 
290 // Mouse up on the hoverable element on the top right corner.
291 // Since the element does not have a click_handler, this should
292 // be captured by the base stack.
293 ctx.simulate_window_event(
294 Event::LeftMouseUp {
295 position: vec2f(90., 10.),
296 modifiers: Default::default(),
297 },
298 window_id,
299 presenter.clone(),
300 );
301 
302 // Click on the base stack.
303 ctx.simulate_window_event(
304 Event::LeftMouseDown {
305 position: vec2f(10., 10.),
306 modifiers: Default::default(),
307 click_count: 1,
308 is_first_mouse: false,
309 },
310 window_id,
311 presenter,
312 );
313 });
314 
315 view.read(app, |view, _| {
316 assert_eq!(
317 2,
318 *view
319 .mouse_downs
320 .get(&ElementIdentifier::BottomStack)
321 .unwrap()
322 );
323 assert_eq!(
324 1,
325 *view
326 .mouse_downs
327 .get(&ElementIdentifier::HoverableElementBottomLeft)
328 .unwrap()
329 );
330 assert_eq!(
331 1,
332 *view.mouse_ups.get(&ElementIdentifier::BottomStack).unwrap()
333 );
334 });
335 });
336}
337 
338#[test]
339fn test_hoverable_element_hover_handling_no_delay() {
340 App::test((), |mut app| async move {
341 let app = &mut app;
342 app.update(init);
343 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
344 
345 let mut presenter = Presenter::new(window_id);
346 
347 let mut updated = HashSet::new();
348 updated.insert(app.root_view_id(window_id).unwrap());
349 let invalidation = WindowInvalidation {
350 updated,
351 ..Default::default()
352 };
353 
354 // Make sure there are no hover events to start.
355 view.read(app, |view, _| {
356 assert_eq!(
357 0,
358 view.num_hover_in_events(&ElementIdentifier::HoverableElementBottomLeft)
359 );
360 assert_eq!(
361 0,
362 view.num_hover_out_events(&ElementIdentifier::HoverableElementBottomLeft)
363 );
364 });
365 
366 app.update(move |ctx| {
367 presenter.invalidate(invalidation, ctx);
368 presenter.build_scene(vec2f(100., 100.), 1., None, ctx);
369 let presenter = Rc::new(RefCell::new(presenter));
370 
371 // Move the mouse over the hoverable element in the bottom left corner.
372 // This event should be handled immediately by the hover handler
373 // without delay.
374 let event = mouse_moved_event(vec2f(10., 90.));
375 
376 // Before the event, the cursor should have it's default shape.
377 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
378 ctx.simulate_window_event(event.clone(), window_id, presenter.clone());
379 ctx.set_last_mouse_move_event(window_id, event);
380 
381 // After the event, the cursor should have the set cursor shape.
382 assert_eq!(ctx.get_cursor_shape(), Cursor::Crosshair);
383 
384 // Move the mouse to over the covering element. Still over the bottom-left
385 // hoverable, but since it's covered, it should be treated as not hovering
386 let event = mouse_moved_event(vec2f(20., 80.));
387 ctx.simulate_window_event(event.clone(), window_id, presenter.clone());
388 ctx.set_last_mouse_move_event(window_id, event);
389 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
390 
391 // Move the mouse back to the bottom-left hoverable (not over the covering element)
392 // This should trigger another hover event
393 let event = mouse_moved_event(vec2f(10., 90.));
394 ctx.simulate_window_event(event.clone(), window_id, presenter.clone());
395 ctx.set_last_mouse_move_event(window_id, event);
396 assert_eq!(ctx.get_cursor_shape(), Cursor::Crosshair);
397 });
398 
399 view.read(app, |view, _| {
400 assert_eq!(
401 2,
402 view.num_hover_in_events(&ElementIdentifier::HoverableElementBottomLeft)
403 );
404 assert_eq!(
405 1,
406 view.num_hover_out_events(&ElementIdentifier::HoverableElementBottomLeft)
407 );
408 });
409 });
410}
411 
412#[test]
413fn test_hoverable_element_hover_handling_with_hover_in_delay() {
414 App::test((), |mut app| async move {
415 let app = &mut app;
416 app.update(init);
417 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
418 View::default().with_hover_in_delay(Duration::from_millis(500))
419 });
420 
421 let presenter = Rc::new(RefCell::new(Presenter::new(window_id)));
422 let presenter_clone = presenter.clone();
423 
424 let mut updated = HashSet::new();
425 updated.insert(app.root_view_id(window_id).unwrap());
426 let invalidation = WindowInvalidation {
427 updated,
428 ..Default::default()
429 };
430 
431 app.update(move |ctx| {
432 presenter.borrow_mut().invalidate(invalidation, ctx);
433 presenter
434 .borrow_mut()
435 .build_scene(vec2f(100., 100.), 1., None, ctx);
436 
437 // Move the mouse over the hoverable in the top-left corner.
438 // This should not immmediately trigger hover events because this hoverable
439 // has a 0.5s hover-in delay.
440 let event = mouse_moved_event(vec2f(90., 10.));
441 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
442 ctx.simulate_window_event(event.clone(), window_id, presenter);
443 ctx.set_last_mouse_move_event(window_id, event);
444 
445 // The cursor, however, should be updated immediately.
446 assert_eq!(ctx.get_cursor_shape(), Cursor::PointingHand);
447 });
448 
449 view.read(app, |view, _| {
450 assert_eq!(
451 0,
452 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
453 );
454 assert_eq!(
455 0,
456 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
457 );
458 });
459 
460 // Wait 1s for the delay to complete, then verify that we got a hover event from the
461 // top-right Hoverable
462 Timer::after(Duration::from_secs(1)).await;
463 view.read(app, |view, _| {
464 assert_eq!(
465 1,
466 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
467 );
468 assert_eq!(
469 0,
470 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
471 );
472 });
473 
474 app.update(move |ctx| {
475 // Move the mouse away from the hoverable.
476 // There's no hover-out delay so there shouldn't
477 // be any delay in registering the hover-out event.
478 let event = mouse_moved_event(vec2f(100., 100.));
479 ctx.simulate_window_event(event.clone(), window_id, presenter_clone);
480 ctx.set_last_mouse_move_event(window_id, event);
481 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
482 });
483 
484 view.read(app, |view, _| {
485 assert_eq!(
486 1,
487 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
488 );
489 assert_eq!(
490 1,
491 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
492 );
493 });
494 });
495}
496 
497#[test]
498fn test_hoverable_element_hover_handling_with_hover_out_delay() {
499 App::test((), |mut app| async move {
500 let app = &mut app;
501 app.update(init);
502 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
503 View::default().with_hover_out_delay(Duration::from_millis(500))
504 });
505 
506 let presenter = Rc::new(RefCell::new(Presenter::new(window_id)));
507 let presenter_clone = presenter.clone();
508 
509 let mut updated = HashSet::new();
510 updated.insert(app.root_view_id(window_id).unwrap());
511 let invalidation = WindowInvalidation {
512 updated,
513 ..Default::default()
514 };
515 
516 app.update(move |ctx| {
517 presenter.borrow_mut().invalidate(invalidation, ctx);
518 presenter
519 .borrow_mut()
520 .build_scene(vec2f(100., 100.), 1., None, ctx);
521 
522 // Move the mouse over the hoverable in the top-left corner.
523 // This should immmediately trigger a hover event because there is no
524 // hover-in delay.
525 let event = mouse_moved_event(vec2f(90., 10.));
526 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
527 ctx.simulate_window_event(event.clone(), window_id, presenter);
528 ctx.set_last_mouse_move_event(window_id, event);
529 assert_eq!(ctx.get_cursor_shape(), Cursor::PointingHand);
530 });
531 
532 view.read(app, |view, _| {
533 assert_eq!(
534 1,
535 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
536 );
537 assert_eq!(
538 0,
539 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
540 );
541 });
542 
543 app.update(move |ctx| {
544 // Move the mouse away from the hoverable.
545 // This should not immmediately trigger hover events because
546 // this hoverable has a 0.5s hover-out delay.
547 let event = mouse_moved_event(vec2f(100., 100.));
548 ctx.simulate_window_event(event.clone(), window_id, presenter_clone);
549 ctx.set_last_mouse_move_event(window_id, event);
550 
551 // The cursor, however, should be updated immediately.
552 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
553 });
554 
555 view.read(app, |view, _| {
556 assert_eq!(
557 1,
558 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
559 );
560 assert_eq!(
561 0,
562 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
563 );
564 });
565 
566 Timer::after(Duration::from_millis(1000)).await;
567 view.read(app, |view, _| {
568 assert_eq!(
569 1,
570 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
571 );
572 assert_eq!(
573 1,
574 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
575 );
576 });
577 });
578}
579 
580#[test]
581fn test_hoverable_element_hover_handling_with_hover_in_out_delay() {
582 App::test((), |mut app| async move {
583 let app = &mut app;
584 app.update(init);
585 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
586 View::default()
587 .with_hover_out_delay(Duration::from_millis(500))
588 .with_hover_in_delay(Duration::from_millis(500))
589 });
590 
591 let presenter = Rc::new(RefCell::new(Presenter::new(window_id)));
592 
593 let mut updated = HashSet::new();
594 updated.insert(app.root_view_id(window_id).unwrap());
595 let invalidation = WindowInvalidation {
596 updated,
597 ..Default::default()
598 };
599 
600 app.update(move |ctx| {
601 presenter.borrow_mut().invalidate(invalidation, ctx);
602 presenter
603 .borrow_mut()
604 .build_scene(vec2f(100., 100.), 1., None, ctx);
605 
606 // Move the mouse over the hoverable in the top-left corner.
607 // This should not immmediately trigger hover events because
608 // this hoverable has a 0.5s hover-out delay.
609 let event = mouse_moved_event(vec2f(90., 10.));
610 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
611 ctx.simulate_window_event(event.clone(), window_id, presenter.clone());
612 ctx.set_last_mouse_move_event(window_id, event);
613 
614 // The cursor should still update immediately.
615 assert_eq!(ctx.get_cursor_shape(), Cursor::PointingHand);
616 
617 // Move the mouse away from the hoverable in the top-left corner.
618 // Again, there's a hover-out delay so no hover events should be
619 // fired still.
620 let event = mouse_moved_event(vec2f(100., 100.));
621 ctx.simulate_window_event(event.clone(), window_id, presenter.clone());
622 ctx.set_last_mouse_move_event(window_id, event);
623 
624 // The cursor should still update immediately.
625 assert_eq!(ctx.get_cursor_shape(), Cursor::Arrow);
626 
627 // Move it back over the hoverable and wait.
628 let event = mouse_moved_event(vec2f(90., 10.));
629 ctx.simulate_window_event(event.clone(), window_id, presenter);
630 ctx.set_last_mouse_move_event(window_id, event);
631 
632 // The cursor should still update immediately.
633 assert_eq!(ctx.get_cursor_shape(), Cursor::PointingHand);
634 });
635 
636 view.read(app, |view, _| {
637 assert_eq!(
638 0,
639 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
640 );
641 assert_eq!(
642 0,
643 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
644 );
645 });
646 
647 // After waiting, there should ultimately be just one hover-in event
648 // for the final mouse movement.
649 //
650 // The other hover-in and hover-out events should have been dropped
651 // due to the mouse moving in and out of the hoverable during the
652 // delay period.
653 Timer::after(Duration::from_millis(1000)).await;
654 view.read(app, |view, _| {
655 assert_eq!(
656 1,
657 view.num_hover_in_events(&ElementIdentifier::HoverableElementTopRight)
658 );
659 assert_eq!(
660 0,
661 view.num_hover_out_events(&ElementIdentifier::HoverableElementTopRight)
662 );
663 });
664 });
665}
666 
667// Why would Elements that haven't been painted need to receive any mouse events?
668// Shouldn't paint happen BEFORE any user interaction? Yes, but remember that Elements are
669// disposable, and may get discarded and created anew between invalidations. Most of the time,
670// Elements do get painted again immediately after creation. However, sometimes Elements can
671// get scrolled out of view, e.g. outside a ClippedScrollable, and may not get painted. If the
672// element, like an Editor, is still focused while out of view, it needs to respond to events
673// without panicking. This test just never paints the Hoverable, and dispatches events on it.
674#[test]
675fn test_unpainted_hoverable_receives_click_events_without_panic() {
676 App::test((), |mut app| async move {
677 let app = &mut app;
678 app.update(init);
679 let (window_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
680 
681 let mut presenter = Presenter::new(window_id);
682 
683 let hover_state = MouseStateHandle::default();
684 let mut hoverable = Hoverable::new(hover_state, |_state| {
685 Text::new_inline("foobar", FamilyId(0), 10.0).finish()
686 });
687 
688 app.update(move |ctx| {
689 // We can't use ctx.simulate_window_event for click events here because that would
690 // require us to paint the scene by calling presenter.build_scene. That's because
691 // that code path uses the sizes and origins of the elements to determine which
692 // elements to dispatch the click event on. Instead, we need to call the dispatch_event
693 // method on the Element directly. That requires us to mock the EventContext, which we
694 // can pluck from the Presenter like so:
695 let mut event_ctx = presenter.mock_event_context(ctx.font_cache());
696 
697 let mouse_down = Event::LeftMouseDown {
698 position: vec2f(10., 90.),
699 modifiers: Default::default(),
700 click_count: 1,
701 is_first_mouse: false,
702 };
703 hoverable.dispatch_event(&DispatchedEvent::from(mouse_down), &mut event_ctx, ctx);
704 
705 let mouse_up = Event::LeftMouseUp {
706 position: vec2f(10., 90.),
707 modifiers: Default::default(),
708 };
709 hoverable.dispatch_event(&DispatchedEvent::from(mouse_up), &mut event_ctx, ctx);
710 });
711 });
712}
713