StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use super::{elements::Axis, Event}; |
| 2 | use crate::assets::asset_cache::AssetHandle; |
| 3 | use crate::elements::{DropTargetPosition, Selection}; |
| 4 | |
| 5 | use crate::fonts; |
| 6 | use crate::zoom::Scale; |
| 7 | use 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 | }; |
| 17 | use instant::Instant; |
| 18 | use pathfinder_geometry::vector::{vec2f, Vector2F}; |
| 19 | use std::{ |
| 20 | any::Any, |
| 21 | collections::{HashMap, HashSet}, |
| 22 | marker::PhantomData, |
| 23 | rc::Rc, |
| 24 | time::Duration, |
| 25 | }; |
| 26 | |
| 27 | pub 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 | |
| 39 | pub 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 | |
| 48 | pub struct AfterLayoutContext<'a> { |
| 49 | rendered_views: &'a mut HashMap<EntityId, Box<dyn Element>>, |
| 50 | pub text_layout_cache: &'a LayoutCache, |
| 51 | } |
| 52 | |
| 53 | pub 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)] |
| 73 | pub 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)] |
| 98 | pub 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)] |
| 107 | pub 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)] |
| 127 | pub 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 | |
| 141 | impl 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 | |
| 220 | pub 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 | |
| 243 | impl<'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)] |
| 275 | pub struct SizeConstraint { |
| 276 | pub min: Vector2F, |
| 277 | pub max: Vector2F, |
| 278 | } |
| 279 | |
| 280 | pub struct ChildView<T> { |
| 281 | view_id: EntityId, |
| 282 | size: Option<Vector2F>, |
| 283 | origin: Option<Point>, |
| 284 | phantom_data: PhantomData<T>, |
| 285 | } |
| 286 | |
| 287 | pub 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 |
| 295 | pub enum DispatchedActionKind { |
| 296 | Legacy { |
| 297 | name: &'static str, |
| 298 | arg: Box<dyn Any>, |
| 299 | }, |
| 300 | Typed(Box<dyn Action>), |
| 301 | } |
| 302 | |
| 303 | impl 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(¤t) { |
| 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 | |
| 579 | impl 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 | |
| 601 | impl 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 | |
| 610 | impl 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 | |
| 664 | impl 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 | |
| 761 | impl 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 | |
| 829 | use super::Element; |
| 830 | use crate::geometry::rect::RectF; |
| 831 | |
| 832 | impl<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 | |
| 847 | impl<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"] |
| 888 | mod tests; |
| 889 |