1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4use std::rc::Rc;
5
6use ravenrs_core::{
7 BuildCause, BuildCauses, Color, CursorIcon, Effect, Event, FrameWork, Key, Modifiers, PhaseTimes, Point,
8 PointerButton, Primitive, Quad, Rect, Scene, Size, ViewCost,
9};
10use ravenrs_layout::{Align, Axis, LayoutStyle, LayoutTree};
11use ravenrs_reactive::{Observer, State};
12use ravenrs_style::{Semantic, Theme};
13use ravenrs_text::TextSystem;
14
15use crate::View;
16use crate::context::{
17 BuildCx, Drag, DragPhase, Element, Interaction, LocalStore, NodeId, Overlay, Paint, PaintCx, Placement,
18 QuadPaint, ScrollState, Shortcut, TextLeaf, WidgetId,
19};
20use crate::motion::{GhostLayer, MotionEntry, MotionSystem, NodePaint, Sampled, flipped};
21
22mod access;
23
24type Waker = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
25
26const SCROLLBAR_WIDTH: f32 = 4.0;
29const SCROLLBAR_HOLD: f64 = 0.9;
31const SCROLLBAR_FADE: f64 = 0.25;
32const SCROLLBAR_ALPHA: f32 = 0.45;
33
34const MAX_FRAME_GAP: f64 = 0.25;
36
37const STALL_STEP: f64 = 0.05;
40
41const LIVE_FRAME: f64 = 1.0 / 60.0;
43
44const PAINT_MARGIN: f32 = 64.0;
47
48fn touches(frame: Rect, clip: Rect) -> bool {
50 frame.max_x() + PAINT_MARGIN > clip.x()
51 && frame.x() - PAINT_MARGIN < clip.max_x()
52 && frame.max_y() + PAINT_MARGIN > clip.y()
53 && frame.y() - PAINT_MARGIN < clip.max_y()
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct UiStats {
61 pub builds: u64,
63 pub layouts: u64,
65 pub placements: u64,
67 pub paints: u64,
69 pub shapes: u64,
71 pub causes: BuildCauses,
73}
74
75pub(crate) struct ViewProfile {
77 clock: Rc<dyn Fn() -> f64>,
78 costs: HashMap<&'static str, ViewCost>,
79 open: Vec<OpenView>,
80}
81
82struct OpenView {
83 name: &'static str,
84 started: f64,
85 elements: usize,
86 shapes: u64,
87 inner_time: f64,
89 inner_elements: usize,
90 inner_shapes: u64,
91}
92
93#[cfg_attr(feature = "live", ravenrs_live::expose)]
94impl ViewProfile {
95 pub(crate) fn enter(&mut self, name: &'static str, elements: usize, shapes: u64) {
96 let started = (self.clock)();
97 self.open.push(OpenView {
98 name,
99 started,
100 elements,
101 shapes,
102 inner_time: 0.0,
103 inner_elements: 0,
104 inner_shapes: 0,
105 });
106 }
107
108 pub(crate) fn leave(&mut self, elements: usize, shapes: u64) {
109 let Some(view) = self.open.pop() else { return };
110 let total = ((self.clock)() - view.started).max(0.0);
111 let made = elements.saturating_sub(view.elements);
112 let shaped = shapes.saturating_sub(view.shapes);
113 if let Some(outer) = self.open.last_mut() {
114 outer.inner_time += total;
115 outer.inner_elements += made;
116 outer.inner_shapes += shaped;
117 }
118 let cost = self
119 .costs
120 .entry(view.name)
121 .or_insert_with(|| ViewCost { name: view.name, ..ViewCost::default() });
122 cost.count += 1;
123 cost.time += std::time::Duration::from_secs_f64((total - view.inner_time).max(0.0));
124 cost.elements += made.saturating_sub(view.inner_elements) as u32;
125 cost.shapes += shaped.saturating_sub(view.inner_shapes) as u32;
126 }
127}
128
129#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
136pub struct Ui {
137 root: Box<dyn View>,
138 theme: State<Theme>,
139 text: TextSystem,
140 layout: LayoutTree<TextLeaf>,
141 elements: Vec<Element>,
142 root_node: Option<NodeId>,
143 overlays: Vec<Overlay>,
144 modal_return: Option<Option<WidgetId>>,
146 frames: Vec<Rect>,
148 clips: Vec<Rect>,
150 content: Vec<Size>,
152 paint_order: Vec<NodeId>,
154 scroll: ScrollState,
155 modifiers: Rc<Cell<Modifiers>>,
156 parents: Vec<Option<NodeId>>,
158 hover_chain: Vec<WidgetId>,
160 local: LocalStore,
161 observer: Observer,
162 state_dirty: Rc<Cell<bool>>,
163 ui_waker: crate::UiWaker,
164 waker: Waker,
165 needs_build: bool,
166 viewport_moved: bool,
168 viewport: Size,
169 scale: f32,
170 interaction: Interaction,
171 reveal_focus: bool,
174 dragging: Option<WidgetId>,
175 scrolled_at: HashMap<WidgetId, f64>,
177 scrollbar_showing: bool,
179 time: f64,
181 shortcuts: Vec<Shortcut>,
182 activity: Option<crate::context::ActivityObserver>,
183 access_tree_asked: bool,
184 wake: Option<f64>,
186 pointer: Option<Point>,
187 cursor: CursorIcon,
188 scene: Scene,
189 access_map: access::AccessMap,
190 clipboard: crate::Clipboard,
191 sound: crate::Sound,
192 links: crate::Links,
193 full_screen: crate::FullScreen,
194 glide: Option<(WidgetId, f32, f32, f64)>,
196 appeared: std::collections::HashSet<WidgetId>,
198 file_picker: crate::FilePicker,
199 files_over: State<bool>,
200 motion: MotionSystem,
201 motions: Vec<MotionEntry>,
202 shifts: Vec<Point>,
204 views: Vec<Rect>,
207 painted_palette: Option<ravenrs_style::Palette>,
209 crossfade: bool,
211 snap_layout: bool,
213 system_reduced_motion: bool,
215 next_live: Option<f64>,
217 has_live: bool,
219 value_moving: bool,
221 painting_overlays: bool,
222 stats: UiStats,
223 phase_clock: Option<Rc<dyn Fn() -> f64>>,
225 phases: PhaseTimes,
226 build_cause: Option<BuildCause>,
228 frame: FrameWork,
229 frame_start: (PhaseTimes, u64),
231 view_profile: Option<Rc<RefCell<ViewProfile>>>,
232 hover_readers: Rc<RefCell<std::collections::HashSet<WidgetId>>>,
233}
234
235impl fmt::Debug for Ui {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.debug_struct("Ui")
238 .field("elements", &self.elements.len())
239 .field("viewport", &self.viewport)
240 .finish_non_exhaustive()
241 }
242}
243
244#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
245impl Ui {
246 pub fn new(root: impl View + 'static, theme: Theme) -> Self {
248 let state_dirty = Rc::new(Cell::new(true));
249 let waker: Waker = Rc::new(RefCell::new(None));
250 let (dirty, wake) = (state_dirty.clone(), waker.clone());
251 let observer = Observer::new(move || {
252 dirty.set(true);
253 let callback = wake.borrow().clone();
254 if let Some(callback) = callback {
255 callback();
256 }
257 });
258 Self {
259 root: Box::new(root),
260 theme: State::new(theme),
261 text: TextSystem::new(),
262 layout: LayoutTree::new(),
263 elements: Vec::new(),
264 root_node: None,
265 overlays: Vec::new(),
266 modal_return: None,
267 frames: Vec::new(),
268 clips: Vec::new(),
269 content: Vec::new(),
270 paint_order: Vec::new(),
271 scroll: ScrollState::default(),
272 modifiers: Rc::new(Cell::new(Modifiers::default())),
273 parents: Vec::new(),
274 hover_chain: Vec::new(),
275 local: LocalStore::default(),
276 observer,
277 state_dirty,
278 ui_waker: crate::UiWaker::default(),
279 waker,
280 needs_build: true,
281 viewport_moved: false,
282 viewport: Size::new(800.0, 600.0),
283 scale: 1.0,
284 interaction: Interaction::default(),
285 reveal_focus: false,
286 dragging: None,
287 scrolled_at: HashMap::new(),
288 scrollbar_showing: false,
289 time: 0.0,
290 shortcuts: Vec::new(),
291 activity: None,
292 access_tree_asked: false,
293 wake: None,
294 pointer: None,
295 cursor: CursorIcon::Default,
296 scene: Scene::new(),
297 access_map: access::AccessMap::default(),
298 clipboard: crate::Clipboard::default(),
299 sound: crate::Sound::default(),
300 links: crate::Links::default(),
301 full_screen: crate::FullScreen::default(),
302 glide: None,
303 appeared: std::collections::HashSet::new(),
304 file_picker: crate::FilePicker::default(),
305 files_over: State::new(false),
306 motion: MotionSystem::default(),
307 motions: Vec::new(),
308 shifts: Vec::new(),
309 views: Vec::new(),
310 painted_palette: None,
311 crossfade: false,
312 snap_layout: false,
313 system_reduced_motion: false,
314 next_live: None,
315 has_live: false,
316 value_moving: false,
317 painting_overlays: false,
318 stats: UiStats::default(),
319 phase_clock: None,
320 phases: PhaseTimes::default(),
321 build_cause: Some(BuildCause::First),
322 frame: FrameWork::default(),
323 frame_start: (PhaseTimes::default(), 0),
324 view_profile: None,
325 hover_readers: Rc::default(),
326 }
327 }
328
329 fn ask_build(&mut self, cause: BuildCause) {
330 self.needs_build = true;
331 self.build_cause.get_or_insert(cause);
332 }
333
334 fn take_build_cause(&mut self) -> BuildCause {
336 if let Some(cause) = self.build_cause.take() {
337 cause
338 } else if self.state_dirty.get() {
339 BuildCause::Data
340 } else if self.ui_waker.is_dirty() {
341 BuildCause::Remote
342 } else {
343 BuildCause::Clock
344 }
345 }
346
347 pub fn take_frame_work(&mut self) -> FrameWork {
349 let shapes = self.text.stats().shapes;
350 let (phases, shapes_before) = self.frame_start;
351 let now = self.phases;
352 let mut work = std::mem::take(&mut self.frame);
353 work.phases = PhaseTimes {
354 build: now.build - phases.build,
355 layout: now.layout - phases.layout,
356 place: now.place - phases.place,
357 paint: now.paint - phases.paint,
358 };
359 work.shapes = (shapes - shapes_before) as u32;
360 work.elements = self.elements.len() as u32;
361 self.frame_start = (now, shapes);
362 work
363 }
364
365 pub fn set_view_profiling(&mut self, on: bool) {
368 self.view_profile = match (&self.phase_clock, on) {
369 (Some(clock), true) => Some(self.view_profile.take().unwrap_or_else(|| {
370 Rc::new(RefCell::new(ViewProfile {
371 clock: clock.clone(),
372 costs: HashMap::new(),
373 open: Vec::new(),
374 }))
375 })),
376 _ => None,
377 };
378 }
379
380 pub fn view_costs(&self) -> Vec<ViewCost> {
383 let Some(profile) = &self.view_profile else { return Vec::new() };
384 let mut costs: Vec<ViewCost> = profile.borrow().costs.values().cloned().collect();
385 costs.sort_by(|a, b| b.time.cmp(&a.time).then(a.name.cmp(b.name)));
386 costs
387 }
388
389 pub fn stats(&self) -> UiStats {
391 UiStats { shapes: self.text.stats().shapes, ..self.stats }
392 }
393
394 pub fn set_phase_clock(&mut self, clock: Option<Rc<dyn Fn() -> f64>>) {
398 self.phase_clock = clock;
399 }
400
401 pub fn phase_times(&self) -> PhaseTimes {
403 self.phases
404 }
405
406 fn phase_start(&self) -> Option<f64> {
407 self.phase_clock.as_ref().map(|clock| clock())
408 }
409
410 fn phase_elapsed(&self, started: Option<f64>) -> std::time::Duration {
411 match (started, &self.phase_clock) {
412 (Some(started), Some(clock)) => std::time::Duration::from_secs_f64((clock() - started).max(0.0)),
413 _ => std::time::Duration::ZERO,
414 }
415 }
416
417 pub fn set_clipboard(&mut self, provider: impl crate::ClipboardProvider + 'static) {
420 self.clipboard = crate::Clipboard::new(provider);
421 self.ask_build(BuildCause::System);
422 }
423
424 pub fn clipboard(&self) -> crate::Clipboard {
426 self.clipboard.clone()
427 }
428
429 pub fn set_links(&mut self, provider: impl crate::LinkOpener + 'static) {
432 self.links.set_opener(provider);
433 }
434
435 pub fn links(&self) -> crate::Links {
437 self.links.clone()
438 }
439
440 pub fn set_full_screen(&mut self, provider: impl crate::FullScreenProvider + 'static) {
443 self.full_screen = crate::FullScreen::new(provider);
444 self.ask_build(BuildCause::System);
445 }
446
447 pub fn full_screen(&self) -> crate::FullScreen {
449 self.full_screen.clone()
450 }
451
452 pub fn set_sound(&mut self, provider: impl crate::SoundProvider + 'static) {
454 self.sound = crate::Sound::new(provider);
455 self.ask_build(BuildCause::System);
456 }
457
458 pub fn sound(&self) -> crate::Sound {
460 self.sound.clone()
461 }
462
463 pub fn set_file_picker(&mut self, provider: impl crate::FilePickerProvider + 'static) {
466 self.file_picker = crate::FilePicker::new(provider);
467 self.ask_build(BuildCause::System);
468 }
469
470 pub fn set_files_over(&mut self, over: bool) {
472 self.files_over.set_if_changed(over);
473 }
474
475 pub fn drop_files(&mut self, position: Option<Point>, files: Vec<crate::PickedFile>) -> bool {
479 self.files_over.set_if_changed(false);
480 let mut node = position.and_then(|p| self.hit_test(p));
481 while let Some(n) = node {
482 if let Some(handler) = self.elements[n.0 as usize].on_file_drop.clone() {
483 handler(files);
484 return true;
485 }
486 node = self.parents.get(n.0 as usize).copied().flatten();
487 }
488 let mut places = self.elements.iter().filter_map(|e| e.on_file_drop.clone());
489 match (places.next(), places.next()) {
490 (Some(only), None) => {
491 only(files);
492 true
493 }
494 _ => false,
495 }
496 }
497
498 pub fn set_waker(&mut self, waker: impl Fn() + 'static) {
500 *self.waker.borrow_mut() = Some(Rc::new(waker));
501 }
502
503 pub fn ui_waker(&self) -> crate::UiWaker {
505 self.ui_waker.clone()
506 }
507
508 pub fn set_remote_waker(&mut self, notify: impl Fn() + Send + Sync + 'static) {
511 self.ui_waker.set_notify(Box::new(notify));
512 }
513
514 pub fn set_viewport(&mut self, size: Size, scale: f32) {
516 if size != self.viewport || scale != self.scale {
517 self.viewport = size;
518 self.scale = scale;
519 self.ask_build(BuildCause::Viewport);
520 self.snap_layout = true;
521 }
522 }
523
524 pub fn viewport(&self) -> Size {
526 self.viewport
527 }
528
529 pub fn scale(&self) -> f32 {
531 self.scale
532 }
533
534 pub fn theme(&self) -> Theme {
536 *self.theme.peek()
537 }
538
539 pub fn set_theme(&mut self, theme: Theme) {
541 self.theme.set(theme);
542 }
543
544 pub fn theme_state(&self) -> State<Theme> {
546 self.theme.clone()
547 }
548
549 pub fn text_system(&mut self) -> &mut TextSystem {
551 &mut self.text
552 }
553
554 pub fn needs_redraw(&self) -> bool {
556 self.needs_rebuild() || self.motion.active || self.live_is_due()
557 }
558
559 fn needs_rebuild(&self) -> bool {
560 self.needs_build
561 || self.state_dirty.get()
562 || self.ui_waker.is_dirty()
563 || self.wake.is_some_and(|w| self.time >= w)
564 }
565
566 fn live_is_due(&self) -> bool {
567 self.next_live.is_some_and(|t| self.time >= t)
568 }
569
570 pub fn is_animating(&self) -> bool {
573 self.motion.active || self.value_moving
574 }
575
576 pub fn set_time(&mut self, seconds: f64) {
584 let elapsed = seconds - self.time;
585 self.motion.clock += if (0.0..=MAX_FRAME_GAP).contains(&elapsed) { elapsed } else { STALL_STEP };
586 self.motion.enabled = true;
587 self.time = seconds;
588 }
589
590 pub fn set_reduced_motion(&mut self, reduced: bool) {
594 if self.system_reduced_motion != reduced {
595 self.system_reduced_motion = reduced;
596 self.ask_build(BuildCause::System);
597 }
598 }
599
600 pub fn next_wake(&self) -> Option<f64> {
603 match (self.wake, self.next_live) {
604 (Some(a), Some(b)) => Some(a.min(b)),
605 (a, b) => a.or(b),
606 }
607 }
608
609 pub fn is_dragging(&self) -> bool {
612 self.dragging.is_some()
613 }
614
615 pub fn cursor(&self) -> CursorIcon {
617 self.cursor
618 }
619
620 pub fn update(&mut self) -> &Scene {
622 let anchor = self.links.take_anchor();
623 if self.needs_rebuild() {
624 self.rebuild();
625 if self.refresh_hover() {
627 self.rebuild();
628 }
629 if std::mem::take(&mut self.viewport_moved) {
632 self.rebuild();
633 self.viewport_moved = false;
634 }
635 } else if self.glide.is_some() && anchor.is_none() {
636 self.advance_glide();
637 } else if self.motion.active || self.live_is_due() {
638 let _span = tracing::trace_span!("ravenrs::motion_frame").entered();
640 self.paint();
641 }
642 if let Some(name) = anchor {
643 self.start_glide(&name);
644 }
645 &self.scene
646 }
647
648 fn start_glide(&mut self, name: &str) {
651 let Some(i) = self.elements.iter().position(|e| e.anchor.as_ref().is_some_and(|(n, _)| &**n == name))
652 else {
653 tracing::debug!(name, "no anchor of that name");
654 return;
655 };
656 let offset = self.elements[i].anchor.as_ref().map_or(0.0, |a| a.1);
657 let mut node = self.parents.get(i).copied().flatten();
658 while let Some(n) = node {
659 let k = n.0 as usize;
660 if self.elements[k].scroll {
661 let current = self.clamped_offset(k);
662 let to = (current.y + self.frames[i].y() - self.frames[k].y() - offset)
663 .clamp(0.0, self.max_offset(k).y);
664 let widget = self.elements[k].widget;
665 if self.reduced_motion() {
666 self.scroll.offsets.insert(widget, Point::new(current.x, to));
667 self.place_and_paint();
668 } else {
669 self.glide = Some((widget, current.y, to, self.time));
670 self.next_live = Some(self.time);
671 }
672 return;
673 }
674 node = self.parents.get(k).copied().flatten();
675 }
676 }
677
678 fn advance_glide(&mut self) {
680 let Some((widget, from, to, start)) = self.glide else { return };
681 const GLIDE: f64 = 0.55;
682 let t = ((self.time - start) / GLIDE).clamp(0.0, 1.0) as f32;
683 let eased = ravenrs_animation::Easing::EaseInOut.sample(t);
684 let x = self.scroll.offsets.get(&widget).map_or(0.0, |o| o.x);
685 self.scroll.offsets.insert(widget, Point::new(x, from + (to - from) * eased));
686 self.scrolled_at.insert(widget, self.time);
687 if t >= 1.0 {
688 self.glide = None;
689 } else {
690 self.next_live = Some(self.time + LIVE_FRAME);
691 }
692 self.place_and_paint();
693 self.refresh_hover();
694 }
695
696 fn tell_appeared(&mut self) {
698 let mut due = Vec::new();
699 for (i, e) in self.elements.iter().enumerate() {
700 let Some(handler) = &e.on_appear else { continue };
701 if self.appeared.contains(&e.widget) {
702 continue;
703 }
704 let (frame, view) = (self.frames[i], self.views[i]);
705 if frame.max_y() > view.y()
706 && frame.y() < view.max_y()
707 && frame.max_x() > view.x()
708 && frame.x() < view.max_x()
709 {
710 due.push((e.widget, handler.clone()));
711 }
712 }
713 for (widget, handler) in due {
714 self.appeared.insert(widget);
715 handler();
716 }
717 }
718
719 pub fn scene(&self) -> &Scene {
721 &self.scene
722 }
723
724 pub fn element_count(&self) -> usize {
726 self.elements.len()
727 }
728
729 fn tell_activity(&self, activity: crate::context::UiActivity<'_>) {
730 if let Some(observer) = self.activity.clone() {
731 observer(activity, self.time);
732 }
733 }
734
735 pub fn handle_event(&mut self, event: &Event) -> bool {
737 match event {
738 Event::PointerMove { position } => {
739 self.pointer = Some(*position);
740 self.tell_activity(crate::context::UiActivity::PointerMoved(*position));
741 if let Some(widget) = self.dragging {
742 self.drag(widget, DragPhase::Move, *position);
743 }
744 self.refresh_hover();
745 if let Some(node) = self.hit_test(*position) {
746 let i = node.0 as usize;
747 if let Some(handler) = self.elements[i].on_pointer_move.clone() {
748 handler(*position, self.frames[i]);
749 }
750 }
751 }
752 Event::PointerDown { position, button: PointerButton::Primary } => {
753 self.pointer = Some(*position);
754 self.tell_activity(crate::context::UiActivity::Clicked(*position));
755 self.reveal_focus = false;
756 self.dismiss_overlays_outside(*position);
757 self.refresh_hover();
758 let target = self.hit_test(*position);
759 let element = target.map(|n| &self.elements[n.0 as usize]);
760 let widget = element.map(|e| e.widget);
761 let draggable = element.is_some_and(|e| e.on_drag.is_some());
762 if self.interaction.pressed != widget {
763 self.interaction.pressed = widget;
764 self.ask_build(BuildCause::Press);
765 }
766 let mut node = target;
768 let mut focused = None;
769 while let Some(n) = node {
770 let e = &self.elements[n.0 as usize];
771 if e.focus_radius.is_some() {
772 focused = Some(e.widget);
773 break;
774 }
775 node = self.parents.get(n.0 as usize).copied().flatten();
776 }
777 if self.interaction.focused != focused || self.interaction.focus_visible {
778 self.interaction.focused = focused;
779 self.interaction.focus_visible = false;
780 self.ask_build(BuildCause::Focus);
781 }
782 if let (true, Some(w)) = (draggable, widget) {
783 self.dragging = Some(w);
784 self.drag(w, DragPhase::Start, *position);
785 }
786 }
787 Event::PointerUp { position, button: PointerButton::Primary } => {
788 if let Some(w) = self.dragging.take() {
789 self.drag(w, DragPhase::End, *position);
790 }
791 let pressed = self.interaction.pressed.take();
792 if pressed.is_some() {
793 self.ask_build(BuildCause::Press);
794 }
795 if let Some(node) = self.hit_test(*position) {
796 let element = &self.elements[node.0 as usize];
797 if pressed == Some(element.widget) {
798 let handler = element.on_click.clone();
799 if let Some(handler) = handler {
800 handler();
801 }
802 }
803 }
804 }
805 Event::PointerLeave => {
806 self.pointer = None;
807 self.refresh_hover();
808 if self.dragging.is_none() && self.interaction.pressed.take().is_some() {
809 self.ask_build(BuildCause::Press);
810 }
811 }
812 Event::Scroll { position, delta } => {
813 if self.scroll(*position, *delta) {
814 if !self.needs_build {
815 self.place_and_paint();
816 self.refresh_hover();
817 }
818 return true;
819 }
820 }
821 Event::Pinch { position, delta } => {
822 self.pinch(*position, *delta);
823 }
824 Event::PointerDown { position, button: PointerButton::Secondary } => {
825 self.pointer = Some(*position);
826 self.dismiss_overlays_outside(*position);
827 let mut node = self.hit_test(*position);
828 while let Some(n) = node {
829 if let Some(handler) = self.elements[n.0 as usize].on_context_menu.clone() {
830 handler(*position);
831 break;
832 }
833 if let Some(handler) = self.elements[n.0 as usize].on_context_menu_in.clone() {
834 handler(*position, self.frames[n.0 as usize]);
835 break;
836 }
837 node = self.parents.get(n.0 as usize).copied().flatten();
838 }
839 }
840 Event::DoubleClick { position } => {
841 if let Some(node) = self.hit_test(*position) {
842 let handler = self.elements[node.0 as usize].on_double_click.clone();
843 if let Some(handler) = handler {
844 handler();
845 }
846 }
847 }
848 Event::KeyUp { modifiers, .. } => self.modifiers.set(*modifiers),
849 Event::KeyDown { key, modifiers } => {
850 self.modifiers.set(*modifiers);
851 let handler = self.focused_element().and_then(|e| e.on_key.clone());
852 if let Some(handler) = handler
853 && handler(key, *modifiers)
854 {
855 return self.needs_redraw();
856 }
857 let command = modifiers.ctrl || modifiers.meta;
858 if *key == Key::Escape
860 && let Some(dismiss) = self.overlays.iter().rev().find_map(|o| o.on_dismiss.clone())
861 {
862 dismiss();
863 return self.needs_redraw();
864 }
865 let shortcut = self.shortcuts.iter().find(|s| {
866 s.command == command
867 && (!s.shift || modifiers.shift)
868 && match (&s.key, key) {
869 (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
870 (a, b) => a == b,
871 }
872 });
873 if let Some(s) = shortcut.map(|s| s.handler.clone()) {
874 s();
875 return self.needs_redraw();
876 }
877 match key {
878 Key::Tab => self.move_focus(!modifiers.shift),
879 Key::Enter | Key::Space => {
880 let handler = self.focused_element().and_then(|e| e.on_click.clone());
881 if let Some(handler) = handler {
882 handler();
883 }
884 }
885 Key::Escape => {
886 let dismiss = self.overlays.iter().rev().find_map(|o| o.on_dismiss.clone());
887 if let Some(dismiss) = dismiss {
888 dismiss();
889 } else if self.interaction.focused.take().is_some() {
890 self.ask_build(BuildCause::Focus);
891 }
892 }
893 _ => {}
894 }
895 }
896 Event::FocusLost => {
897 self.dragging = None;
898 if self.interaction.pressed.take().is_some() {
899 self.ask_build(BuildCause::Press);
900 }
901 }
902 _ => {}
903 }
904 self.needs_redraw()
905 }
906
907 fn focused_element(&self) -> Option<&Element> {
908 let w = self.interaction.focused?;
909 self.elements.iter().find(|e| e.widget == w && e.focus_radius.is_some())
910 }
911
912 fn modal_root(&self) -> Option<NodeId> {
915 self.overlays.iter().rev().find(|o| o.placement == Placement::Fill).map(|o| o.root)
916 }
917
918 fn modal_layers(&self, root: NodeId) -> Vec<NodeId> {
920 let from = self.overlays.iter().position(|o| o.root == root).unwrap_or(0);
921 self.overlays[from..].iter().map(|o| o.root).collect()
922 }
923
924 fn is_inside(&self, mut node: NodeId, root: NodeId) -> bool {
925 loop {
926 if node == root {
927 return true;
928 }
929 match self.parents.get(node.0 as usize).copied().flatten() {
930 Some(parent) => node = parent,
931 None => return false,
932 }
933 }
934 }
935
936 fn first_focusable(&self, root: NodeId) -> Option<WidgetId> {
938 let mut stack = vec![root];
939 while let Some(n) = stack.pop() {
940 let e = &self.elements[n.0 as usize];
941 if e.focus_radius.is_some() {
942 return Some(e.widget);
943 }
944 stack.extend(e.children.iter().rev());
945 }
946 None
947 }
948
949 fn trap_focus(&mut self) {
952 match self.modal_root() {
953 Some(root) => {
954 let focused = self.interaction.focused;
955 let layers = self.modal_layers(root);
956 let inside = (0..self.elements.len()).any(|i| {
957 let e = &self.elements[i];
958 Some(e.widget) == focused
959 && e.focus_radius.is_some()
960 && layers.iter().any(|layer| self.is_inside(NodeId(i as u32), *layer))
961 });
962 if !inside {
963 self.modal_return.get_or_insert(focused);
964 let first = self.first_focusable(root);
966 if self.interaction.focused != first {
967 self.interaction.focused = first;
968 self.ask_build(BuildCause::Focus);
969 }
970 }
971 }
972 None => {
973 if let Some(back) = self.modal_return.take() {
974 self.interaction.focused = back;
975 self.ask_build(BuildCause::Focus);
976 }
977 }
978 }
979 }
980
981 fn drag(&mut self, widget: WidgetId, phase: DragPhase, position: Point) {
982 let found = self
983 .elements
984 .iter()
985 .enumerate()
986 .find(|(_, e)| e.widget == widget && e.on_drag.is_some())
987 .and_then(|(i, e)| {
988 let parent = self.parents.get(i).copied().flatten();
989 let container = parent.and_then(|p| self.frames.get(p.0 as usize).copied());
990 let frame = self.frames.get(i).copied()?;
991 Some((e.on_drag.clone()?, frame, container.unwrap_or(frame)))
992 });
993 if let Some((handler, frame, container)) = found {
994 handler(Drag { phase, position, frame, container });
995 }
996 }
997
998 fn dismiss_overlays_outside(&mut self, p: Point) {
999 let mut handlers = Vec::new();
1000 for (i, overlay) in self.overlays.iter().enumerate() {
1001 let Some(dismiss) = &overlay.on_dismiss else { continue };
1002 let inside = |n: NodeId| self.frames.get(n.0 as usize).is_some_and(|f| f.contains(p));
1003 let in_anchor = overlay.placement != Placement::Fill && inside(overlay.anchor);
1004 let in_popup_above =
1006 self.overlays[i + 1..].iter().any(|o| o.placement != Placement::Fill && inside(o.root));
1007 if !inside(overlay.root) && !in_anchor && !in_popup_above {
1008 handlers.push(dismiss.clone());
1009 }
1010 }
1011 for h in handlers {
1012 h();
1013 }
1014 }
1015
1016 fn pinch(&mut self, p: Point, delta: f32) {
1019 let layers = self.modal_root().map(|root| self.modal_layers(root));
1020 let handler = self.paint_order.iter().rev().find_map(|&node| {
1021 let i = node.0 as usize;
1022 let e = &self.elements[i];
1023 let reachable = layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(node, *root)));
1024 let inside = self.frames[i].intersect(&self.clips[i]).contains(p);
1025 if reachable && inside { e.on_pinch.clone() } else { None }
1026 });
1027 if let Some(handler) = handler {
1028 handler(delta);
1029 }
1030 }
1031
1032 fn scroll(&mut self, p: Point, delta: Point) -> bool {
1036 let layers = self.modal_root().map(|root| self.modal_layers(root));
1037 for &node in self.paint_order.iter().rev() {
1038 let i = node.0 as usize;
1039 let e = &self.elements[i];
1040 if !(e.scroll || e.scrolls_with_widget) || !self.frames[i].intersect(&self.clips[i]).contains(p) {
1041 continue;
1042 }
1043 if !layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(node, *root))) {
1044 continue;
1045 }
1046 let i = if e.scroll {
1048 i
1049 } else {
1050 let widget = e.widget;
1051 match self.elements.iter().position(|other| other.scroll && other.widget == widget) {
1052 Some(found) => found,
1053 None => continue,
1054 }
1055 };
1056 let e = &self.elements[i];
1057 let max = self.max_offset(i);
1058 let current = self.scroll.offsets.get(&e.widget).copied().unwrap_or(Point::ZERO);
1059 let delta = if e.wheel_sideways && delta.x == 0.0 { Point::new(delta.y, 0.0) } else { delta };
1060 let next = Point::new(
1061 if e.scroll_x { (current.x - delta.x).clamp(0.0, max.x) } else { 0.0 },
1062 (current.y - delta.y).clamp(0.0, max.y),
1063 );
1064 if (next.x - current.x).abs() > f32::EPSILON || (next.y - current.y).abs() > f32::EPSILON {
1065 self.scroll.offsets.insert(e.widget, next);
1066 self.scrolled_at.insert(e.widget, self.time);
1067 self.scrollbar_showing = true;
1068 self.next_live = Some(self.time);
1069 let e = &self.elements[i];
1070 if e.rebuild_on_scroll {
1071 self.ask_build(BuildCause::Scroll);
1072 }
1073 return true;
1074 }
1075 }
1076 false
1077 }
1078
1079 fn max_offset(&self, i: usize) -> Point {
1080 let (content, frame) = (self.content[i], self.frames[i]);
1081 Point::new((content.width - frame.width()).max(0.0), (content.height - frame.height()).max(0.0))
1082 }
1083
1084 fn clamped_offset(&self, i: usize) -> Point {
1085 let max = self.max_offset(i);
1086 let o = self.scroll.offsets.get(&self.elements[i].widget).copied().unwrap_or(Point::ZERO);
1087 Point::new(o.x.clamp(0.0, max.x), o.y.clamp(0.0, max.y))
1088 }
1089
1090 fn move_focus(&mut self, forward: bool) {
1091 let layers = self.modal_root().map(|root| self.modal_layers(root));
1092 let focusable: Vec<WidgetId> = self
1093 .paint_order
1094 .iter()
1095 .filter(|n| layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(**n, *root))))
1096 .map(|n| &self.elements[n.0 as usize])
1097 .filter(|e| e.focus_radius.is_some())
1098 .map(|e| e.widget)
1099 .collect();
1100 if focusable.is_empty() {
1101 return;
1102 }
1103 let current = self.interaction.focused.and_then(|w| focusable.iter().position(|f| *f == w));
1104 let next = match (current, forward) {
1105 (None, true) => 0,
1106 (None, false) => focusable.len() - 1,
1107 (Some(i), true) => (i + 1) % focusable.len(),
1108 (Some(i), false) => (i + focusable.len() - 1) % focusable.len(),
1109 };
1110 self.interaction.focused = Some(focusable[next]);
1111 self.interaction.focus_visible = true;
1112 self.reveal_focus = true;
1113 self.ask_build(BuildCause::Focus);
1114 }
1115
1116 fn refresh_hover(&mut self) -> bool {
1118 let target = self.pointer.and_then(|p| self.hit_test(p));
1119 let element = target.map(|n| &self.elements[n.0 as usize]);
1120 let hovered = element.map(|e| e.widget);
1121 self.cursor = element.and_then(|e| e.cursor).unwrap_or_default();
1122 let mut chain = Vec::new();
1123 let mut node = target;
1124 while let Some(n) = node {
1125 chain.push(self.elements[n.0 as usize].widget);
1126 node = self.parents.get(n.0 as usize).copied().flatten();
1127 }
1128 if hovered != self.interaction.hovered || chain != self.hover_chain {
1129 let readers = self.hover_readers.borrow();
1132 let matters = |widget: &WidgetId| readers.contains(widget);
1133 let affected = chain.iter().filter(|w| !self.hover_chain.contains(w)).any(matters)
1134 || self.hover_chain.iter().filter(|w| !chain.contains(w)).any(matters)
1135 || hovered.iter().chain(&self.interaction.hovered).any(matters);
1136 drop(readers);
1137 self.interaction.hovered = hovered;
1138 self.hover_chain = chain;
1139 if affected {
1140 self.ask_build(BuildCause::Hover);
1141 }
1142 return affected;
1143 }
1144 false
1145 }
1146
1147 fn hit_test(&self, p: Point) -> Option<NodeId> {
1148 self.paint_order.iter().rev().copied().find(|n| {
1149 let i = n.0 as usize;
1150 self.elements[i].is_interactive()
1151 && self.frames.get(i).zip(self.clips.get(i)).is_some_and(|(f, c)| f.intersect(c).contains(p))
1152 })
1153 }
1154
1155 fn rebuild(&mut self) {
1156 let _span = tracing::trace_span!("ravenrs::frame").entered();
1157 self.needs_build = false;
1158 let cause = self.take_build_cause();
1159 self.build_cause = Some(cause);
1160 self.state_dirty.set(false);
1161 self.ui_waker.clear();
1163 if let Err(e) = self.build() {
1164 tracing::error!(error = %e, "build failed; keeping the previous frame");
1165 return;
1166 }
1167 if let Err(e) = self.compute_layout() {
1168 tracing::error!(error = %e, "layout failed; keeping the previous frame");
1169 return;
1170 }
1171 self.place_and_paint();
1172 self.forget_stale_frames();
1173 }
1174
1175 fn forget_stale_frames(&mut self) {
1179 let asked = |e: &&Element| e.scroll || e.report_frame;
1180 if self.scroll.viewports.len() <= self.elements.iter().filter(asked).count() {
1182 return;
1183 }
1184 let live: HashSet<WidgetId> = self.elements.iter().filter(asked).map(|e| e.widget).collect();
1185 self.scroll.viewports.retain(|widget, _| live.contains(widget));
1186 }
1187
1188 fn build(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1189 let _span = tracing::trace_span!("ravenrs::build").entered();
1190 self.stats.builds += 1;
1191 self.frame.builds += 1;
1192 let cause = self.take_build_cause();
1193 self.stats.causes.add(cause);
1194 self.frame.cause.get_or_insert(cause);
1195 if let Some(profile) = &self.view_profile {
1196 profile.borrow_mut().costs.clear();
1197 }
1198 let started = self.phase_start();
1199 let result = self.build_views();
1200 self.phases.build += self.phase_elapsed(started);
1201 result
1202 }
1203
1204 fn build_views(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1205 self.layout.clear();
1206 self.elements.clear();
1207 self.root_node = None;
1208 self.overlays.clear();
1209 let placeholder = self.layout.node(LayoutStyle::leaf(), &[])?;
1210 let viewport = self.viewport;
1211 let theme = self.theme.clone();
1212 self.local.begin_build();
1213 let modifiers = self.modifiers.clone();
1214 let clipboard = self.clipboard.clone();
1215 let sound = self.sound.clone();
1216 let links = self.links.clone();
1217 let full_screen = self.full_screen.clone();
1218 let file_picker = self.file_picker.clone();
1219 let files_over = self.files_over.clone();
1220 let ui_waker = self.ui_waker.clone();
1221 let view_profile = self.view_profile.clone();
1222 self.hover_readers.borrow_mut().clear();
1223 let hover_readers = self.hover_readers.clone();
1224 let Self { root, text, layout, elements, interaction, observer, hover_chain, local, scroll, .. } =
1225 self;
1226 let now = self.time;
1227 let motion_clock = self.motion.clock;
1228 let motion_enabled = self.motion.enabled;
1229 let reduced_motion = self.system_reduced_motion || theme.peek().interaction.reduced_motion;
1230 self.motion.reduced = reduced_motion;
1231 let (node, overlays, requests, focus, wake, shortcuts, activity, asked, error, motions, moving) =
1232 observer.track(|| {
1233 let mut cx = BuildCx::new(
1234 theme,
1235 viewport,
1236 text,
1237 layout,
1238 elements,
1239 *interaction,
1240 hover_chain,
1241 scroll,
1242 modifiers,
1243 local,
1244 placeholder,
1245 clipboard,
1246 ui_waker,
1247 );
1248 cx.now = now;
1249 cx.sound = sound;
1250 cx.links = links;
1251 cx.full_screen = full_screen;
1252 cx.file_picker = file_picker;
1253 cx.files_over = files_over;
1254 cx.motion_clock = motion_clock;
1255 cx.motion_enabled = motion_enabled;
1256 cx.reduced_motion = reduced_motion;
1257 cx.view_profile = view_profile;
1258 cx.hover_readers = hover_readers;
1259 let content = cx.child(&**root);
1260 let style = LayoutStyle {
1261 width: Some(viewport.width),
1262 height: Some(viewport.height),
1263 align: Align::Stretch,
1264 overflow_hidden: true,
1265 ..LayoutStyle::stack(Axis::Vertical)
1266 };
1267 let node = cx.container(style, &[content], Paint::None);
1268 (
1269 node,
1270 std::mem::take(&mut cx.overlays),
1271 std::mem::take(&mut cx.scroll_requests),
1272 cx.focus_request.take(),
1273 cx.wake.take(),
1274 std::mem::take(&mut cx.shortcuts),
1275 cx.activity.take(),
1276 cx.access_tree_asked,
1277 cx.error.take(),
1278 std::mem::take(&mut cx.motions),
1279 cx.value_moving,
1280 )
1281 });
1282 self.wake = wake;
1283 self.value_moving = moving;
1284 self.shortcuts = shortcuts;
1285 self.activity = activity;
1286 self.access_tree_asked = asked;
1287 if let Some(widget) = focus
1288 && self.interaction.focused != Some(widget)
1289 {
1290 self.interaction.focus_visible &= self.interaction.focused.is_some();
1293 self.interaction.focused = Some(widget);
1294 self.reveal_focus = true;
1295 self.ask_build(BuildCause::Focus);
1297 }
1298 for (widget, offset) in requests {
1299 if self.scroll.offsets.insert(widget, offset) != Some(offset) {
1300 self.ask_build(BuildCause::Scroll);
1302 }
1303 }
1304 self.local.end_build();
1305 if let Some(e) = error {
1306 return Err(e);
1307 }
1308 self.root_node = Some(node);
1309 let mut overlays = overlays;
1310 overlays.sort_by_key(|overlay| !overlay.floating);
1311 self.overlays = overlays;
1312 self.motions = motions;
1313 let palette = self.theme.peek().palette;
1314 self.crossfade = self.motion.enabled && self.painted_palette.is_some_and(|p| p != palette);
1315 let elements = &self.elements;
1316 let paints = |n: NodeId| {
1317 let e = &elements[n.0 as usize];
1318 NodePaint::of(&e.paint, e.text_color)
1319 };
1320 self.motion.sync(&self.motions, paints, self.crossfade);
1321 self.parents.clear();
1322 self.parents.resize(self.elements.len(), None);
1323 for (i, e) in self.elements.iter().enumerate() {
1324 for c in &e.children {
1325 self.parents[c.0 as usize] = Some(NodeId(i as u32));
1326 }
1327 }
1328 for o in &self.overlays {
1330 if o.placement != Placement::Fill {
1331 self.parents[o.root.0 as usize] = Some(o.anchor);
1332 }
1333 }
1334 self.trap_focus();
1335 Ok(())
1336 }
1337
1338 fn compute_layout(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1339 let _span = tracing::trace_span!("ravenrs::layout").entered();
1340 self.stats.layouts += 1;
1341 let started = self.phase_start();
1342 let result = self.solve_layout();
1343 self.phases.layout += self.phase_elapsed(started);
1344 result
1345 }
1346
1347 fn solve_layout(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1348 let Some(root) = self.root_node else { return Ok(()) };
1349 let roots = std::iter::once(root).chain(self.overlays.iter().map(|o| o.root)).collect::<Vec<_>>();
1350 for node in roots {
1351 let layout_id = self.elements[node.0 as usize].layout;
1352 self.layout.compute(layout_id, self.viewport, |leaf, input| {
1353 let limit = input.known_width.or(input.available_width.limit());
1354 let one_line = leaf.truncate.is_some();
1356 let size = leaf.layout.measure(if one_line { None } else { limit });
1357 let width = match limit {
1358 Some(limit) if one_line => size.width.ceil().min(limit),
1359 _ => size.width.ceil(),
1360 };
1361 Size::new(input.known_width.unwrap_or(width), input.known_height.unwrap_or(size.height))
1362 })?;
1363 }
1364 self.content.clear();
1365 self.content.resize(self.elements.len(), Size::ZERO);
1366 for (i, e) in self.elements.iter().enumerate() {
1367 if e.scroll {
1368 self.content[i] = self.layout.content_size(e.layout)?;
1369 }
1370 if e.measure {
1371 let content = self.layout.content_size(e.layout)?;
1372 let before = self.scroll.contents.insert(e.widget, content);
1373 self.viewport_moved |= e.rebuild_on_scroll && before != Some(content);
1375 }
1376 }
1377 Ok(())
1378 }
1379
1380 fn place_and_paint(&mut self) {
1382 if self.root_node.is_none() {
1383 return;
1384 }
1385 self.place_all();
1386 if self.scroll_focus_into_view() {
1387 self.place_all();
1388 }
1389 self.tell_appeared();
1390 let snap = std::mem::take(&mut self.snap_layout);
1391 for entry in &self.motions {
1392 if let Some(t) = entry.motion.layout {
1393 let i = entry.node.0 as usize;
1394 let unscrolled = self.frames[i].translate(Point::ZERO - self.shifts[i]);
1395 self.motion.sync_frame(entry.key, self.elements[i].widget, unscrolled, t, snap);
1396 }
1397 }
1398 self.paint();
1399 }
1400
1401 fn place_all(&mut self) {
1403 let Some(root) = self.root_node else { return };
1404 self.stats.placements += 1;
1405 let started = self.phase_start();
1406 self.frames.clear();
1407 self.frames.resize(self.elements.len(), Rect::ZERO);
1408 self.shifts.clear();
1409 self.shifts.resize(self.elements.len(), Point::ZERO);
1410 self.views.clear();
1411 self.views.resize(self.elements.len(), Rect::from_origin_size(Point::ZERO, self.viewport));
1412 self.place(root, Point::ZERO);
1413 let overlays = std::mem::take(&mut self.overlays);
1414 for overlay in &overlays {
1415 let size = self.layout.frame(self.elements[overlay.root.0 as usize].layout).map(|f| f.size);
1416 let size = size.unwrap_or(Size::ZERO);
1417 let anchor = self.frames.get(overlay.anchor.0 as usize).copied().unwrap_or(Rect::ZERO);
1418 let origin = overlay_origin(overlay.placement, anchor, size, overlay.gap, self.viewport);
1419 self.place(overlay.root, origin);
1420 }
1421 self.overlays = overlays;
1422 self.phases.place += self.phase_elapsed(started);
1423 }
1424
1425 fn scroll_focus_into_view(&mut self) -> bool {
1429 if !std::mem::take(&mut self.reveal_focus) {
1430 return false;
1431 }
1432 let focused = self.interaction.focused;
1433 let is_focused = |e: &Element| Some(e.widget) == focused && e.focus_radius.is_some();
1434 let Some(start) = self.elements.iter().position(is_focused) else { return false };
1435 let ix = self.theme().interaction;
1436 let mut target = self.frames[start].inflate(ix.focus_ring_offset + ix.focus_ring_width);
1437 let mut scrolled = false;
1438 let mut node = NodeId(start as u32);
1439 while !self.overlays.iter().any(|o| o.root == node) {
1441 let Some(parent) = self.parents.get(node.0 as usize).copied().flatten() else { break };
1442 node = parent;
1443 let i = node.0 as usize;
1444 if !self.elements[i].scroll {
1445 continue;
1446 }
1447 let (frame, max, offset) = (self.frames[i], self.max_offset(i), self.clamped_offset(i));
1448 let next = Point::new(
1449 if self.elements[i].scroll_x {
1450 reveal_offset(offset.x, (target.x(), target.max_x()), (frame.x(), frame.max_x()), max.x)
1451 } else {
1452 offset.x
1453 },
1454 reveal_offset(offset.y, (target.y(), target.max_y()), (frame.y(), frame.max_y()), max.y),
1455 );
1456 if next != offset {
1457 self.scroll.offsets.insert(self.elements[i].widget, next);
1458 if self.elements[i].rebuild_on_scroll {
1460 self.ask_build(BuildCause::Scroll);
1461 }
1462 target = target.translate(offset - next);
1463 scrolled = true;
1464 }
1465 }
1466 scrolled
1467 }
1468
1469 fn place(&mut self, node: NodeId, origin: Point) {
1472 let window = Rect::from_origin_size(Point::ZERO, self.viewport);
1473 let mut stack = vec![(node, origin, true, Point::ZERO, window, window)];
1474 while let Some((node, parent_origin, is_root, mut shift, parent_frame, view)) = stack.pop() {
1475 let i = node.0 as usize;
1476 let Ok(local) = self.layout.frame(self.elements[i].layout) else { continue };
1477 let mut frame = if is_root {
1478 Rect::from_origin_size(parent_origin, local.size)
1479 } else {
1480 local.translate(parent_origin)
1481 };
1482 if let Some(top) = self.elements[i].pin.filter(|_| !is_root) {
1484 let lowest = (parent_frame.max_y() - frame.height()).max(frame.y());
1485 let y = frame.y().max(view.y() + top).min(lowest);
1486 let moved = Point::new(0.0, y - frame.y());
1487 frame = frame.translate(moved);
1488 shift = shift + moved;
1489 }
1490 self.frames[i] = frame;
1491 self.shifts[i] = shift;
1492 self.views[i] = view;
1493 let mut child_origin = frame.origin + self.elements[i].translate;
1494 let mut child_shift = shift;
1495 let mut child_view = view;
1496 if self.elements[i].scroll {
1497 child_view = frame;
1498 let offset = self.clamped_offset(i);
1499 child_origin = child_origin - offset;
1500 child_shift = child_shift - offset;
1501 let widget = self.elements[i].widget;
1502 let before = self.scroll.viewports.insert(widget, frame.size);
1503 self.viewport_moved |= self.elements[i].rebuild_on_scroll && before != Some(frame.size);
1504 self.scroll.offsets.insert(widget, offset);
1506 } else if self.elements[i].report_frame {
1507 let before = self.scroll.viewports.insert(self.elements[i].widget, frame.size);
1508 self.viewport_moved |= before != Some(frame.size);
1509 }
1510 let element = &self.elements[i];
1511 for child in &element.children {
1512 stack.push((*child, child_origin, false, child_shift, frame, child_view));
1513 }
1514 }
1515 }
1516
1517 fn paint(&mut self) {
1518 let _span = tracing::trace_span!("ravenrs::paint").entered();
1519 self.stats.paints += 1;
1520 self.frame.paints += 1;
1521 let started = self.phase_start();
1522 self.paint_scene();
1523 self.phases.paint += self.phase_elapsed(started);
1524 }
1525
1526 fn paint_scene(&mut self) {
1527 let theme = self.theme();
1528 self.scrollbar_showing = false;
1529 let viewport = Rect::from_origin_size(Point::ZERO, self.viewport);
1530 if self.crossfade {
1531 let mut old = Vec::with_capacity(self.scene.len() + 1);
1532 old.push(Primitive::Quad(Quad::new(viewport, self.scene.clear_color())));
1533 old.extend_from_slice(self.scene.primitives());
1534 self.motion.crossfade(old, viewport, theme.motion.base);
1535 self.crossfade = false;
1536 }
1537 self.painted_palette = Some(theme.palette);
1538 self.scene.clear();
1539 self.scene.set_clear_color(theme.color(Semantic::Canvas));
1540 self.paint_order.clear();
1541 self.clips.clear();
1542 self.clips.resize(self.elements.len(), Rect::ZERO);
1543 self.motion.active = false;
1544 self.has_live = false;
1545 self.painting_overlays = false;
1546 if let Some(root) = self.root_node {
1547 self.paint_node(root, viewport, &theme);
1548 }
1549 self.motion.paint_ghosts(GhostLayer::Main, &mut self.scene);
1550 self.painting_overlays = true;
1551 for i in 0..self.overlays.len() {
1552 let root = self.overlays[i].root;
1553 self.paint_node(root, viewport, &theme);
1554 }
1555 self.motion.paint_ghosts(GhostLayer::Overlay, &mut self.scene);
1556 self.motion.paint_ghosts(GhostLayer::Top, &mut self.scene);
1557 let live = self.has_live && !self.reduced_motion();
1558 self.next_live = (live || self.scrollbar_showing).then_some(self.time + LIVE_FRAME);
1559 self.tell_activity(crate::context::UiActivity::Painted(&self.scene));
1560 if std::mem::take(&mut self.access_tree_asked) {
1561 let tree = self.access_tree();
1562 self.tell_activity(crate::context::UiActivity::Described(&tree));
1563 }
1564 }
1565
1566 fn progress_of(&self, i: usize, range: crate::ScrollRange) -> f32 {
1568 let mut scene = None;
1569 if range == crate::ScrollRange::Scene {
1570 let mut node = Some(NodeId(i as u32));
1571 while let Some(n) = node {
1572 let k = n.0 as usize;
1573 if let Some(track) = self.elements[k].scroll_track {
1574 scene = Some((k, track));
1575 break;
1576 }
1577 node = self.parents.get(k).copied().flatten();
1578 }
1579 }
1580 match scene {
1581 Some((k, track)) => range.progress(self.frames[i], self.views[k], Some((self.frames[k], track))),
1582 None => range.progress(self.frames[i], self.views[i], None),
1583 }
1584 }
1585
1586 fn scroll_effect(&self, i: usize) -> Effect {
1588 let Some(link) = &self.elements[i].scroll_link else { return Effect::IDENTITY };
1589 let effect = link.effect_at(self.progress_of(i, link.scroll_range()));
1590 if self.reduced_motion() { Effect::IDENTITY.with_opacity(effect.opacity) } else { effect }
1592 }
1593
1594 fn reduced_motion(&self) -> bool {
1595 self.system_reduced_motion || self.theme.peek().interaction.reduced_motion
1596 }
1597
1598 fn paint_shape(&mut self, i: usize, frame: Rect, quad: Option<QuadPaint>) {
1600 match &self.elements[i].paint {
1601 Paint::None => {}
1602 Paint::Quad(q) => {
1603 let q = quad.unwrap_or(*q);
1604 let mut quad = Quad::new(frame, q.fill).radius(q.corner_radius);
1605 quad.border = q.border;
1606 quad.shadow = q.shadow;
1607 self.scene.push_quad(quad);
1608 }
1609 Paint::Custom(draw) => {
1610 let draw = draw.clone();
1611 draw(frame, &mut self.scene);
1612 }
1613 Paint::Live(draw) => {
1614 let draw = draw.clone();
1615 let time = if self.reduced_motion() { 0.0 } else { self.time };
1616 self.has_live = true;
1617 draw(frame, time, &mut self.scene);
1618 }
1619 Paint::Rich(draw) => {
1620 let draw = draw.clone();
1621 let mut cx = PaintCx { scene: &mut self.scene, text: &mut self.text, scale: self.scale };
1622 draw(frame, &mut cx);
1623 }
1624 }
1625 }
1626
1627 fn sample_motion(&mut self, i: usize) -> Option<(MotionEntry, Sampled)> {
1629 let index = self.elements[i].motion?;
1630 let entry = *self.motions.get(index as usize)?;
1631 let e = &self.elements[i];
1632 let paint = NodePaint::of(&e.paint, e.text_color);
1633 Some((entry, self.motion.sample(entry.key, &paint, self.shifts[i])))
1634 }
1635
1636 fn anchor_of(&self, node: NodeId) -> Option<Rect> {
1638 let overlay = self.overlays.iter().find(|o| o.root == node)?;
1639 self.frames.get(overlay.anchor.0 as usize).copied()
1640 }
1641
1642 fn paint_node(&mut self, node: NodeId, clip: Rect, theme: &Theme) {
1643 let i = node.0 as usize;
1644 self.paint_order.push(node);
1645 self.clips[i] = clip;
1646 let frame = self.frames[i];
1647 let start = self.scene.len();
1648 let sampled = self.sample_motion(i);
1649 let motion = sampled.as_ref().map(|(entry, _)| *entry);
1650 let (quad, text_color, visual, mut effect) = match sampled {
1651 Some((_, s)) => (s.quad, s.text, s.frame.unwrap_or(frame), s.effect),
1652 None => (None, None, frame, Effect::IDENTITY),
1653 };
1654 if let Some((range, cell)) = self.elements[i].progress_out.clone() {
1655 cell.set(self.progress_of(i, range));
1656 }
1657 let mut linked = self.scroll_effect(i);
1658 if let Some(link) = self.elements[i].time_link.clone() {
1659 let time = if self.reduced_motion() { 0.0 } else { self.time };
1660 self.has_live = true;
1661 linked = linked.then(link.effect_at(time, frame));
1662 }
1663 effect = effect.then(linked);
1664 let shown = effect != Effect::IDENTITY
1669 || motion.is_some_and(|m| m.motion.exit.is_some())
1670 || frame.is_empty()
1671 || touches(visual, clip);
1672 let underlaid = motion.is_some_and(|m| m.motion.underlay);
1673 if shown && !underlaid {
1674 self.paint_shape(i, visual, quad);
1675 }
1676 let exit = motion.filter(|m| m.motion.exit.is_some());
1677 if exit.is_some() {
1678 self.motion.begin_capture();
1679 }
1680 let content_start = self.scene.len();
1682 if let Some(color) = self.elements[i].text_color.filter(|_| shown) {
1683 let color = text_color.unwrap_or(color);
1684 let layout_id = self.elements[i].layout;
1685 if let Some(leaf) = self.layout.context_mut(layout_id) {
1686 if let Some((text, attrs)) = &leaf.truncate {
1687 let fitted = self.text.layout_ellipsized(text, attrs, frame.width() + 0.5);
1688 fitted.paint(frame.origin, color, &mut self.scene);
1689 } else {
1690 leaf.layout.measure(Some(frame.width() + 0.5));
1692 leaf.layout.paint(frame.origin, color, &mut self.scene);
1693 }
1694 }
1695 }
1696 let clips_children = self.elements[i].clip;
1697 let child_clip = if clips_children { clip.intersect(&frame) } else { clip };
1698 let pushes_clip = clips_children && shown;
1699 if pushes_clip {
1700 self.scene.push_clip(frame);
1701 }
1702 let children = self.elements[i].children.clone();
1703 for &child in &children {
1704 if let Some(m) = self.elements[child.0 as usize].motion
1705 && self.motions.get(m as usize).is_some_and(|e| e.motion.underlay)
1706 {
1707 let c = child.0 as usize;
1708 if let Some((_, s)) = self.sample_motion(c) {
1709 self.paint_shape(c, s.frame.unwrap_or(self.frames[c]), s.quad);
1710 }
1711 }
1712 }
1713 for child in children {
1714 self.paint_node(child, child_clip, theme);
1715 }
1716 let outside = self.elements[i].scrollbar_outside;
1717 if shown && self.elements[i].scroll && outside == 0.0 {
1718 self.paint_scrollbar(i, theme, 0.0);
1719 }
1720 if pushes_clip {
1721 self.scene.pop_clip();
1722 }
1723 if shown && self.elements[i].scroll && outside != 0.0 {
1724 self.paint_scrollbar(i, theme, outside);
1725 }
1726 let element = &self.elements[i];
1727 if let Some(radius) = element.focus_radius
1728 && shown
1729 && self.interaction.focus_visible
1730 && self.interaction.focused == Some(element.widget)
1731 {
1732 let ix = theme.interaction;
1733 let grow = ix.focus_ring_offset + ix.focus_ring_width;
1734 let ring = Quad::new(frame.inflate(grow), Color::TRANSPARENT)
1735 .radius(radius + grow)
1736 .border(ix.focus_ring_width, theme.color(Semantic::FocusRing));
1737 self.scene.push_quad(ring);
1738 }
1739 let Some(entry) = motion else {
1740 self.scene.apply_effect(start..self.scene.len(), linked, frame.center());
1741 return;
1742 };
1743 let end = self.scene.len();
1744 if !entry.motion.shape_only && visual.origin != frame.origin {
1745 self.scene.apply_effect(
1746 content_start..end,
1747 {
1748 let d = visual.origin - frame.origin;
1749 Effect::moved(d.x, d.y)
1750 },
1751 Point::ZERO,
1752 );
1753 }
1754 let mut pivot = visual.center();
1755 let mut flip = false;
1756 if entry.motion.anchored
1757 && let Some(anchor) = self.anchor_of(node)
1758 {
1759 let target = anchor.center();
1760 pivot = Point::new(
1761 target.x.clamp(visual.x(), visual.max_x()),
1762 target.y.clamp(visual.y(), visual.max_y()),
1763 );
1764 flip = visual.center().y < target.y;
1765 effect = flipped(effect, flip);
1766 }
1767 if exit.is_some() {
1768 let layer = if self.painting_overlays { GhostLayer::Overlay } else { GhostLayer::Main };
1769 let primitives = &self.scene.primitives()[start..end];
1770 self.motion.end_capture(entry.key, primitives, start..end, clip, pivot, flip, layer);
1771 }
1772 self.scene.apply_effect(start..end, effect, pivot);
1773 }
1774
1775 fn paint_scrollbar(&mut self, i: usize, theme: &Theme, outside: f32) {
1778 let frame = self.frames[i];
1779 let (content, max) = (self.content[i], self.max_offset(i));
1780 let offset = self.clamped_offset(i);
1781 let Some(&moved) = self.scrolled_at.get(&self.elements[i].widget) else { return };
1782 let age = self.time - moved;
1783 if age >= SCROLLBAR_HOLD + SCROLLBAR_FADE {
1784 return;
1785 }
1786 self.scrollbar_showing = true;
1787 let fading = ((age - SCROLLBAR_HOLD) / SCROLLBAR_FADE).clamp(0.0, 1.0) as f32;
1788 let alpha = if self.reduced_motion() { SCROLLBAR_ALPHA } else { SCROLLBAR_ALPHA * (1.0 - fading) };
1789 let color = theme.color(Semantic::InkMuted).with_alpha(alpha);
1790 if max.y > 0.5 && frame.height() > 0.0 {
1791 let track = frame.height() - 8.0;
1792 let thumb = (track * frame.height() / content.height).max(24.0).min(track);
1793 let y = frame.y() + 4.0 + (track - thumb) * (offset.y / max.y);
1794 let rect = Rect::new(frame.max_x() - SCROLLBAR_WIDTH - 3.0 + outside, y, SCROLLBAR_WIDTH, thumb);
1795 self.scene.push_quad(Quad::new(rect, color).radius(SCROLLBAR_WIDTH / 2.0));
1796 }
1797 if self.elements[i].scroll_x && max.x > 0.5 && frame.width() > 0.0 {
1798 let track = frame.width() - 8.0;
1799 let thumb = (track * frame.width() / content.width).max(24.0).min(track);
1800 let x = frame.x() + 4.0 + (track - thumb) * (offset.x / max.x);
1801 let rect = Rect::new(x, frame.max_y() - SCROLLBAR_WIDTH - 3.0, thumb, SCROLLBAR_WIDTH);
1802 self.scene.push_quad(Quad::new(rect, color).radius(SCROLLBAR_WIDTH / 2.0));
1803 }
1804 }
1805}
1806
1807fn reveal_offset(offset: f32, target: (f32, f32), view: (f32, f32), max: f32) -> f32 {
1811 let next = if target.0 < view.0 || target.1 - target.0 > view.1 - view.0 {
1812 offset + target.0 - view.0
1813 } else if target.1 > view.1 {
1814 offset + target.1 - view.1
1815 } else {
1816 offset
1817 };
1818 next.clamp(0.0, max)
1819}
1820
1821const VIEWPORT_MARGIN: f32 = 16.0;
1823
1824fn overlay_origin(placement: Placement, anchor: Rect, size: Size, gap: f32, viewport: Size) -> Point {
1825 let mut p = match placement {
1826 Placement::Below => Point::new(anchor.x(), anchor.max_y() + gap),
1827 Placement::Above => Point::new(anchor.x(), anchor.y() - gap - size.height),
1828 Placement::Right => Point::new(anchor.max_x() + gap, anchor.y()),
1829 Placement::AboveCenter => {
1830 Point::new(anchor.center().x - size.width / 2.0, anchor.y() - gap - size.height)
1831 }
1832 Placement::RightCenter => Point::new(anchor.max_x() + gap, anchor.center().y - size.height / 2.0),
1833 Placement::LeftCenter => {
1834 Point::new(anchor.x() - gap - size.width, anchor.center().y - size.height / 2.0)
1835 }
1836 Placement::RightEnd => Point::new(anchor.max_x() + gap, anchor.max_y() - size.height),
1837 Placement::LeftStart => Point::new(anchor.x() - gap - size.width, anchor.y()),
1838 Placement::LeftEnd => Point::new(anchor.x() - gap - size.width, anchor.max_y() - size.height),
1839 Placement::Fill => return Point::ZERO,
1840 Placement::At(p) => p,
1841 Placement::BottomRight => Point::new(
1842 viewport.width - size.width - VIEWPORT_MARGIN,
1843 viewport.height - size.height - VIEWPORT_MARGIN,
1844 ),
1845 Placement::BottomLeft => Point::new(VIEWPORT_MARGIN, viewport.height - size.height - VIEWPORT_MARGIN),
1846 Placement::BottomCenter => {
1847 Point::new((viewport.width - size.width) / 2.0, viewport.height - size.height - VIEWPORT_MARGIN)
1848 }
1849 Placement::TopLeft => Point::new(VIEWPORT_MARGIN, VIEWPORT_MARGIN),
1850 Placement::TopCenter => Point::new((viewport.width - size.width) / 2.0, VIEWPORT_MARGIN),
1851 Placement::TopRight => Point::new(viewport.width - size.width - VIEWPORT_MARGIN, VIEWPORT_MARGIN),
1852 };
1853 let left = anchor.x() - gap - size.width;
1856 if placement == Placement::Right && p.x + size.width > viewport.width && left >= 0.0 {
1857 p.x = left;
1858 }
1859 if placement == Placement::Below && p.y + size.height > viewport.height {
1861 p.y = anchor.y() - gap - size.height;
1862 }
1863 if matches!(placement, Placement::Above | Placement::AboveCenter) && p.y < 0.0 {
1864 p.y = anchor.max_y() + gap;
1865 }
1866 p.x = p.x.clamp(4.0, (viewport.width - size.width - 4.0).max(4.0));
1867 p.y = p.y.clamp(4.0, (viewport.height - size.height - 4.0).max(4.0));
1868 p
1869}
1870
1871#[cfg(test)]
1872mod frame_memory_tests {
1873 use ravenrs_core::Event;
1874
1875 use super::*;
1876 use crate::{Text, VirtualList};
1877
1878 #[test]
1879 fn a_measured_list_does_not_remember_every_row_it_ever_showed() {
1880 let list = VirtualList::new(5_000, |_| Text::new("row")).height(300.0).key(|i| i as u64);
1881 let mut ui = Ui::new(list, Theme::raven_dark());
1882 ui.set_viewport(Size::new(400.0, 400.0), 1.0);
1883 ui.update();
1884 ui.update();
1885 let at_rest = ui.scroll.viewports.len();
1886 assert!(at_rest > 1, "a measured row reports its frame, or this test proves nothing");
1887 for _ in 0..100 {
1888 ui.handle_event(&Event::Scroll {
1889 position: Point::new(100.0, 100.0),
1890 delta: Point::new(0.0, -600.0),
1891 });
1892 ui.update();
1893 ui.update();
1894 }
1895 assert!(ui.scroll.offsets.values().any(|o| o.y > 10_000.0), "the list did scroll");
1896 let scrolled = ui.scroll.viewports.len();
1897 assert!(scrolled <= at_rest + 4, "{scrolled} frames kept, {at_rest} at rest");
1898 }
1899}