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/presenter.rs
StratoSDK / crates / strato-ui-core / src / presenter.rs
1use super::{elements::Axis, Event};
2use crate::assets::asset_cache::AssetHandle;
3use crate::elements::{DropTargetPosition, Selection};
4 
5use crate::fonts;
6use crate::zoom::Scale;
7use crate::{
8 elements::Point,
9 event::DispatchedEvent,
10 fonts::Cache as FontCache,
11 platform::Cursor,
12 scene::{Scene, ZIndex},
13 text_layout::LayoutCache,
14 Action, AppContext, ClipBounds, EntityId, TaskId, View, ViewHandle, WindowId,
15 WindowInvalidation,
16};
17use instant::Instant;
18use pathfinder_geometry::vector::{vec2f, Vector2F};
19use std::{
20 any::Any,
21 collections::{HashMap, HashSet},
22 marker::PhantomData,
23 rc::Rc,
24 time::Duration,
25};
26 
27pub struct Presenter {
28 // Number of frames rendered so far by this presenter
29 frame_count: usize,
30 window_id: WindowId,
31 scene: Option<Rc<Scene>>,
32 rendered_views: HashMap<EntityId, Box<dyn Element>>,
33 parents: HashMap<EntityId, EntityId>,
34 text_layout_cache: LayoutCache,
35 position_cache: PositionCache,
36 highlighted_view: Option<EntityId>,
37}
38 
39pub struct LayoutContext<'a> {
40 rendered_views: &'a mut HashMap<EntityId, Box<dyn Element>>,
41 parents: &'a mut HashMap<EntityId, EntityId>,
42 pub text_layout_cache: &'a LayoutCache,
43 view_stack: Vec<EntityId>,
44 pub window_size: Vector2F,
45 pub position_cache: &'a PositionCache,
46}
47 
48pub struct AfterLayoutContext<'a> {
49 rendered_views: &'a mut HashMap<EntityId, Box<dyn Element>>,
50 pub text_layout_cache: &'a LayoutCache,
51}
52 
53pub struct PaintContext<'a> {
54 rendered_views: &'a mut HashMap<EntityId, Box<dyn Element>>,
55 pub font_cache: &'a FontCache,
56 pub text_layout_cache: &'a LayoutCache,
57 pub position_cache: &'a mut PositionCache,
58 pub scene: &'a mut Scene,
59 pub window_size: Vector2F,
60 /// The maximum dimension size in pixels, either width or height, for a 2D-texture. `None`
61 /// will be treated as unbounded.
62 pub max_texture_dimension_2d: Option<u32>,
63 pub highlighted_view: Option<EntityId>,
64 pub current_selection: Option<Selection>,
65 /// Holds the time the scene should be repainted next, if animated.
66 repaint_at: Option<Instant>,
67 pending_assets: HashSet<AssetHandle>,
68 /// Keep track of all the views that were actually painted in this scene.
69 views_painted: HashSet<EntityId>,
70}
71 
72#[derive(Default)]
73pub struct DispatchResult {
74 /// Whether the event was marked as handled, either by the RootView or a descendent
75 pub handled: bool,
76 
77 /// All actions to dispatch as a result of the event being handled
78 pub actions: Vec<DispatchedAction>,
79 
80 /// All views to notify as a result of the event being handled
81 pub notified: HashSet<EntityId>,
82 
83 /// Views that need to be notified after a delay
84 pub notify_timers_to_set: HashMap<TaskId, ViewToNotify>,
85 
86 /// Views that need to have notify timers cleared
87 pub notify_timers_to_clear: HashSet<TaskId>,
88 
89 /// An optional update to the mouse cursor
90 pub cursor_update: Option<CursorUpdate>,
91 
92 /// Whether the soft keyboard was requested by an element during dispatch.
93 /// Used on mobile WASM to trigger the keyboard in user gesture context.
94 pub soft_keyboard_requested: bool,
95}
96 
97#[derive(Debug, Copy, Clone)]
98pub struct ViewToNotify {
99 /// The view to notify after a delay
100 pub view_id: EntityId,
101 
102 /// The time to notify the view
103 pub notify_at: Instant,
104}
105 
106#[derive(Debug, Clone)]
107pub enum CursorUpdate {
108 /// Set the cursor to the given cursor type at the given z-index
109 Set {
110 cursor: Cursor,
111 z_index: ZIndex,
112 view_id: EntityId,
113 },
114 
115 /// Reset top the default cursor (usually the pointer)
116 Reset,
117}
118 
119/// A set of element rects that are cached across frames
120/// The API allows for callers to control how conflicting position ids are
121/// handled. The typical usage is that in a stack, every time you paint at
122/// a higher z-index, you start a new stack context. Every element at the
123/// current z-index is in the same position namespace. Elements at lower
124/// z-indices will take precedence over elements at higher z-indices if there
125/// are naming conflicts.
126#[derive(Default, Clone)]
127pub struct PositionCache {
128 /// A stack of pending positions to cache
129 pending_positions: Vec<HashMap<String, RectF>>,
130 
131 /// The positions that have been committed to the cache.
132 committed_positions: HashMap<String, RectF>,
133 
134 /// Positions that are only cached for a single frame
135 single_frame_positions: HashSet<String>,
136 
137 /// Positions for a drop target. These positions are always cleared on every frame.
138 drop_target_positions: Vec<DropTargetPosition>,
139}
140 
141impl PositionCache {
142 pub fn new() -> Self {
143 PositionCache {
144 pending_positions: Default::default(),
145 committed_positions: Default::default(),
146 single_frame_positions: Default::default(),
147 drop_target_positions: Default::default(),
148 }
149 }
150 
151 /// Starts a new namespace for position id caching.
152 pub fn start(&mut self) {
153 self.pending_positions.push(HashMap::new());
154 }
155 
156 /// Ends the current namespace for position id caching, and commits all
157 /// of the positions.
158 pub fn end(&mut self) {
159 let mut last = self
160 .pending_positions
161 .pop()
162 .expect("mismatched stack start/end");
163 self.committed_positions.extend(last.drain());
164 }
165 
166 /// Caches a position in the current namespace. This position will remain
167 /// cached until it's explicitly cleared.
168 pub fn cache_position_indefinitely(&mut self, position_id: String, bounds: RectF) {
169 if let Some(last) = self.pending_positions.last_mut() {
170 last.insert(position_id.clone(), bounds);
171 self.single_frame_positions.remove(&position_id);
172 }
173 }
174 
175 /// Caches a position in the current namespace until the next frame is rendered.
176 pub fn cache_position_for_one_frame(&mut self, position_id: String, bounds: RectF) {
177 if let Some(last) = self.pending_positions.last_mut() {
178 last.insert(position_id.clone(), bounds);
179 self.single_frame_positions.insert(position_id);
180 }
181 }
182 
183 pub(crate) fn cache_drop_target_position(&mut self, drop_target_position: DropTargetPosition) {
184 self.drop_target_positions.push(drop_target_position);
185 }
186 
187 /// Clears a position from the cache.
188 pub fn clear_position<S>(&mut self, position_id: S)
189 where
190 S: AsRef<str>,
191 {
192 self.committed_positions.remove(position_id.as_ref());
193 self.single_frame_positions.remove(position_id.as_ref());
194 }
195 
196 /// Clears any positions that should be cached for a single frame. This always clears any cached
197 /// drop target positions--we don't permit them to be cached for multiple frames.
198 pub fn clear_single_frame_positions(&mut self) {
199 for position_id in self.single_frame_positions.drain() {
200 self.committed_positions.remove(&position_id);
201 }
202 self.drop_target_positions.clear();
203 }
204 
205 /// Returns a cached position, if there is one.
206 pub fn get_position<S>(&self, position_id: S) -> Option<RectF>
207 where
208 S: AsRef<str>,
209 {
210 self.committed_positions.get(position_id.as_ref()).copied()
211 }
212 
213 /// Returns an iterator of `DropTargetPosition`s. Used to determine if a draggable element
214 /// was dropped on a `DropTarget`.
215 pub(crate) fn drop_target_data(&self) -> impl Iterator<Item = DropTargetPosition> + '_ {
216 self.drop_target_positions.iter().cloned()
217 }
218}
219 
220pub struct EventContext<'a> {
221 // Scene is optional because it's technically possible for a window event to
222 // be fired before the first scene has been rendered.
223 scene: Option<Rc<Scene>>,
224 rendered_views: &'a mut HashMap<EntityId, Box<dyn Element>>,
225 actions: Vec<DispatchedAction>,
226 pub font_cache: &'a FontCache,
227 pub text_layout_cache: &'a LayoutCache,
228 position_cache: &'a PositionCache,
229 view_stack: Vec<EntityId>,
230 notified: HashSet<EntityId>,
231 /// A map of timer ids to (view_id, duration) pairs for delayed notification
232 notify_timers_to_set: HashMap<TaskId, ViewToNotify>,
233 notify_timers_to_clear: HashSet<TaskId>,
234 /// Any update to the cursor after the processing of events
235 /// For now it's highest z-index wins if multiple elements try to set the
236 /// cursor (later we could make this more sophisticated)
237 cursor_update: Option<CursorUpdate>,
238 /// Flag indicating the soft keyboard should be shown.
239 /// Used on mobile WASM to trigger the keyboard in user gesture context.
240 soft_keyboard_requested: bool,
241}
242 
243impl<'a> EventContext<'a> {
244 /// Returns whether the given position is covered by a rect at a higher index
245 pub fn is_covered(&self, position: Point) -> bool {
246 self.scene
247 .as_ref()
248 .is_some_and(|scene| scene.is_covered(position))
249 }
250 
251 /// Returns the visible portion of rect at the given origin and size
252 pub fn visible_rect(&self, origin: Point, size: Vector2F) -> Option<RectF> {
253 self.scene
254 .as_ref()
255 .and_then(|scene| scene.visible_rect(origin, size))
256 }
257 
258 /// Returns the position of an element that has been saved via the SavePosition
259 /// element type
260 pub fn element_position_by_id<S>(&self, position_id: S) -> Option<RectF>
261 where
262 S: AsRef<str>,
263 {
264 self.position_cache.get_position(position_id)
265 }
266 
267 /// Returns an iterator of `DropTargetPosition`s. Used to determine if a draggable element
268 /// was dropped on a `DropTarget`.
269 pub(crate) fn drop_target_data(&self) -> impl Iterator<Item = DropTargetPosition> + 'a {
270 self.position_cache.drop_target_data()
271 }
272}
273 
274#[derive(Copy, Clone, Debug)]
275pub struct SizeConstraint {
276 pub min: Vector2F,
277 pub max: Vector2F,
278}
279 
280pub struct ChildView<T> {
281 view_id: EntityId,
282 size: Option<Vector2F>,
283 origin: Option<Point>,
284 phantom_data: PhantomData<T>,
285}
286 
287pub struct DispatchedAction {
288 pub view_id: EntityId,
289 pub kind: DispatchedActionKind,
290}
291 
292/// Temporary Enum to support both Legacy and Typed actions at the same time
293///
294/// Will be removed when all views have been converted to Typed actions
295pub enum DispatchedActionKind {
296 Legacy {
297 name: &'static str,
298 arg: Box<dyn Any>,
299 },
300 Typed(Box<dyn Action>),
301}
302 
303impl Presenter {
304 pub fn new(window_id: WindowId) -> Self {
305 Self {
306 frame_count: 0,
307 window_id,
308 rendered_views: HashMap::new(),
309 parents: HashMap::new(),
310 scene: None,
311 text_layout_cache: LayoutCache::new(),
312 position_cache: PositionCache::default(),
313 highlighted_view: None,
314 }
315 }
316 
317 pub fn invalidate(&mut self, invalidation: WindowInvalidation, app: &AppContext) {
318 // Don't try to update views that were also removed
319 for &view_id in invalidation.updated.difference(&invalidation.removed) {
320 match app.render_view(self.window_id, view_id) {
321 Ok(element) => {
322 self.rendered_views.insert(view_id, element);
323 }
324 Err(e) => log::warn!("View was not rendered, error: {e:?}"),
325 };
326 }
327 for view_id in invalidation.removed {
328 self.rendered_views.remove(&view_id);
329 self.parents.remove(&view_id);
330 }
331 }
332 
333 pub fn build_scene(
334 &mut self,
335 window_size: Vector2F,
336 scale_factor: f32,
337 max_texture_dimension_2d: Option<u32>,
338 ctx: &mut AppContext,
339 ) -> Rc<Scene> {
340 self.position_cache.clear_single_frame_positions();
341 
342 // Scale the window size by the zoom factor. We implement zoom by faking a window size that
343 // is proportionally smaller based on the current zoom factor. Once we build up a scene
344 // with the fake window bounds, we then adjust the scale factor to include the zoom level
345 // so every item in the scene is blown up to fit in the actual window bounds.
346 let zoomed_window_size = window_size.scale_down(ctx.zoom_factor());
347 let zoomed_scale_factor = scale_factor.scale_up(ctx.zoom_factor());
348 
349 self.layout(zoomed_window_size, ctx);
350 // In theory, after_layout would be a good place for Elements to update app state with the
351 // results of layout (for example, if a View stored the heights of its children to
352 // implement scrolling). However, it's not safe to pass a AppContext to after_layout
353 // because the presenter is mutably borrowed. Doing so can cause crashes like CORE-1544.
354 // In the future, we might:
355 // * Decouple after_layout from the presenter so it can take a AppContext
356 // * Extend the AfterLayoutContext API to allow state updates, but not other effects
357 self.after_layout(ctx);
358 let (scene, repaint_at, pending_assets) = self.paint(
359 zoomed_scale_factor,
360 zoomed_window_size,
361 max_texture_dimension_2d,
362 ctx,
363 );
364 // After paint, collect a delayed repaint if it exists and start the timer.
365 if let Some(repaint_at) = repaint_at {
366 ctx.manage_delayed_repaint_timers(self.window_id, repaint_at);
367 }
368 ctx.manage_pending_assets(self.window_id, pending_assets);
369 let scene = Rc::new(scene);
370 self.scene = Some(scene.clone());
371 self.text_layout_cache.finish_frame();
372 self.frame_count += 1;
373 ctx.load_requested_fallback_families(self.window_id);
374 scene
375 }
376 
377 fn layout(&mut self, window_size: Vector2F, app: &AppContext) {
378 if let Some(root_view_id) = app.root_view_id(self.window_id) {
379 let mut layout_ctx = LayoutContext {
380 rendered_views: &mut self.rendered_views,
381 parents: &mut self.parents,
382 text_layout_cache: &self.text_layout_cache,
383 view_stack: Vec::new(),
384 window_size,
385 position_cache: &self.position_cache,
386 };
387 layout_ctx.layout(
388 root_view_id,
389 SizeConstraint::new(Vector2F::zero(), window_size),
390 app,
391 );
392 }
393 }
394 
395 fn after_layout(&mut self, app: &AppContext) {
396 if let Some(root_view_id) = app.root_view_id(self.window_id) {
397 let mut ctx = AfterLayoutContext {
398 rendered_views: &mut self.rendered_views,
399 text_layout_cache: &self.text_layout_cache,
400 };
401 ctx.after_layout(root_view_id, app);
402 }
403 }
404 
405 fn paint(
406 &mut self,
407 scale_factor: f32,
408 window_size: Vector2F,
409 max_texture_dimension_2d: Option<u32>,
410 ctx: &mut AppContext,
411 ) -> (Scene, Option<Instant>, HashSet<AssetHandle>) {
412 let mut scene = Scene::new(scale_factor, ctx.rendering_config());
413 let mut repaint_at = None;
414 let mut pending_assets = HashSet::new();
415 
416 if let Some(root_view_id) = ctx.root_view_id(self.window_id) {
417 let mut paint_ctx = PaintContext {
418 font_cache: ctx.font_cache(),
419 text_layout_cache: &self.text_layout_cache,
420 rendered_views: &mut self.rendered_views,
421 position_cache: &mut self.position_cache,
422 scene: &mut scene,
423 window_size,
424 max_texture_dimension_2d,
425 highlighted_view: self.highlighted_view,
426 current_selection: None,
427 repaint_at: None,
428 pending_assets: HashSet::new(),
429 views_painted: HashSet::new(),
430 };
431 paint_ctx.paint(root_view_id, Vector2F::zero(), ctx);
432 
433 repaint_at = paint_ctx.repaint_at;
434 pending_assets.extend(paint_ctx.pending_assets);
435 
436 // If the cursor shape had been changed by a view and that view is no longer being
437 // rendered, reset the cursor.
438 if let Some((window_id, view_id)) = ctx.cursor_updated_for_view {
439 if self.window_id == window_id && !paint_ctx.views_painted.contains(&view_id) {
440 ctx.reset_cursor();
441 }
442 }
443 }
444 
445 // If there is a highlighted view, draw a box over the entire scene with
446 // the same bounds as the highlighted view. This ensures that views
447 // which are fully covered by a child view can still be highlighted.
448 if let Some(view_id) = self.highlighted_view.as_ref() {
449 if let Some(view) = self.rendered_views.get(view_id) {
450 if let Some(bounds) = view.bounds() {
451 scene.start_overlay_layer(ClipBounds::None);
452 scene.draw_rect_with_hit_recording(bounds).with_border(
453 crate::elements::Border::all(2.)
454 // Use a semi-transparent color so that overlapping
455 // content can still be seen through the border.
456 .with_border_color(pathfinder_color::ColorU::new(0, 255, 255, 128)),
457 );
458 scene.stop_layer();
459 }
460 }
461 }
462 
463 (scene, repaint_at, pending_assets)
464 }
465 
466 pub fn ancestors(&self, mut view_id: EntityId) -> Vec<EntityId> {
467 let mut chain = vec![view_id];
468 while let Some(parent_id) = self.parents.get(&view_id) {
469 view_id = *parent_id;
470 chain.push(view_id);
471 }
472 chain.reverse();
473 chain
474 }
475 
476 /// Returns all descendant view IDs of the given root view.
477 /// This is computed by finding all views whose ancestor chain includes the root.
478 pub fn descendants(&self, root_view_id: EntityId) -> Vec<EntityId> {
479 self.parents
480 .keys()
481 .filter(|&&view_id| {
482 let mut current = view_id;
483 while let Some(&parent_id) = self.parents.get(&current) {
484 if parent_id == root_view_id {
485 return true;
486 }
487 current = parent_id;
488 }
489 false
490 })
491 .copied()
492 .collect()
493 }
494 
495 fn create_event_context<'a>(&'a mut self, font_cache: &'a fonts::Cache) -> EventContext<'a> {
496 EventContext {
497 scene: self.scene.clone(),
498 rendered_views: &mut self.rendered_views,
499 position_cache: &self.position_cache,
500 actions: Default::default(),
501 font_cache,
502 text_layout_cache: &self.text_layout_cache,
503 view_stack: Default::default(),
504 notified: Default::default(),
505 notify_timers_to_set: Default::default(),
506 notify_timers_to_clear: Default::default(),
507 cursor_update: Default::default(),
508 soft_keyboard_requested: false,
509 }
510 }
511 
512 #[cfg(test)]
513 pub fn mock_event_context<'a>(&'a mut self, font_cache: &'a fonts::Cache) -> EventContext<'a> {
514 self.create_event_context(font_cache)
515 }
516 
517 pub fn dispatch_event(&mut self, event: Event, app: &AppContext) -> DispatchResult {
518 // Translate all events to be in the coordinate space after factoring in the
519 // zoom factor.
520 let event = event.scale_down(app.zoom_factor());
521 let window_id = self.window_id;
522 let mut event_ctx = self.create_event_context(app.font_cache());
523 let handled = app.root_view_id(window_id).is_some_and(|root_view_id| {
524 event_ctx.dispatch_event_on_view(root_view_id, &DispatchedEvent::from(event), app)
525 });
526 
527 DispatchResult {
528 handled,
529 actions: event_ctx.actions,
530 notified: event_ctx.notified,
531 notify_timers_to_set: event_ctx.notify_timers_to_set,
532 notify_timers_to_clear: event_ctx.notify_timers_to_clear,
533 cursor_update: event_ctx.cursor_update,
534 soft_keyboard_requested: event_ctx.soft_keyboard_requested,
535 }
536 }
537 
538 pub fn scene(&self) -> Option<&Rc<Scene>> {
539 self.scene.as_ref()
540 }
541 
542 pub fn position_cache(&self) -> &PositionCache {
543 &self.position_cache
544 }
545 
546 #[cfg(any(test, feature = "test-util"))]
547 pub fn position_cache_mut(&mut self) -> &mut PositionCache {
548 &mut self.position_cache
549 }
550 
551 pub fn frame_count(&self) -> usize {
552 self.frame_count
553 }
554 
555 pub(crate) fn parents(&self) -> HashMap<EntityId, EntityId> {
556 self.parents.clone()
557 }
558 
559 pub fn set_highlighted_view(&mut self, view_id: EntityId) {
560 self.highlighted_view = Some(view_id);
561 }
562 
563 pub fn clear_highlighted_view(&mut self) {
564 self.highlighted_view = None;
565 }
566 
567 pub fn text_layout_cache(&self) -> &LayoutCache {
568 &self.text_layout_cache
569 }
570 
571 /// Set the parent of a view.
572 /// This will be overwritten on the next layout pass, but is useful before the initial layout
573 /// of a view.
574 pub(crate) fn set_parent(&mut self, view_id: EntityId, parent_id: EntityId) {
575 self.parents.insert(view_id, parent_id);
576 }
577}
578 
579impl LayoutContext<'_> {
580 fn layout(
581 &mut self,
582 view_id: EntityId,
583 constraint: SizeConstraint,
584 app: &AppContext,
585 ) -> Vector2F {
586 let Some(mut rendered_view) = self.rendered_views.remove(&view_id) else {
587 return vec2f(0., 0.);
588 };
589 
590 if let Some(parent_id) = self.view_stack.last() {
591 self.parents.insert(view_id, *parent_id);
592 }
593 self.view_stack.push(view_id);
594 let size = rendered_view.layout(constraint, self, app);
595 self.rendered_views.insert(view_id, rendered_view);
596 self.view_stack.pop();
597 size
598 }
599}
600 
601impl AfterLayoutContext<'_> {
602 fn after_layout(&mut self, view_id: EntityId, app: &AppContext) {
603 if let Some(mut view) = self.rendered_views.remove(&view_id) {
604 view.after_layout(self, app);
605 self.rendered_views.insert(view_id, view);
606 }
607 }
608}
609 
610impl PaintContext<'_> {
611 fn paint(&mut self, view_id: EntityId, origin: Vector2F, app: &AppContext) {
612 if let Some(mut tree) = self.rendered_views.remove(&view_id) {
613 // If this is the highlighted view, draw a debug rectangle with the
614 // same bounds as the view.
615 if self.highlighted_view == Some(view_id) {
616 if let Some(size) = tree.size() {
617 self.scene
618 .draw_rect_with_hit_recording(RectF::new(origin, size))
619 .with_border(
620 crate::elements::Border::all(2.)
621 .with_border_color(pathfinder_color::ColorU::new(0, 255, 255, 255)),
622 );
623 }
624 }
625 self.views_painted.insert(view_id);
626 tree.paint(origin, self, app);
627 self.rendered_views.insert(view_id, tree);
628 }
629 }
630 
631 /// Notifies the window it needs a repaint after a certain duration.
632 pub fn repaint_after(&mut self, delay: Duration) {
633 let start_time = Instant::now();
634 let new_repaint_at = start_time + delay;
635 
636 // We want the repaint timer with the nearest repaint time.
637 if self
638 .repaint_at
639 .is_some_and(|repaint_at| repaint_at <= new_repaint_at)
640 {
641 return;
642 }
643 self.repaint_at(new_repaint_at);
644 }
645 
646 /// Notifies the window it needs a repaint at a certain Instant.
647 /// If there's an existing repaint_at time, keeps the earlier time.
648 pub fn repaint_at(&mut self, new_repaint_at: Instant) {
649 // We want the repaint timer with the nearest repaint time.
650 if self
651 .repaint_at
652 .is_some_and(|repaint_at| repaint_at <= new_repaint_at)
653 {
654 return;
655 }
656 self.repaint_at = Some(new_repaint_at);
657 }
658 
659 pub fn repaint_after_load(&mut self, asset: AssetHandle) {
660 self.pending_assets.insert(asset);
661 }
662}
663 
664impl EventContext<'_> {
665 pub fn dispatch_event_on_view(
666 &mut self,
667 view_id: EntityId,
668 event: &DispatchedEvent,
669 app: &AppContext,
670 ) -> bool {
671 if let Some(mut element) = self.rendered_views.remove(&view_id) {
672 self.view_stack.push(view_id);
673 let handled = element.dispatch_event(event, self, app);
674 self.rendered_views.insert(view_id, element);
675 self.view_stack.pop();
676 handled
677 } else {
678 false
679 }
680 }
681 
682 pub fn dispatch_action<A: 'static + Any>(&mut self, name: &'static str, arg: A) {
683 self.actions.push(DispatchedAction {
684 view_id: *self.view_stack.last().unwrap(),
685 kind: DispatchedActionKind::Legacy {
686 name,
687 arg: Box::new(arg),
688 },
689 });
690 }
691 
692 pub fn dispatch_typed_action<A: Action>(&mut self, action: A) {
693 self.actions.push(DispatchedAction {
694 view_id: *self.view_stack.last().unwrap(),
695 kind: DispatchedActionKind::Typed(Box::new(action)),
696 });
697 }
698 
699 pub fn notify(&mut self) {
700 self.notified.insert(*self.view_stack.last().unwrap());
701 }
702 
703 /// Notifies the view it needs a redraw after a certain duration and returns
704 /// a timer_id and end_time associated with the notify
705 pub fn notify_after(&mut self, delay: Duration) -> (TaskId, Instant) {
706 let timer_id = TaskId::new();
707 let start_time = Instant::now();
708 let notify_at = start_time + delay;
709 self.notify_timers_to_set.insert(
710 timer_id,
711 ViewToNotify {
712 view_id: *self
713 .view_stack
714 .last()
715 .expect("last view id should be defined"),
716 notify_at,
717 },
718 );
719 (timer_id, notify_at)
720 }
721 
722 /// Clears the given notify timer
723 pub fn clear_notify_timer(&mut self, timer_id: TaskId) {
724 self.notify_timers_to_clear.insert(timer_id);
725 }
726 
727 /// Sets a cursor update. If one is already set, then only
728 /// reset if this one is at a higher z-index.
729 pub fn set_cursor(&mut self, cursor: Cursor, at_z_index: ZIndex) {
730 match self.cursor_update {
731 // Don't override cursor if the current z_index is higher.
732 Some(CursorUpdate::Set { z_index, .. }) if z_index > at_z_index => (),
733 _ => {
734 self.cursor_update = Some(CursorUpdate::Set {
735 cursor,
736 z_index: at_z_index,
737 view_id: *self
738 .view_stack
739 .last()
740 .expect("view stack cannot be empty when dispatching event"),
741 })
742 }
743 };
744 }
745 
746 /// Resets the cursor if a new one is not already set as part of
747 /// this dispatch
748 pub fn reset_cursor(&mut self) {
749 if self.cursor_update.is_none() {
750 self.cursor_update = Some(CursorUpdate::Reset);
751 }
752 }
753 
754 /// Request that the soft keyboard be shown on mobile devices.
755 /// This is used on mobile WASM to trigger the keyboard when a text input area is tapped.
756 pub fn request_soft_keyboard(&mut self) {
757 self.soft_keyboard_requested = true;
758 }
759}
760 
761impl SizeConstraint {
762 pub fn new(min: Vector2F, max: Vector2F) -> Self {
763 Self { min, max }
764 }
765 
766 pub fn strict(size: Vector2F) -> Self {
767 Self {
768 min: size,
769 max: size,
770 }
771 }
772 
773 /// Computes constraints for child elements of a Flex where children should
774 /// be tightly bound to the parent constraints along the cross axis, but
775 /// are unbounded along the main axis.
776 pub fn tight_on_cross_axis(main_axis: Axis, parent_constraint: SizeConstraint) -> Self {
777 match main_axis {
778 Axis::Horizontal => Self {
779 min: vec2f(0.0, parent_constraint.max.y()),
780 max: vec2f(f32::INFINITY, parent_constraint.max.y()),
781 },
782 Axis::Vertical => Self {
783 min: vec2f(parent_constraint.max.x(), 0.),
784 max: vec2f(parent_constraint.max.x(), f32::INFINITY),
785 },
786 }
787 }
788 
789 /// For the child elements of the Flex, we want unbounded constraint on the
790 /// axis which Flex is expanding upon (horizontally for rows and vertically for columns)
791 /// and the same max constraint as the parent on the axis which Flex is constrained on.
792 /// We don't set any min cross-axis constraint for the child - it is allowed to be
793 /// smaller than the flex parent.
794 pub fn child_constraint_along_axis(axis: Axis, parent_constraint: SizeConstraint) -> Self {
795 let (_, max) = parent_constraint.constraint_for_axis(axis.invert());
796 match axis {
797 Axis::Horizontal => Self {
798 min: vec2f(0.0, 0.0),
799 max: vec2f(f32::INFINITY, max),
800 },
801 Axis::Vertical => Self {
802 min: vec2f(0.0, 0.0),
803 max: vec2f(max, f32::INFINITY),
804 },
805 }
806 }
807 
808 /// Apply this size constraint to a Vector2f that represents a size.
809 pub fn apply(&self, size: Vector2F) -> Vector2F {
810 size.clamp(self.min, self.max)
811 }
812 
813 pub fn max_along(&self, axis: Axis) -> f32 {
814 match axis {
815 Axis::Horizontal => self.max.x(),
816 Axis::Vertical => self.max.y(),
817 }
818 }
819 
820 /// Returns a min, max pair along the given axis..
821 pub fn constraint_for_axis(&self, axis: Axis) -> (f32, f32) {
822 match axis {
823 Axis::Horizontal => (self.min.x(), self.max.x()),
824 Axis::Vertical => (self.min.y(), self.max.y()),
825 }
826 }
827}
828 
829use super::Element;
830use crate::geometry::rect::RectF;
831 
832impl<T: View> ChildView<T> {
833 pub fn new(handle: &ViewHandle<T>) -> Self {
834 Self::with_id(handle.id())
835 }
836 
837 pub fn with_id(view_id: EntityId) -> Self {
838 Self {
839 view_id,
840 size: None,
841 origin: None,
842 phantom_data: Default::default(),
843 }
844 }
845}
846 
847impl<T> Element for ChildView<T> {
848 fn layout(
849 &mut self,
850 constraint: SizeConstraint,
851 ctx: &mut LayoutContext,
852 app: &AppContext,
853 ) -> Vector2F {
854 let size = ctx.layout(self.view_id, constraint, app);
855 self.size = Some(size);
856 size
857 }
858 
859 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
860 ctx.after_layout(self.view_id, app);
861 }
862 
863 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
864 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
865 ctx.paint(self.view_id, origin, app);
866 }
867 
868 fn dispatch_event(
869 &mut self,
870 event: &DispatchedEvent,
871 ctx: &mut EventContext,
872 app: &AppContext,
873 ) -> bool {
874 ctx.dispatch_event_on_view(self.view_id, event, app)
875 }
876 
877 fn size(&self) -> Option<Vector2F> {
878 self.size
879 }
880 
881 fn origin(&self) -> Option<Point> {
882 self.origin
883 }
884}
885 
886#[cfg(test)]
887#[path = "presenter_tests.rs"]
888mod tests;
889