1#![allow(non_snake_case)]
2
3use std::cell::RefCell;
4use std::cmp::Ordering;
5use std::collections::HashMap;
6use std::hash::{Hash, Hasher};
7use std::rc::Rc;
8use std::sync::Arc;
9
10use repose_core::*;
11use repose_tree::{NodeId, TreeNode, TreeStats, ViewTree};
12use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
13use taffy::TaffyTree;
14use taffy::prelude::*;
15use taffy::style::FlexDirection;
16use taffy::style::Overflow;
17
18use crate::Interactions;
19use crate::textfield::{
20 TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, byte_to_char_index, measure_text,
21};
22
23pub struct LayoutEngine {
25 tree: ViewTree,
27
28 taffy: TaffyTree<NodeContext>,
30
31 taffy_map: FxHashMap<NodeId, taffy::NodeId>,
33
34 reverse_map: FxHashMap<taffy::NodeId, NodeId>,
36
37 text_cache: FxHashMap<NodeId, TextLayout>,
39
40 last_size_px: Option<(u32, u32)>,
42
43 layout_valid: bool,
45
46 paint_cache: FxHashMap<NodeId, PaintCacheEntry>,
48
49 pub stats: LayoutStats,
51
52 last_locals_stamp: Option<u64>,
54
55 view_ids: FxHashMap<NodeId, u64>,
57 next_view_id: u64,
58
59 slider_dragging: Rc<RefCell<FxHashSet<u64>>>,
61 range_active_thumb: Rc<RefCell<FxHashMap<u64, bool>>>,
63}
64
65#[derive(Clone, Debug, Default)]
67pub struct LayoutStats {
68 pub tree: TreeStats,
70
71 pub taffy_created: usize,
73
74 pub taffy_reused: usize,
76
77 pub layout_hits: usize,
79
80 pub layout_misses: usize,
82
83 pub paint_cache_hits: usize,
85
86 pub paint_cache_misses: usize,
88
89 pub paint_culled: usize,
91
92 pub layout_time_ms: f32,
94}
95
96#[derive(Clone)]
97struct PaintCacheEntry {
98 subtree_hash: u64,
99 stamp: u64,
100 rect: repose_core::Rect,
101 sem_parent: Option<u64>,
102 alpha_q: u8,
103 nodes: Arc<Vec<SceneNode>>,
104 hits: Arc<Vec<HitRegion>>,
105 sems: Arc<Vec<SemNode>>,
106}
107
108#[derive(Clone)]
110enum NodeContext {
111 Text {
112 text: String,
113 font_dp: f32,
114 soft_wrap: bool,
115 max_lines: Option<usize>,
116 overflow: TextOverflow,
117 },
118 Button {
119 label: String,
120 },
121 TextField {
122 multiline: bool,
123 },
124 Checkbox,
125 Radio,
126 Switch,
127 Slider,
128 Range,
129 Progress,
130 Container,
131 ScrollContainer,
132}
133
134#[derive(Clone)]
135struct TextLayout {
136 lines: Vec<String>,
137 size_px: f32,
138 line_h_px: f32,
139}
140
141impl Default for LayoutEngine {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl LayoutEngine {
148 pub fn new() -> Self {
149 Self {
150 tree: ViewTree::new(),
151 taffy: TaffyTree::new(),
152 taffy_map: FxHashMap::default(),
153 reverse_map: FxHashMap::default(),
154 text_cache: FxHashMap::default(),
155 last_size_px: None,
156 layout_valid: false,
157 paint_cache: FxHashMap::default(),
158 stats: LayoutStats::default(),
159 last_locals_stamp: None,
160 view_ids: FxHashMap::default(),
161 next_view_id: 1,
162 slider_dragging: Rc::new(RefCell::new(FxHashSet::default())),
163 range_active_thumb: Rc::new(RefCell::new(FxHashMap::default())),
164 }
165 }
166
167 fn ensure_view_id(&mut self, node_id: NodeId) -> u64 {
168 if let Some(&id) = self.view_ids.get(&node_id) {
169 return id;
170 }
171 let id = self.next_view_id;
172 self.next_view_id += 1;
173 self.view_ids.insert(node_id, id);
174 id
175 }
176
177 fn locals_stamp() -> u64 {
178 let mut h = FxHasher::default();
179
180 locals::density().scale.to_bits().hash(&mut h);
182 locals::text_scale().0.to_bits().hash(&mut h);
183
184 let dir_u8 = match locals::text_direction() {
185 locals::TextDirection::Ltr => 0u8,
186 locals::TextDirection::Rtl => 1u8,
187 };
188 dir_u8.hash(&mut h);
189
190 h.finish()
191 }
192
193 pub fn layout_frame(
194 &mut self,
195 root: &View,
196 size_px: (u32, u32),
197 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
198 interactions: &Interactions,
199 focused: Option<u64>,
200 ) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
201 let start = web_time::Instant::now();
202 repose_text::begin_frame();
203 self.stats = LayoutStats::default();
204
205 let locals_stamp = Self::locals_stamp();
207 let locals_changed = self.last_locals_stamp != Some(locals_stamp);
208 if locals_changed {
209 self.layout_valid = false;
210 self.paint_cache.clear();
211 self.text_cache.clear();
212 }
213
214 let root_node_id = self.tree.update(root);
216 self.stats.tree = self.tree.stats.clone();
217
218 let size_changed = self.last_size_px != Some(size_px);
220 let has_tree_mutation =
221 !self.tree.dirty_nodes().is_empty() || !self.tree.removed_ids.is_empty();
222 let need_layout = size_changed || !self.layout_valid || has_tree_mutation || locals_changed;
223
224 let px = |dp_val: f32| dp_to_px(dp_val);
226 let font_px = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
227
228 self.sync_taffy_tree(root_node_id, &font_px);
230
231 let taffy_root = self.taffy_map.get(&root_node_id).copied();
233 if let Some(taffy_root) = taffy_root {
234 if need_layout {
235 if let Ok(mut style) = self.taffy.style(taffy_root).cloned() {
236 style.size.width = length(size_px.0 as f32);
237 style.size.height = length(size_px.1 as f32);
238 let _ = self.taffy.set_style(taffy_root, style);
239 }
240
241 let available = taffy::geometry::Size {
242 width: AvailableSpace::Definite(size_px.0 as f32),
243 height: AvailableSpace::Definite(size_px.1 as f32),
244 };
245
246 let text_cache = &mut self.text_cache;
247 let reverse_map = &self.reverse_map;
248 let tree = &self.tree;
249
250 let _ = self.taffy.compute_layout_with_measure(
251 taffy_root,
252 available,
253 |known, avail, taffy_node, ctx, _style| {
254 Self::measure_node(
255 known,
256 avail,
257 taffy_node,
258 ctx.as_deref(),
259 text_cache,
260 reverse_map,
261 tree,
262 &font_px,
263 &px,
264 )
265 },
266 );
267
268 self.last_locals_stamp = Some(locals_stamp);
269
270 self.layout_valid = true;
271 self.last_size_px = Some(size_px);
272 self.stats.layout_misses += 1;
273 } else {
274 self.stats.layout_hits += 1;
275 }
276 }
277 self.stats.layout_time_ms = (web_time::Instant::now() - start).as_secs_f32() * 1000.0;
278
279 let (scene, hits, sems) = self.paint(
281 root_node_id,
282 textfield_states,
283 interactions,
284 focused,
285 &font_px,
286 );
287
288 self.tree.clear_dirty();
289 (scene, hits, sems)
290 }
291
292 fn sync_taffy_tree(&mut self, root_id: NodeId, font_px: &dyn Fn(f32) -> f32) {
293 for &node_id in &self.tree.removed_ids {
295 if let Some(taffy_id) = self.taffy_map.remove(&node_id) {
296 let _ = self.taffy.remove(taffy_id);
297 self.reverse_map.remove(&taffy_id);
298 self.text_cache.remove(&node_id);
299 self.paint_cache.remove(&node_id);
300 }
301 if let Some(&vid) = self.view_ids.get(&node_id) {
303 self.slider_dragging.borrow_mut().remove(&vid);
304 self.range_active_thumb.borrow_mut().remove(&vid);
305 }
306 self.view_ids.remove(&node_id);
307 }
308
309 let dirty_nodes: Vec<NodeId> = self.tree.dirty_nodes().iter().copied().collect();
311 for node_id in dirty_nodes {
312 self.update_taffy_node(node_id, font_px);
313 }
314
315 if !self.taffy_map.contains_key(&root_id) {
317 self.update_taffy_node(root_id, font_px);
318 }
319 }
320
321 fn update_taffy_node(
322 &mut self,
323 node_id: NodeId,
324 font_px: &dyn Fn(f32) -> f32,
325 ) -> taffy::NodeId {
326 let _ = self.ensure_view_id(node_id);
328
329 if let Some(&t_id) = self.taffy_map.get(&node_id) {
330 self.apply_updates_to_taffy(node_id, t_id, font_px);
331 return t_id;
332 }
333
334 let (style, ctx, children) = {
335 let node = self.tree.get(node_id).expect("Node missing in update");
336 (
337 self.style_from_node(node, font_px),
338 self.context_from_node(node),
339 node.children.clone(),
340 )
341 };
342
343 let child_taffy_ids: Vec<taffy::NodeId> = children
344 .iter()
345 .map(|&child_id| self.update_taffy_node(child_id, font_px))
346 .collect();
347
348 let t_id = if child_taffy_ids.is_empty() {
349 self.taffy.new_leaf_with_context(style, ctx).unwrap()
350 } else {
351 let t = self
352 .taffy
353 .new_with_children(style, &child_taffy_ids)
354 .unwrap();
355 let _ = self.taffy.set_node_context(t, Some(ctx));
356 t
357 };
358
359 self.taffy_map.insert(node_id, t_id);
360 self.reverse_map.insert(t_id, node_id);
361 self.stats.taffy_created += 1;
362 t_id
363 }
364
365 fn apply_updates_to_taffy(
366 &mut self,
367 node_id: NodeId,
368 taffy_id: taffy::NodeId,
369 font_px: &dyn Fn(f32) -> f32,
370 ) {
371 let _ = self.ensure_view_id(node_id);
373
374 let (new_style, new_ctx, children) = {
375 let node = self.tree.get(node_id).unwrap();
376 (
377 self.style_from_node(node, font_px),
378 self.context_from_node(node),
379 node.children.clone(),
380 )
381 };
382
383 let _ = self.taffy.set_style(taffy_id, new_style);
384 let _ = self.taffy.set_node_context(taffy_id, Some(new_ctx));
385
386 let child_taffy_ids: Vec<taffy::NodeId> = children
387 .iter()
388 .map(|&child_id| self.update_taffy_node(child_id, font_px))
389 .collect();
390 let _ = self.taffy.set_children(taffy_id, &child_taffy_ids);
391 self.stats.taffy_reused += 1;
392 }
393
394 fn style_from_node(&self, node: &TreeNode, font_px: &dyn Fn(f32) -> f32) -> taffy::Style {
395 let m = &node.modifier;
396 let kind = &node.kind;
397 let px = |dp_val: f32| dp_to_px(dp_val);
398 let mut s = taffy::Style::default();
399
400 s.display = Display::Flex;
401 match kind {
402 ViewKind::Row => {
403 s.flex_direction = if locals::text_direction() == locals::TextDirection::Rtl {
404 FlexDirection::RowReverse
405 } else {
406 FlexDirection::Row
407 };
408 }
409 ViewKind::Column
410 | ViewKind::Surface
411 | ViewKind::ScrollV { .. }
412 | ViewKind::ScrollXY { .. }
413 | ViewKind::OverlayHost => {
414 s.flex_direction = FlexDirection::Column;
415 }
416 ViewKind::Stack => s.display = Display::Grid,
417 _ => {}
418 }
419
420 s.align_items = Some(AlignItems::Stretch);
421 s.justify_content = Some(JustifyContent::FlexStart);
422
423 if matches!(
424 kind,
425 ViewKind::Checkbox { .. }
426 | ViewKind::RadioButton { .. }
427 | ViewKind::Switch { .. }
428 | ViewKind::Slider { .. }
429 | ViewKind::RangeSlider { .. }
430 | ViewKind::Image { .. }
431 ) {
432 s.flex_shrink = 0.0;
433 } else {
434 s.flex_shrink = 1.0;
435 }
436
437 if let Some(g) = m.flex_grow {
438 s.flex_grow = g.max(0.0);
439 }
440 if let Some(sh) = m.flex_shrink {
441 s.flex_shrink = sh.max(0.0);
442 }
443 if let Some(b) = m.flex_basis {
444 s.flex_basis = length(px(b.max(0.0)));
445 }
446 if let Some(w) = m.flex_wrap {
447 s.flex_wrap = w;
448 }
449 if let Some(d) = m.flex_dir {
450 s.flex_direction = d;
451 }
452 if let Some(a) = m.align_self {
453 s.align_self = Some(a);
454 }
455 if let Some(j) = m.justify_content {
456 s.justify_content = Some(j);
457 }
458 if let Some(ai) = m.align_items_container {
459 s.align_items = Some(ai);
460 }
461 if let Some(ac) = m.align_content {
462 s.align_content = Some(ac);
463 }
464
465 if let Some(v) = m.margin_top {
466 s.margin.top = length(px(v));
467 }
468 if let Some(v) = m.margin_left {
469 s.margin.left = length(px(v));
470 }
471 if let Some(v) = m.margin_right {
472 s.margin.right = length(px(v));
473 }
474 if let Some(v) = m.margin_bottom {
475 s.margin.bottom = length(px(v));
476 }
477
478 if let Some(PositionType::Absolute) = m.position_type {
479 s.position = Position::Absolute;
480 s.inset = taffy::geometry::Rect {
481 left: m.offset_left.map(|v| length(px(v))).unwrap_or_else(auto),
482 right: m.offset_right.map(|v| length(px(v))).unwrap_or_else(auto),
483 top: m.offset_top.map(|v| length(px(v))).unwrap_or_else(auto),
484 bottom: m.offset_bottom.map(|v| length(px(v))).unwrap_or_else(auto),
485 };
486 }
487
488 if let Some(cfg) = &m.grid {
489 s.display = Display::Grid;
490 s.grid_template_columns = (0..cfg.columns.max(1))
491 .map(|_| GridTemplateComponent::Single(flex(1.0)))
492 .collect();
493 s.gap = taffy::geometry::Size {
494 width: length(px(cfg.column_gap)),
495 height: length(px(cfg.row_gap)),
496 };
497 }
498
499 if matches!(kind, ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. }) {
500 s.overflow = taffy::geometry::Point {
501 x: Overflow::Hidden,
502 y: Overflow::Hidden,
503 };
504 }
505
506 if let Some(pv) = m.padding_values {
507 s.padding = taffy::geometry::Rect {
508 left: length(px(pv.left)),
509 right: length(px(pv.right)),
510 top: length(px(pv.top)),
511 bottom: length(px(pv.bottom)),
512 };
513 } else if let Some(p) = m.padding {
514 let v = length(px(p));
515 s.padding = taffy::geometry::Rect {
516 left: v,
517 right: v,
518 top: v,
519 bottom: v,
520 };
521 }
522
523 let mut width_set = false;
524 let mut height_set = false;
525 if let Some(sz) = m.size {
526 if sz.width.is_finite() {
527 s.size.width = length(px(sz.width.max(0.0)));
528 width_set = true;
529 }
530 if sz.height.is_finite() {
531 s.size.height = length(px(sz.height.max(0.0)));
532 height_set = true;
533 }
534 }
535 if let Some(w) = m.width {
536 s.size.width = length(px(w.max(0.0)));
537 width_set = true;
538 }
539 if let Some(h) = m.height {
540 s.size.height = length(px(h.max(0.0)));
541 height_set = true;
542 }
543
544 if (m.fill_max || m.fill_max_w) && !width_set {
545 s.size.width = percent(1.0);
546 if s.min_size.width.is_auto() {
547 s.min_size.width = length(0.0);
548 }
549 }
550 if (m.fill_max || m.fill_max_h) && !height_set {
551 s.size.height = percent(1.0);
552 if matches!(kind, ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. })
553 && s.min_size.height.is_auto()
554 {
555 s.min_size.height = length(0.0);
556 }
557 }
558
559 if s.min_size.width.is_auto() {
560 s.min_size.width = length(0.0);
561 }
562
563 if matches!(kind, ViewKind::Button { .. }) {
564 s.display = Display::Flex;
565 s.flex_direction = if locals::text_direction() == locals::TextDirection::Rtl {
566 FlexDirection::RowReverse
567 } else {
568 FlexDirection::Row
569 };
570 if m.justify_content.is_none() {
571 s.justify_content = Some(JustifyContent::Center);
572 }
573 if m.align_items_container.is_none() {
574 s.align_items = Some(AlignItems::Center);
575 }
576 if m.padding.is_none() && m.padding_values.is_none() {
577 let ph = px(14.0);
578 let pv = px(10.0);
579 s.padding = taffy::geometry::Rect {
580 left: length(ph),
581 right: length(ph),
582 top: length(pv),
583 bottom: length(pv),
584 };
585 }
586 if m.min_height.is_none() && s.min_size.height.is_auto() {
587 s.min_size.height = length(px(40.0));
588 }
589 }
590
591 if let Some(v) = m.min_width {
592 s.min_size.width = length(px(v.max(0.0)));
593 }
594 if let Some(v) = m.min_height {
595 s.min_size.height = length(px(v.max(0.0)));
596 }
597 if let Some(v) = m.max_width {
598 s.max_size.width = length(px(v.max(0.0)));
599 }
600 if let Some(v) = m.max_height {
601 s.max_size.height = length(px(v.max(0.0)));
602 }
603 if let Some(r) = m.aspect_ratio {
604 s.aspect_ratio = Some(r.max(0.0));
605 }
606
607 if m.grid_col_span.is_some() || m.grid_row_span.is_some() {
608 let col_span = m.grid_col_span.unwrap_or(1).max(1);
609 let row_span = m.grid_row_span.unwrap_or(1).max(1);
610 s.grid_column = taffy::geometry::Line {
611 start: GridPlacement::Auto,
612 end: GridPlacement::Span(col_span),
613 };
614 s.grid_row = taffy::geometry::Line {
615 start: GridPlacement::Auto,
616 end: GridPlacement::Span(row_span),
617 };
618 }
619 s
620 }
621
622 fn context_from_node(&self, node: &TreeNode) -> NodeContext {
623 match &node.kind {
624 ViewKind::Text {
625 text,
626 font_size,
627 soft_wrap,
628 max_lines,
629 overflow,
630 ..
631 } => NodeContext::Text {
632 text: text.clone(),
633 font_dp: *font_size,
634 soft_wrap: *soft_wrap,
635 max_lines: *max_lines,
636 overflow: *overflow,
637 },
638 ViewKind::Button { .. } => NodeContext::Button {
639 label: String::new(),
640 },
641 ViewKind::TextField { multiline, .. } => NodeContext::TextField {
642 multiline: *multiline,
643 },
644 ViewKind::Checkbox { .. } => NodeContext::Checkbox,
645 ViewKind::RadioButton { .. } => NodeContext::Radio,
646 ViewKind::Switch { .. } => NodeContext::Switch,
647 ViewKind::Slider { .. } => NodeContext::Slider,
648 ViewKind::RangeSlider { .. } => NodeContext::Range,
649 ViewKind::ProgressBar { .. } => NodeContext::Progress,
650 ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. } => NodeContext::ScrollContainer,
651 ViewKind::OverlayHost => NodeContext::Container,
652 _ => NodeContext::Container,
653 }
654 }
655
656 fn measure_node(
657 known: taffy::geometry::Size<Option<f32>>,
658 avail: taffy::geometry::Size<AvailableSpace>,
659 taffy_node: taffy::NodeId,
660 ctx: Option<&NodeContext>,
661 text_cache: &mut FxHashMap<NodeId, TextLayout>,
662 reverse_map: &FxHashMap<taffy::NodeId, NodeId>,
663 _tree: &ViewTree,
664 font_px: &dyn Fn(f32) -> f32,
665 px: &dyn Fn(f32) -> f32,
666 ) -> taffy::geometry::Size<f32> {
667 match ctx {
668 Some(NodeContext::Text {
669 text,
670 font_dp,
671 soft_wrap,
672 max_lines,
673 overflow,
674 }) => {
675 let size_px_val = font_px(*font_dp);
676 let line_h_px_val = size_px_val * 1.3;
677 let max_content_w = measure_text(text, size_px_val)
678 .positions
679 .last()
680 .copied()
681 .unwrap_or(0.0)
682 .max(0.0);
683
684 let mut min_content_w = 0.0f32;
685 for w in text.split_whitespace() {
686 let ww = measure_text(w, size_px_val)
687 .positions
688 .last()
689 .copied()
690 .unwrap_or(0.0);
691 min_content_w = min_content_w.max(ww);
692 }
693 if min_content_w <= 0.0 {
694 min_content_w = max_content_w;
695 }
696
697 let wrap_w_px = if let Some(w) = known.width.filter(|w| *w > 0.5) {
698 w
699 } else {
700 match avail.width {
701 AvailableSpace::Definite(w) if w > 0.5 => w,
702 AvailableSpace::MinContent => min_content_w,
703 AvailableSpace::MaxContent => max_content_w,
704 _ => max_content_w,
705 }
706 };
707
708 let lines = if *soft_wrap {
709 repose_text::wrap_lines(text, size_px_val, wrap_w_px, *max_lines, true).0
710 } else if matches!(overflow, TextOverflow::Ellipsis)
711 && max_content_w > wrap_w_px + 0.5
712 {
713 vec![repose_text::ellipsize_line(text, size_px_val, wrap_w_px)]
714 } else {
715 vec![text.clone()]
716 };
717
718 let max_line_w = lines
719 .iter()
720 .map(|line| {
721 measure_text(line, size_px_val)
722 .positions
723 .last()
724 .copied()
725 .unwrap_or(0.0)
726 })
727 .fold(0.0f32, f32::max);
728
729 if let Some(node_id) = reverse_map.get(&taffy_node) {
730 text_cache.insert(
731 *node_id,
732 TextLayout {
733 lines: lines.clone(),
734 size_px: size_px_val,
735 line_h_px: line_h_px_val,
736 },
737 );
738 }
739 taffy::geometry::Size {
740 width: max_line_w,
741 height: line_h_px_val * lines.len().max(1) as f32,
742 }
743 }
744 Some(NodeContext::Button { label }) => taffy::geometry::Size {
745 width: (label.len() as f32 * font_px(16.0) * 0.6) + px(24.0),
746 height: px(36.0),
747 },
748 Some(NodeContext::TextField { multiline }) => taffy::geometry::Size {
749 width: known.width.unwrap_or(px(160.0)),
750 height: if *multiline {
751 known.height.unwrap_or(px(140.0))
752 } else {
753 px(36.0)
754 },
755 },
756 Some(NodeContext::Checkbox) => taffy::geometry::Size {
757 width: known.width.unwrap_or(px(24.0)),
758 height: px(24.0),
759 },
760 Some(NodeContext::Radio) => taffy::geometry::Size {
761 width: known.width.unwrap_or(px(18.0)),
762 height: px(18.0),
763 },
764 Some(NodeContext::Switch) => taffy::geometry::Size {
765 width: known.width.unwrap_or(px(46.0)),
766 height: px(28.0),
767 },
768 Some(NodeContext::Slider) => taffy::geometry::Size {
769 width: known.width.unwrap_or(px(200.0)),
770 height: px(28.0),
771 },
772 Some(NodeContext::Range) => taffy::geometry::Size {
773 width: known.width.unwrap_or(px(220.0)),
774 height: px(28.0),
775 },
776 Some(NodeContext::Progress) => taffy::geometry::Size {
777 width: known.width.unwrap_or(px(200.0)),
778 height: px(12.0),
779 },
780 _ => taffy::geometry::Size::ZERO,
781 }
782 }
783
784 fn paint(
785 &mut self,
786 root_id: NodeId,
787 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
788 interactions: &Interactions,
789 focused: Option<u64>,
790 font_px: &dyn Fn(f32) -> f32,
791 ) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
792 let mut scene = Scene {
793 clear_color: locals::theme().background,
794 nodes: vec![],
795 };
796 let mut hits = Vec::new();
797 let mut sems = Vec::new();
798 let mut deferred: Vec<(NodeId, (f32, f32), f32, Option<u64>, f32)> = Vec::new();
799 let mut deferred_blockers: Vec<(f32, repose_core::Rect)> = Vec::new();
800
801 self.walk_paint(
802 root_id,
803 &mut scene,
804 &mut hits,
805 &mut sems,
806 textfield_states,
807 interactions,
808 focused,
809 (0.0, 0.0),
810 1.0,
811 None,
812 font_px,
813 true,
814 &mut deferred,
815 false, );
817
818 deferred.sort_by(|a, b| a.4.partial_cmp(&b.4).unwrap_or(Ordering::Equal));
820 for (node_id, parent_offset_px, alpha_accum, sem_parent, z) in deferred.iter().copied() {
821 let view_id = *self.view_ids.get(&node_id).unwrap_or(&0);
822 let taffy_id = self.taffy_map[&node_id];
823 let layout = self.taffy.layout(taffy_id).unwrap();
824 let rect = repose_core::Rect {
825 x: parent_offset_px.0 + layout.location.x,
826 y: parent_offset_px.1 + layout.location.y,
827 w: layout.size.width,
828 h: layout.size.height,
829 };
830 if let Some(node) = self.tree.get(node_id) {
831 if node.modifier.input_blocker && !node.modifier.hit_passthrough {
832 deferred_blockers.push((z, rect));
833 }
834 }
835 self.walk_paint(
836 node_id,
837 &mut scene,
838 &mut hits,
839 &mut sems,
840 textfield_states,
841 interactions,
842 focused,
843 parent_offset_px,
844 alpha_accum,
845 sem_parent,
846 font_px,
847 true,
848 &mut Vec::new(), true, );
851 let _ = view_id;
852 }
853 deferred.clear();
854
855 if !deferred_blockers.is_empty() {
856 deferred_blockers.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
857 let max_z = hits
858 .iter()
859 .map(|h| h.z_index)
860 .fold(0.0_f32, |a, b| a.max(b));
861 let bump = max_z + 1.0;
862 for (i, (_z, rect)) in deferred_blockers.iter().enumerate() {
863 let blocker_id = u64::MAX - i as u64;
864 hits.push(HitRegion {
865 id: blocker_id,
866 rect: *rect,
867 on_click: None,
868 on_scroll: None,
869 focusable: false,
870 on_pointer_down: None,
871 on_pointer_move: None,
872 on_pointer_up: None,
873 on_pointer_enter: None,
874 on_pointer_leave: None,
875 z_index: bump + i as f32,
876 on_text_change: None,
877 on_text_submit: None,
878 tf_state_key: None,
879 tf_multiline: false,
880 on_action: None,
881 cursor: None,
882 on_drag_start: None,
883 on_drag_end: None,
884 on_drag_enter: None,
885 on_drag_over: None,
886 on_drag_leave: None,
887 on_drop: None,
888 });
889 }
890 }
891
892 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
893 (scene, hits, sems)
894 }
895
896 fn paint_stamp_hash(
897 &self,
898 root: NodeId,
899 interactions: &Interactions,
900 focused: Option<u64>,
901 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
902 sem_parent: Option<u64>,
903 alpha_accum: f32,
904 ) -> u64 {
905 let mut h = FxHasher::default();
906 sem_parent.hash(&mut h);
907 let alpha_q: u8 = (alpha_accum.clamp(0.0, 1.0) * 255.0).round() as u8;
908 alpha_q.hash(&mut h);
909 interactions.hover.hash(&mut h);
910 focused.hash(&mut h);
911 if !interactions.pressed.is_empty() {
912 let mut pressed: Vec<u64> = interactions.pressed.iter().copied().collect();
913 pressed.sort_unstable();
914 pressed.hash(&mut h);
915 }
916
917 let mut stack = Vec::new();
918 stack.push(root);
919 while let Some(id) = stack.pop() {
920 let Some(n) = self.tree.get(id) else { continue };
921 match &n.kind {
922 ViewKind::ScrollV {
923 get_scroll_offset, ..
924 } => {
925 if let Some(get) = get_scroll_offset {
926 let q = (get() * 8.0) as i32;
927 q.hash(&mut h);
928 }
929 }
930 ViewKind::ScrollXY {
931 get_scroll_offset_xy,
932 ..
933 } => {
934 if let Some(get) = get_scroll_offset_xy {
935 let (x, y) = get();
936 ((x * 8.0) as i32).hash(&mut h);
937 ((y * 8.0) as i32).hash(&mut h);
938 }
939 }
940 ViewKind::TextField { state_key, .. } => {
941 let vid = *self.view_ids.get(&id).unwrap_or(&0);
942 let tf_key = if *state_key != 0 { *state_key } else { vid };
943 if let Some(st_rc) = textfield_states.get(&tf_key) {
944 let st = st_rc.borrow();
945 let mut th = FxHasher::default();
946 st.text.hash(&mut th);
947 th.finish().hash(&mut h);
948 st.selection.start.hash(&mut h);
949 st.selection.end.hash(&mut h);
950 if let Some(r) = &st.composition {
951 r.start.hash(&mut h);
952 r.end.hash(&mut h);
953 } else {
954 0usize.hash(&mut h);
955 0usize.hash(&mut h);
956 }
957 ((st.scroll_offset * 8.0) as i32).hash(&mut h);
958 ((st.scroll_offset_y * 8.0) as i32).hash(&mut h);
959 st.caret_visible().hash(&mut h);
960 }
961 }
962 _ => {}
963 }
964 for &ch in n.children.iter() {
965 stack.push(ch);
966 }
967 }
968 h.finish()
969 }
970
971 fn walk_paint_view(
972 &mut self,
973 view: &View,
974 scene: &mut Scene,
975 hits: &mut Vec<HitRegion>,
976 sems: &mut Vec<SemNode>,
977 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
978 interactions: &Interactions,
979 focused: Option<u64>,
980 parent_offset_px: (f32, f32),
981 alpha_accum: f32,
982 sem_parent: Option<u64>,
983 font_px: &dyn Fn(f32) -> f32,
984 ) {
985 let root_id = self.tree.update(view);
986 self.sync_taffy_tree(root_id, font_px);
987 self.walk_paint(
988 root_id,
989 scene,
990 hits,
991 sems,
992 textfield_states,
993 interactions,
994 focused,
995 parent_offset_px,
996 alpha_accum,
997 sem_parent,
998 font_px,
999 false,
1000 &mut Vec::new(),
1001 false,
1002 );
1003 }
1004
1005 fn walk_paint(
1006 &mut self,
1007 node_id: NodeId,
1008 scene: &mut Scene,
1009 hits: &mut Vec<HitRegion>,
1010 sems: &mut Vec<SemNode>,
1011 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1012 interactions: &Interactions,
1013 focused: Option<u64>,
1014 parent_offset_px: (f32, f32),
1015 alpha_accum: f32,
1016 sem_parent: Option<u64>,
1017 font_px: &dyn Fn(f32) -> f32,
1018 allow_cache: bool,
1019 deferred: &mut Vec<(NodeId, (f32, f32), f32, Option<u64>, f32)>,
1020 skip_defer: bool,
1021 ) {
1022 let (subtree_hash, modifier, kind, children) = {
1023 let n = self.tree.get(node_id).unwrap();
1024 (
1025 n.subtree_hash,
1026 n.modifier.clone(),
1027 n.kind.clone(),
1028 n.children.clone(),
1029 )
1030 };
1031
1032 let view_id = *self.view_ids.get(&node_id).unwrap_or(&0);
1033
1034 if !skip_defer {
1036 if let Some(render_z) = modifier.render_z_index {
1037 if !deferred.is_empty() || render_z != 0.0 {
1038 deferred.push((node_id, parent_offset_px, alpha_accum, sem_parent, render_z));
1040 return;
1041 }
1042 }
1043 }
1044 debug_assert!(view_id != 0);
1045
1046 let taffy_id = self.taffy_map[&node_id];
1047 let layout = self.taffy.layout(taffy_id).unwrap();
1048
1049 let local_rect = repose_core::Rect {
1050 x: layout.location.x,
1051 y: layout.location.y,
1052 w: layout.size.width,
1053 h: layout.size.height,
1054 };
1055 let rect = repose_core::Rect {
1056 x: parent_offset_px.0 + local_rect.x,
1057 y: parent_offset_px.1 + local_rect.y,
1058 w: local_rect.w,
1059 h: local_rect.h,
1060 };
1061
1062 let content_rect = if let Some(pv) = modifier.padding_values {
1063 repose_core::Rect {
1064 x: rect.x + dp_to_px(pv.left),
1065 y: rect.y + dp_to_px(pv.top),
1066 w: (rect.w - dp_to_px(pv.left) - dp_to_px(pv.right)).max(0.0),
1067 h: (rect.h - dp_to_px(pv.top) - dp_to_px(pv.bottom)).max(0.0),
1068 }
1069 } else if let Some(p) = modifier.padding {
1070 let p_px = dp_to_px(p);
1071 repose_core::Rect {
1072 x: rect.x + p_px,
1073 y: rect.y + p_px,
1074 w: (rect.w - 2.0 * p_px).max(0.0),
1075 h: (rect.h - 2.0 * p_px).max(0.0),
1076 }
1077 } else {
1078 rect
1079 };
1080
1081 let base_px = (rect.x, rect.y);
1082
1083 let is_hovered = interactions.hover == Some(view_id);
1084 let is_pressed = interactions.pressed.contains(&view_id);
1085 let is_focused = focused == Some(view_id);
1086 let this_alpha = modifier.alpha.unwrap_or(1.0);
1087 let alpha_accum = (alpha_accum * this_alpha).clamp(0.0, 1.0);
1088 let alpha_q: u8 = (alpha_accum * 255.0).round() as u8;
1089
1090 if allow_cache && modifier.repaint_boundary {
1092 let stamp = self.paint_stamp_hash(
1093 node_id,
1094 interactions,
1095 focused,
1096 textfield_states,
1097 sem_parent,
1098 alpha_accum,
1099 );
1100 if let Some(entry) = self.paint_cache.get(&node_id) {
1101 if entry.subtree_hash == subtree_hash
1102 && entry.stamp == stamp
1103 && entry.rect == rect
1104 && entry.sem_parent == sem_parent
1105 && entry.alpha_q == alpha_q
1106 {
1107 self.stats.paint_cache_hits += 1;
1108 scene.nodes.extend(entry.nodes.iter().cloned());
1109 hits.extend(entry.hits.iter().cloned());
1110 sems.extend(entry.sems.iter().cloned());
1111 return;
1112 }
1113 }
1114 self.stats.paint_cache_misses += 1;
1115 let mut local_scene = Scene {
1116 clear_color: scene.clear_color,
1117 nodes: Vec::new(),
1118 };
1119 let mut local_hits = Vec::new();
1120 let mut local_sems = Vec::new();
1121 self.walk_paint(
1122 node_id,
1123 &mut local_scene,
1124 &mut local_hits,
1125 &mut local_sems,
1126 textfield_states,
1127 interactions,
1128 focused,
1129 parent_offset_px,
1130 alpha_accum / this_alpha.max(1e-6),
1131 sem_parent,
1132 font_px,
1133 false,
1134 &mut Vec::new(), true, );
1137
1138 let entry = PaintCacheEntry {
1139 subtree_hash,
1140 stamp,
1141 rect,
1142 sem_parent,
1143 alpha_q,
1144 nodes: Arc::new(local_scene.nodes.clone()),
1145 hits: Arc::new(local_hits.clone()),
1146 sems: Arc::new(local_sems.clone()),
1147 };
1148 self.paint_cache.insert(node_id, entry);
1149 scene.nodes.extend(local_scene.nodes);
1150 hits.extend(local_hits);
1151 sems.extend(local_sems);
1152 return;
1153 }
1154
1155 let round_clip_px = clamp_radius(
1156 modifier.clip_rounded.map(dp_to_px).unwrap_or(0.0),
1157 rect.w,
1158 rect.h,
1159 );
1160 let push_round_clip = round_clip_px > 0.5 && rect.w > 0.5 && rect.h > 0.5;
1161 if (this_alpha - 1.0).abs() > 1e-6 {}
1162 if let Some(tf) = modifier.transform {
1163 scene.nodes.push(SceneNode::PushTransform { transform: tf });
1164 }
1165 if push_round_clip {
1166 scene.nodes.push(SceneNode::PushClip {
1167 rect,
1168 radius: round_clip_px,
1169 });
1170 }
1171
1172 if let Some(bg) = modifier.background {
1173 scene.nodes.push(SceneNode::Rect {
1174 rect,
1175 brush: mul_alpha_brush(bg, alpha_accum),
1176 radius: round_clip_px,
1177 });
1178 }
1179 if let Some(b) = &modifier.border {
1180 scene.nodes.push(SceneNode::Border {
1181 rect,
1182 color: mul_alpha_color(b.color, alpha_accum),
1183 width: dp_to_px(b.width),
1184 radius: clamp_radius(
1185 dp_to_px(b.radius.max(modifier.clip_rounded.unwrap_or(0.0))),
1186 rect.w,
1187 rect.h,
1188 ),
1189 });
1190 }
1191 if let Some(p) = &modifier.painter {
1192 (p)(scene, rect);
1193 }
1194
1195 let has_pointer = modifier.on_pointer_down.is_some()
1196 || modifier.on_pointer_move.is_some()
1197 || modifier.on_pointer_up.is_some()
1198 || modifier.on_pointer_enter.is_some()
1199 || modifier.on_pointer_leave.is_some();
1200
1201 let has_dnd = modifier.on_drag_start.is_some()
1202 || modifier.on_drag_end.is_some()
1203 || modifier.on_drag_enter.is_some()
1204 || modifier.on_drag_over.is_some()
1205 || modifier.on_drag_leave.is_some()
1206 || modifier.on_drop.is_some();
1207
1208 let kind_handles_hit = matches!(
1209 kind,
1210 ViewKind::Button { .. }
1211 | ViewKind::TextField { .. }
1212 | ViewKind::Checkbox { .. }
1213 | ViewKind::RadioButton { .. }
1214 | ViewKind::Switch { .. }
1215 | ViewKind::Slider { .. }
1216 | ViewKind::RangeSlider { .. }
1217 | ViewKind::ScrollV { .. }
1218 | ViewKind::ScrollXY { .. }
1219 );
1220
1221 let needs_hit = has_pointer || modifier.click || has_dnd || modifier.on_action.is_some();
1222
1223 if needs_hit && !kind_handles_hit && !modifier.hit_passthrough {
1224 hits.push(HitRegion {
1225 id: view_id,
1226 rect,
1227 on_click: None,
1228 on_scroll: None,
1229 focusable: false,
1230 on_pointer_down: modifier.on_pointer_down.clone(),
1231 on_pointer_move: modifier.on_pointer_move.clone(),
1232 on_pointer_up: modifier.on_pointer_up.clone(),
1233 on_pointer_enter: modifier.on_pointer_enter.clone(),
1234 on_pointer_leave: modifier.on_pointer_leave.clone(),
1235 z_index: modifier.z_index,
1236 on_text_change: None,
1237 on_text_submit: None,
1238 tf_state_key: None,
1239 tf_multiline: false,
1240 on_action: modifier.on_action.clone(),
1241 cursor: modifier.cursor,
1242
1243 on_drag_start: modifier.on_drag_start.clone(),
1244 on_drag_end: modifier.on_drag_end.clone(),
1245 on_drag_enter: modifier.on_drag_enter.clone(),
1246 on_drag_over: modifier.on_drag_over.clone(),
1247 on_drag_leave: modifier.on_drag_leave.clone(),
1248 on_drop: modifier.on_drop.clone(),
1249 });
1250 }
1251
1252 let mut next_sem_parent = sem_parent;
1253
1254 match &kind {
1255 ViewKind::Text {
1256 text,
1257 color,
1258 font_size,
1259 soft_wrap,
1260 overflow,
1261 ..
1262 } => {
1263 let tl = self.text_cache.get(&node_id);
1264 let (size_px, line_h_px, lines) = if let Some(tl) = tl {
1265 (tl.size_px, tl.line_h_px, tl.lines.clone())
1266 } else {
1267 let px = font_px(*font_size);
1268 (px, px * 1.3, vec![text.clone()])
1269 };
1270 let total_h = lines.len() as f32 * line_h_px;
1271 let need_v_clip =
1272 total_h > content_rect.h + 0.5 && *overflow != TextOverflow::Visible;
1273 if lines.len() > 1 && !*soft_wrap { }
1274
1275 let need_clip =
1276 *overflow != TextOverflow::Visible && (need_v_clip || content_rect.w > 0.0);
1277 if need_clip {
1278 scene.nodes.push(SceneNode::PushClip {
1279 rect: content_rect,
1280 radius: 0.0,
1281 });
1282 }
1283 for (i, ln) in lines.iter().enumerate() {
1284 scene.nodes.push(SceneNode::Text {
1285 rect: repose_core::Rect {
1286 x: content_rect.x,
1287 y: content_rect.y + i as f32 * line_h_px,
1288 w: content_rect.w,
1289 h: line_h_px,
1290 },
1291 text: Arc::<str>::from(ln.clone()),
1292 color: mul_alpha_color(*color, alpha_accum),
1293 size: size_px,
1294 });
1295 }
1296 if need_clip {
1297 scene.nodes.push(SceneNode::PopClip);
1298 }
1299 sems.push(SemNode {
1300 id: view_id,
1301 parent: sem_parent,
1302 role: Role::Text,
1303 label: Some(text.clone()),
1304 rect,
1305 focused: is_focused,
1306 enabled: true,
1307 });
1308 next_sem_parent = Some(view_id);
1309 }
1310 ViewKind::Button { on_click } => {
1311 if modifier.background.is_none() {
1312 let th = locals::theme();
1313 let base = if is_pressed {
1314 th.button_bg_pressed
1315 } else if is_hovered {
1316 th.button_bg_hover
1317 } else {
1318 th.button_bg
1319 };
1320 scene.nodes.push(SceneNode::Rect {
1321 rect,
1322 brush: Brush::Solid(base),
1323 radius: modifier.clip_rounded.map(dp_to_px).unwrap_or(dp_to_px(6.0)),
1324 });
1325 }
1326 if (modifier.click || on_click.is_some()) && !modifier.hit_passthrough {
1327 hits.push(HitRegion {
1328 id: view_id,
1329 rect,
1330 on_click: on_click.clone(),
1331 on_scroll: None,
1332 focusable: true,
1333 on_pointer_down: modifier.on_pointer_down.clone(),
1334 on_pointer_move: modifier.on_pointer_move.clone(),
1335 on_pointer_up: modifier.on_pointer_up.clone(),
1336 on_pointer_enter: modifier.on_pointer_enter.clone(),
1337 on_pointer_leave: modifier.on_pointer_leave.clone(),
1338 z_index: modifier.z_index,
1339 on_text_change: None,
1340 on_text_submit: None,
1341 tf_state_key: None,
1342 tf_multiline: false,
1343 on_action: modifier.on_action.clone(),
1344 cursor: modifier.cursor,
1345
1346 on_drag_start: modifier.on_drag_start.clone(),
1347 on_drag_end: modifier.on_drag_end.clone(),
1348 on_drag_enter: modifier.on_drag_enter.clone(),
1349 on_drag_over: modifier.on_drag_over.clone(),
1350 on_drag_leave: modifier.on_drag_leave.clone(),
1351 on_drop: modifier.on_drop.clone(),
1352 });
1353 }
1354 sems.push(SemNode {
1355 id: view_id,
1356 parent: sem_parent,
1357 role: Role::Button,
1358 label: infer_label(&self.tree, node_id),
1359 rect,
1360 focused: is_focused,
1361 enabled: true,
1362 });
1363 next_sem_parent = Some(view_id);
1364 if is_focused {
1365 scene.nodes.push(SceneNode::Border {
1366 rect,
1367 color: locals::theme().focus,
1368 width: dp_to_px(2.0),
1369 radius: modifier.clip_rounded.map(dp_to_px).unwrap_or(dp_to_px(6.0)),
1370 });
1371 }
1372 }
1373 ViewKind::Image { handle, tint, fit } => {
1374 scene.nodes.push(SceneNode::Image {
1375 rect,
1376 handle: *handle,
1377 tint: mul_alpha_color(*tint, alpha_accum),
1378 fit: *fit,
1379 });
1380 }
1381 ViewKind::TextField {
1382 state_key,
1383 hint,
1384 multiline,
1385 on_change,
1386 on_submit,
1387 ..
1388 } => {
1389 let tf_key = if *state_key != 0 { *state_key } else { view_id };
1390
1391 let pad_x = dp_to_px(TF_PADDING_X_DP);
1392 let inner = repose_core::Rect {
1393 x: rect.x + pad_x,
1394 y: rect.y + dp_to_px(8.0),
1395 w: rect.w - 2.0 * pad_x,
1396 h: rect.h - dp_to_px(16.0),
1397 };
1398
1399 let on_scroll = if *multiline {
1401 let key = tf_key;
1402 let h = inner.h;
1403 let font_val = font_px(TF_FONT_DP);
1404 let wrap_w = inner.w.max(1.0);
1405 let states = textfield_states.get(&key).cloned();
1406 Some(Rc::new(move |d: Vec2| -> Vec2 {
1407 let Some(st_rc) = states.as_ref() else {
1408 return d;
1409 };
1410 let mut st = st_rc.borrow_mut();
1411 st.set_inner_height(h);
1412 let layout = crate::textfield::layout_text_area(&st.text, font_val, wrap_w);
1413 let content_h = layout.ranges.len().max(1) as f32 * layout.line_h_px;
1414 let max_y = (content_h - st.inner_height).max(0.0);
1415
1416 let before = st.scroll_offset_y;
1417 let target = (st.scroll_offset_y - d.y).clamp(0.0, max_y);
1418 st.scroll_offset_y = target;
1419
1420 let consumed = before - target;
1421 Vec2 {
1422 x: d.x,
1423 y: d.y - consumed,
1424 }
1425 }) as Rc<dyn Fn(Vec2) -> Vec2>)
1426 } else {
1427 None
1428 };
1429
1430 if !modifier.hit_passthrough {
1431 hits.push(HitRegion {
1432 id: view_id,
1433 rect,
1434 on_click: None,
1435 on_scroll,
1436 focusable: true,
1437 on_pointer_down: None,
1438 on_pointer_move: None,
1439 on_pointer_up: None,
1440 on_pointer_enter: None,
1441 on_pointer_leave: None,
1442 z_index: modifier.z_index,
1443 on_text_change: on_change.clone(),
1444 on_text_submit: on_submit.clone(),
1445 tf_state_key: Some(tf_key),
1446 tf_multiline: *multiline,
1447 on_action: modifier.on_action.clone(),
1448 cursor: Some(crate::CursorIcon::Text),
1449 on_drag_start: modifier.on_drag_start.clone(),
1450 on_drag_end: modifier.on_drag_end.clone(),
1451 on_drag_enter: modifier.on_drag_enter.clone(),
1452 on_drag_over: modifier.on_drag_over.clone(),
1453 on_drag_leave: modifier.on_drag_leave.clone(),
1454 on_drop: modifier.on_drop.clone(),
1455 });
1456 }
1457
1458 scene.nodes.push(SceneNode::PushClip {
1459 rect: inner,
1460 radius: 0.0,
1461 });
1462
1463 if is_focused {
1464 scene.nodes.push(SceneNode::Border {
1465 rect,
1466 color: locals::theme().focus,
1467 width: dp_to_px(2.0),
1468 radius: modifier.clip_rounded.map(dp_to_px).unwrap_or(dp_to_px(6.0)),
1469 });
1470 }
1471
1472 if let Some(state_rc) = textfield_states.get(&tf_key) {
1473 {
1474 let mut st = state_rc.borrow_mut();
1475 st.set_inner_width(inner.w);
1476 st.set_inner_height(inner.h);
1477 }
1478
1479 let st = state_rc.borrow();
1480 let font_val = font_px(TF_FONT_DP);
1481
1482 if !*multiline {
1483 let m = measure_text(&st.text, font_val);
1484
1485 if st.selection.start != st.selection.end {
1486 let sx = m
1487 .positions
1488 .get(byte_to_char_index(&m, st.selection.start))
1489 .copied()
1490 .unwrap_or(0.0)
1491 - st.scroll_offset;
1492 let ex = m
1493 .positions
1494 .get(byte_to_char_index(&m, st.selection.end))
1495 .copied()
1496 .unwrap_or(sx)
1497 - st.scroll_offset;
1498
1499 let th = locals::theme();
1500 let selection = mul_alpha_color(th.focus, 85.0 / 255.0);
1501 scene.nodes.push(SceneNode::Rect {
1502 rect: repose_core::Rect {
1503 x: inner.x + sx.max(0.0),
1504 y: inner.y,
1505 w: (ex - sx).max(0.0),
1506 h: inner.h,
1507 },
1508 brush: Brush::Solid(selection),
1509 radius: 0.0,
1510 });
1511 }
1512
1513 let th = locals::theme();
1514 let txt_col = if st.text.is_empty() {
1515 th.on_surface_variant
1516 } else {
1517 th.on_surface
1518 };
1519
1520 scene.nodes.push(SceneNode::Text {
1521 rect: repose_core::Rect {
1522 x: inner.x - st.scroll_offset,
1523 y: inner.y,
1524 w: inner.w,
1525 h: inner.h,
1526 },
1527 text: Arc::from(if st.text.is_empty() {
1528 hint.clone()
1529 } else {
1530 st.text.clone()
1531 }),
1532 color: txt_col,
1533 size: font_val,
1534 });
1535
1536 if st.selection.start == st.selection.end && st.caret_visible() {
1537 let cx = m
1538 .positions
1539 .get(byte_to_char_index(&m, st.selection.end))
1540 .copied()
1541 .unwrap_or(0.0)
1542 - st.scroll_offset;
1543 scene.nodes.push(SceneNode::Rect {
1544 rect: repose_core::Rect {
1545 x: inner.x + cx.max(0.0),
1546 y: inner.y,
1547 w: dp_to_px(1.0),
1548 h: inner.h,
1549 },
1550 brush: Brush::Solid(th.on_surface),
1551 radius: 0.0,
1552 });
1553 }
1554 } else {
1555 let layout = crate::textfield::layout_text_area(
1556 &st.text,
1557 font_val,
1558 inner.w.max(1.0),
1559 );
1560 let line_h = layout.line_h_px;
1561 let content_h = layout.ranges.len().max(1) as f32 * line_h;
1562 drop(st);
1563
1564 {
1566 let mut st = state_rc.borrow_mut();
1567 st.clamp_scroll(content_h);
1568 }
1569
1570 let st = state_rc.borrow();
1572 let th = locals::theme();
1573 let selection = mul_alpha_color(th.focus, 85.0 / 255.0);
1574 if st.text.is_empty() {
1575 scene.nodes.push(SceneNode::Text {
1576 rect: repose_core::Rect {
1577 x: inner.x,
1578 y: inner.y,
1579 w: inner.w,
1580 h: inner.h,
1581 },
1582 text: Arc::from(hint.clone()),
1583 color: th.on_surface_variant,
1584 size: font_val,
1585 });
1586 } else {
1587 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1588 let ln = &st.text[s..e];
1589 let draw_y = inner.y + (i as f32) * line_h - st.scroll_offset_y;
1590 if draw_y + line_h < inner.y - 1.0
1591 || draw_y > inner.y + inner.h + 1.0
1592 {
1593 continue;
1594 }
1595 scene.nodes.push(SceneNode::Text {
1596 rect: repose_core::Rect {
1597 x: inner.x,
1598 y: draw_y,
1599 w: inner.w,
1600 h: line_h,
1601 },
1602 text: Arc::<str>::from(ln.to_string()),
1603 color: locals::theme().on_surface,
1604 size: font_val,
1605 });
1606 }
1607 }
1608
1609 if st.selection.start != st.selection.end {
1611 let (sel_a, sel_b) = if st.selection.start < st.selection.end {
1612 (st.selection.start, st.selection.end)
1613 } else {
1614 (st.selection.end, st.selection.start)
1615 };
1616 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1617 let os = sel_a.max(s);
1618 let oe = sel_b.min(e);
1619 if os >= oe {
1620 continue;
1621 }
1622 let ln = &st.text[s..e];
1623 let m = measure_text(ln, font_val);
1624
1625 let ls = os - s;
1626 let le = oe - s;
1627
1628 let sx = m
1629 .positions
1630 .get(byte_to_char_index(&m, ls))
1631 .copied()
1632 .unwrap_or(0.0);
1633 let ex = m
1634 .positions
1635 .get(byte_to_char_index(&m, le))
1636 .copied()
1637 .unwrap_or(sx);
1638
1639 let draw_y = inner.y + (i as f32) * line_h - st.scroll_offset_y;
1640 scene.nodes.push(SceneNode::Rect {
1641 rect: repose_core::Rect {
1642 x: inner.x + sx,
1643 y: draw_y,
1644 w: (ex - sx).max(0.0),
1645 h: line_h,
1646 },
1647 brush: Brush::Solid(selection),
1648 radius: 0.0,
1649 });
1650 }
1651 }
1652
1653 if st.selection.start == st.selection.end && st.caret_visible() {
1655 let caret = st.selection.end.min(st.text.len());
1656 let (cx, cy, _li) = crate::textfield::caret_xy_for_byte(
1657 &st.text,
1658 font_val,
1659 inner.w.max(1.0),
1660 caret,
1661 );
1662 let draw_x = inner.x + cx;
1663 let draw_y = inner.y + cy - st.scroll_offset_y;
1664 scene.nodes.push(SceneNode::Rect {
1665 rect: repose_core::Rect {
1666 x: draw_x,
1667 y: draw_y,
1668 w: dp_to_px(1.0),
1669 h: line_h,
1670 },
1671 brush: Brush::Solid(th.on_surface),
1672 radius: 0.0,
1673 });
1674 }
1675 }
1676 } else {
1677 let th = locals::theme();
1678 scene.nodes.push(SceneNode::Text {
1679 rect: inner,
1680 text: Arc::from(hint.clone()),
1681 color: th.on_surface_variant,
1682 size: font_px(TF_FONT_DP),
1683 });
1684 }
1685
1686 scene.nodes.push(SceneNode::PopClip);
1687
1688 sems.push(SemNode {
1689 id: view_id,
1690 parent: sem_parent,
1691 role: Role::TextField,
1692 label: Some(hint.clone()),
1693 rect,
1694 focused: is_focused,
1695 enabled: true,
1696 });
1697 next_sem_parent = Some(view_id);
1698 }
1699 ViewKind::Checkbox { checked, on_change } => {
1700 let th = locals::theme();
1701 let sz = dp_to_px(18.0);
1702 let bx = rect.x;
1703 let by = rect.y + (rect.h - sz) * 0.5;
1704 scene.nodes.push(SceneNode::Rect {
1705 rect: repose_core::Rect {
1706 x: bx,
1707 y: by,
1708 w: sz,
1709 h: sz,
1710 },
1711 brush: Brush::Solid(if *checked { th.primary } else { th.surface }),
1712 radius: dp_to_px(3.0),
1713 });
1714 scene.nodes.push(SceneNode::Border {
1715 rect: repose_core::Rect {
1716 x: bx,
1717 y: by,
1718 w: sz,
1719 h: sz,
1720 },
1721 color: th.outline,
1722 width: dp_to_px(1.0),
1723 radius: dp_to_px(3.0),
1724 });
1725 if *checked {
1726 scene.nodes.push(SceneNode::Text {
1727 rect: repose_core::Rect {
1728 x: bx + dp_to_px(3.0),
1729 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
1730 w: sz,
1731 h: font_px(16.0),
1732 },
1733 text: Arc::from("✓"),
1734 color: th.on_primary,
1735 size: font_px(16.0),
1736 });
1737 }
1738 let toggled = !*checked;
1739 let on_click = on_change.as_ref().map(|cb| {
1740 let cb = cb.clone();
1741 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1742 });
1743 hits.push(HitRegion {
1744 id: view_id,
1745 rect,
1746 on_click,
1747 on_scroll: None,
1748 focusable: true,
1749 on_pointer_down: None,
1750 on_pointer_move: None,
1751 on_pointer_up: None,
1752 on_pointer_enter: None,
1753 on_pointer_leave: None,
1754 z_index: modifier.z_index,
1755 on_text_change: None,
1756 on_text_submit: None,
1757 tf_state_key: None,
1758 tf_multiline: false,
1759 on_action: modifier.on_action.clone(),
1760 cursor: modifier.cursor,
1761
1762 on_drag_start: modifier.on_drag_start.clone(),
1763 on_drag_end: modifier.on_drag_end.clone(),
1764 on_drag_enter: modifier.on_drag_enter.clone(),
1765 on_drag_over: modifier.on_drag_over.clone(),
1766 on_drag_leave: modifier.on_drag_leave.clone(),
1767 on_drop: modifier.on_drop.clone(),
1768 });
1769 sems.push(SemNode {
1770 id: view_id,
1771 parent: sem_parent,
1772 role: Role::Checkbox,
1773 label: None,
1774 rect,
1775 focused: is_focused,
1776 enabled: true,
1777 });
1778 next_sem_parent = Some(view_id);
1779 if is_focused {
1780 scene.nodes.push(SceneNode::Border {
1781 rect,
1782 color: th.focus,
1783 width: dp_to_px(2.0),
1784 radius: dp_to_px(6.0),
1785 });
1786 }
1787 }
1788 ViewKind::RadioButton {
1789 selected,
1790 on_select,
1791 } => {
1792 let th = locals::theme();
1793 let d = dp_to_px(18.0);
1794 let cy = rect.y + (rect.h - d) * 0.5;
1795 scene.nodes.push(SceneNode::Border {
1796 rect: repose_core::Rect {
1797 x: rect.x,
1798 y: cy,
1799 w: d,
1800 h: d,
1801 },
1802 color: th.outline,
1803 width: dp_to_px(1.5),
1804 radius: d * 0.5,
1805 });
1806 if *selected {
1807 scene.nodes.push(SceneNode::Rect {
1808 rect: repose_core::Rect {
1809 x: rect.x + dp_to_px(4.0),
1810 y: cy + dp_to_px(4.0),
1811 w: d - dp_to_px(8.0),
1812 h: d - dp_to_px(8.0),
1813 },
1814 brush: Brush::Solid(th.primary),
1815 radius: (d - dp_to_px(8.0)) * 0.5,
1816 });
1817 }
1818 if !modifier.hit_passthrough {
1819 hits.push(HitRegion {
1820 id: view_id,
1821 rect,
1822 on_click: on_select.clone(),
1823 on_scroll: None,
1824 focusable: true,
1825 on_pointer_down: None,
1826 on_pointer_move: None,
1827 on_pointer_up: None,
1828 on_pointer_enter: None,
1829 on_pointer_leave: None,
1830 z_index: modifier.z_index,
1831 on_text_change: None,
1832 on_text_submit: None,
1833 tf_state_key: None,
1834 tf_multiline: false,
1835 on_action: modifier.on_action.clone(),
1836 cursor: modifier.cursor,
1837
1838 on_drag_start: modifier.on_drag_start.clone(),
1839 on_drag_end: modifier.on_drag_end.clone(),
1840 on_drag_enter: modifier.on_drag_enter.clone(),
1841 on_drag_over: modifier.on_drag_over.clone(),
1842 on_drag_leave: modifier.on_drag_leave.clone(),
1843 on_drop: modifier.on_drop.clone(),
1844 });
1845 }
1846 sems.push(SemNode {
1847 id: view_id,
1848 parent: sem_parent,
1849 role: Role::RadioButton,
1850 label: None,
1851 rect,
1852 focused: is_focused,
1853 enabled: true,
1854 });
1855 next_sem_parent = Some(view_id);
1856 if is_focused {
1857 scene.nodes.push(SceneNode::Border {
1858 rect,
1859 color: th.focus,
1860 width: dp_to_px(2.0),
1861 radius: dp_to_px(6.0),
1862 });
1863 }
1864 }
1865 ViewKind::Switch { checked, on_change } => {
1866 let th = locals::theme();
1867 let tw = dp_to_px(46.0);
1868 let th_h = dp_to_px(26.0);
1869 let ty = rect.y + (rect.h - th_h) * 0.5;
1870 scene.nodes.push(SceneNode::Rect {
1871 rect: repose_core::Rect {
1872 x: rect.x,
1873 y: ty,
1874 w: tw,
1875 h: th_h,
1876 },
1877 brush: Brush::Solid(if *checked { th.primary } else { th.outline }),
1878 radius: th_h * 0.5,
1879 });
1880 let kw = dp_to_px(22.0);
1881 let kx = if *checked {
1882 rect.x + tw - kw - dp_to_px(2.0)
1883 } else {
1884 rect.x + dp_to_px(2.0)
1885 };
1886 scene.nodes.push(SceneNode::Rect {
1887 rect: repose_core::Rect {
1888 x: kx,
1889 y: ty + (th_h - kw) * 0.5,
1890 w: kw,
1891 h: kw,
1892 },
1893 brush: Brush::Solid(th.on_surface),
1894 radius: kw * 0.5,
1895 });
1896 let t = !*checked;
1897 let on_click = on_change.as_ref().map(|cb| {
1898 let cb = cb.clone();
1899 Rc::new(move || cb(t)) as Rc<dyn Fn()>
1900 });
1901 hits.push(HitRegion {
1902 id: view_id,
1903 rect,
1904 on_click,
1905 on_scroll: None,
1906 focusable: true,
1907 on_pointer_down: None,
1908 on_pointer_move: None,
1909 on_pointer_up: None,
1910 on_pointer_enter: None,
1911 on_pointer_leave: None,
1912 z_index: modifier.z_index,
1913 on_text_change: None,
1914 on_text_submit: None,
1915 tf_state_key: None,
1916 tf_multiline: false,
1917 on_action: modifier.on_action.clone(),
1918 cursor: modifier.cursor,
1919
1920 on_drag_start: modifier.on_drag_start.clone(),
1921 on_drag_end: modifier.on_drag_end.clone(),
1922 on_drag_enter: modifier.on_drag_enter.clone(),
1923 on_drag_over: modifier.on_drag_over.clone(),
1924 on_drag_leave: modifier.on_drag_leave.clone(),
1925 on_drop: modifier.on_drop.clone(),
1926 });
1927 sems.push(SemNode {
1928 id: view_id,
1929 parent: sem_parent,
1930 role: Role::Switch,
1931 label: None,
1932 rect,
1933 focused: is_focused,
1934 enabled: true,
1935 });
1936 next_sem_parent = Some(view_id);
1937 if is_focused {
1938 scene.nodes.push(SceneNode::Border {
1939 rect,
1940 color: th.focus,
1941 width: dp_to_px(2.0),
1942 radius: dp_to_px(6.0),
1943 });
1944 }
1945 }
1946 ViewKind::Slider {
1947 value,
1948 min,
1949 max,
1950 step,
1951 on_change,
1952 } => {
1953 let th = locals::theme();
1954 let th_h = dp_to_px(4.0);
1955 let kn_d = dp_to_px(20.0);
1956 let cy = rect.y + rect.h * 0.5;
1957 scene.nodes.push(SceneNode::Rect {
1958 rect: repose_core::Rect {
1959 x: rect.x,
1960 y: cy - th_h * 0.5,
1961 w: rect.w,
1962 h: th_h,
1963 },
1964 brush: Brush::Solid(th.outline),
1965 radius: th_h * 0.5,
1966 });
1967 let t = norm(*value, *min, *max).clamp(0.0, 1.0);
1968 let kx = rect.x + t * rect.w;
1969 scene.nodes.push(SceneNode::Rect {
1970 rect: repose_core::Rect {
1971 x: kx - kn_d * 0.5,
1972 y: cy - kn_d * 0.5,
1973 w: kn_d,
1974 h: kn_d,
1975 },
1976 brush: Brush::Solid(th.surface),
1977 radius: kn_d * 0.5,
1978 });
1979 scene.nodes.push(SceneNode::Border {
1980 rect: repose_core::Rect {
1981 x: kx - kn_d * 0.5,
1982 y: cy - kn_d * 0.5,
1983 w: kn_d,
1984 h: kn_d,
1985 },
1986 color: th.outline,
1987 width: dp_to_px(1.0),
1988 radius: kn_d * 0.5,
1989 });
1990 if let Some(cb) = on_change.clone() {
1991 let dragging = self.slider_dragging.clone();
1992 let id = view_id;
1993 let r = rect;
1994 let minv = *min;
1995 let maxv = *max;
1996 let stepv = *step;
1997
1998 let on_pd = {
1999 let cb = cb.clone();
2000 let dragging = dragging.clone();
2001 Rc::new(move |pe: PointerEvent| {
2002 dragging.borrow_mut().insert(id);
2003 (cb)(value_from_x(pe.position.x, r, minv, maxv, stepv));
2004 }) as Rc<dyn Fn(PointerEvent)>
2005 };
2006
2007 let on_pm = {
2008 let cb = cb.clone();
2009 let dragging = dragging.clone();
2010 Rc::new(move |pe: PointerEvent| {
2011 if !dragging.borrow().contains(&id) {
2012 return; }
2014 (cb)(value_from_x(pe.position.x, r, minv, maxv, stepv));
2015 }) as Rc<dyn Fn(PointerEvent)>
2016 };
2017
2018 let on_pu = {
2019 let dragging = dragging.clone();
2020 Rc::new(move |_pe: PointerEvent| {
2021 dragging.borrow_mut().remove(&id);
2022 }) as Rc<dyn Fn(PointerEvent)>
2023 };
2024
2025 let on_scroll = {
2027 let cb = cb.clone();
2028 let cur = *value;
2029 Rc::new(move |d: Vec2| -> Vec2 {
2030 let dir = wheel_to_steps(d.y);
2031 if dir == 0 {
2032 return d;
2033 }
2034 let next = apply_step(cur, dir, minv, maxv, stepv);
2035 if (next - cur).abs() > 1e-6 {
2036 (cb)(next);
2037 Vec2 { x: d.x, y: 0.0 }
2038 } else {
2039 d
2040 }
2041 }) as Rc<dyn Fn(Vec2) -> Vec2>
2042 };
2043
2044 hits.push(HitRegion {
2045 id: view_id,
2046 rect,
2047 on_click: None,
2048 on_scroll: Some(on_scroll),
2049 focusable: true,
2050 on_pointer_down: Some(on_pd),
2051 on_pointer_move: Some(on_pm),
2052 on_pointer_up: Some(on_pu),
2053 on_pointer_enter: modifier.on_pointer_enter.clone(),
2054 on_pointer_leave: modifier.on_pointer_leave.clone(),
2055 z_index: modifier.z_index,
2056 on_text_change: None,
2057 on_text_submit: None,
2058 tf_state_key: None,
2059 tf_multiline: false,
2060 on_action: modifier.on_action.clone(),
2061 cursor: modifier.cursor,
2062
2063 on_drag_start: modifier.on_drag_start.clone(),
2064 on_drag_end: modifier.on_drag_end.clone(),
2065 on_drag_enter: modifier.on_drag_enter.clone(),
2066 on_drag_over: modifier.on_drag_over.clone(),
2067 on_drag_leave: modifier.on_drag_leave.clone(),
2068 on_drop: modifier.on_drop.clone(),
2069 });
2070 }
2071 sems.push(SemNode {
2072 id: view_id,
2073 parent: sem_parent,
2074 role: Role::Slider,
2075 label: None,
2076 rect,
2077 focused: is_focused,
2078 enabled: true,
2079 });
2080 next_sem_parent = Some(view_id);
2081 if is_focused {
2082 scene.nodes.push(SceneNode::Border {
2083 rect,
2084 color: th.focus,
2085 width: dp_to_px(2.0),
2086 radius: dp_to_px(6.0),
2087 });
2088 }
2089 }
2090 ViewKind::RangeSlider {
2091 start,
2092 end,
2093 min,
2094 max,
2095 step,
2096 on_change,
2097 } => {
2098 let th = locals::theme();
2099 let th_h = dp_to_px(4.0);
2100 let kn_d = dp_to_px(20.0);
2101 let cy = rect.y + rect.h * 0.5;
2102 scene.nodes.push(SceneNode::Rect {
2103 rect: repose_core::Rect {
2104 x: rect.x,
2105 y: cy - th_h * 0.5,
2106 w: rect.w,
2107 h: th_h,
2108 },
2109 brush: Brush::Solid(th.outline),
2110 radius: th_h * 0.5,
2111 });
2112 let t0 = norm(*start, *min, *max).clamp(0.0, 1.0);
2113 let t1 = norm(*end, *min, *max).clamp(0.0, 1.0);
2114 let k0 = rect.x + t0 * rect.w;
2115 let k1 = rect.x + t1 * rect.w;
2116 scene.nodes.push(SceneNode::Rect {
2117 rect: repose_core::Rect {
2118 x: k0.min(k1),
2119 y: cy - th_h * 0.5,
2120 w: (k1 - k0).abs(),
2121 h: th_h,
2122 },
2123 brush: Brush::Solid(th.primary),
2124 radius: th_h * 0.5,
2125 });
2126 scene.nodes.push(SceneNode::Rect {
2127 rect: repose_core::Rect {
2128 x: k0 - kn_d * 0.5,
2129 y: cy - kn_d * 0.5,
2130 w: kn_d,
2131 h: kn_d,
2132 },
2133 brush: Brush::Solid(th.surface),
2134 radius: kn_d * 0.5,
2135 });
2136 scene.nodes.push(SceneNode::Rect {
2137 rect: repose_core::Rect {
2138 x: k1 - kn_d * 0.5,
2139 y: cy - kn_d * 0.5,
2140 w: kn_d,
2141 h: kn_d,
2142 },
2143 brush: Brush::Solid(th.surface),
2144 radius: kn_d * 0.5,
2145 });
2146 sems.push(SemNode {
2147 id: view_id,
2148 parent: sem_parent,
2149 role: Role::Slider,
2150 label: None,
2151 rect,
2152 focused: is_focused,
2153 enabled: true,
2154 });
2155 next_sem_parent = Some(view_id);
2156 if is_focused {
2157 scene.nodes.push(SceneNode::Border {
2158 rect,
2159 color: th.focus,
2160 width: dp_to_px(2.0),
2161 radius: dp_to_px(6.0),
2162 });
2163 }
2164
2165 if let Some(cb) = on_change.clone() {
2166 let dragging = self.slider_dragging.clone();
2167 let active = self.range_active_thumb.clone();
2168
2169 let id = view_id;
2170 let r = rect;
2171 let minv = *min;
2172 let maxv = *max;
2173 let stepv = *step;
2174
2175 let start0 = *start;
2176 let end0 = *end;
2177
2178 let on_pd = {
2179 let cb = cb.clone();
2180 let dragging = dragging.clone();
2181 let active = active.clone();
2182 Rc::new(move |pe: PointerEvent| {
2183 dragging.borrow_mut().insert(id);
2184
2185 let v = value_from_x(pe.position.x, r, minv, maxv, stepv);
2187 let use_end = (v - end0).abs() < (v - start0).abs();
2188 active.borrow_mut().insert(id, use_end);
2189
2190 let (mut a, mut b) = (start0, end0);
2191 if use_end {
2192 b = v.max(a);
2193 } else {
2194 a = v.min(b);
2195 }
2196 (cb)(a, b);
2197 }) as Rc<dyn Fn(PointerEvent)>
2198 };
2199
2200 let on_pm = {
2201 let cb = cb.clone();
2202 let dragging = dragging.clone();
2203 let active = active.clone();
2204 Rc::new(move |pe: PointerEvent| {
2205 if !dragging.borrow().contains(&id) {
2206 return;
2207 }
2208 let v = value_from_x(pe.position.x, r, minv, maxv, stepv);
2209 let use_end = *active.borrow().get(&id).unwrap_or(&false);
2210
2211 let (mut a, mut b) = (start0, end0);
2212 if use_end {
2213 b = v.max(a);
2214 } else {
2215 a = v.min(b);
2216 }
2217 (cb)(a, b);
2218 }) as Rc<dyn Fn(PointerEvent)>
2219 };
2220
2221 let on_pu = {
2222 let dragging = dragging.clone();
2223 let active = active.clone();
2224 Rc::new(move |_pe: PointerEvent| {
2225 dragging.borrow_mut().remove(&id);
2226 active.borrow_mut().remove(&id);
2227 }) as Rc<dyn Fn(PointerEvent)>
2228 };
2229
2230 let on_scroll = {
2232 let cb = cb.clone();
2233 let cur_a = *start;
2234 let cur_b = *end;
2235 let active_map = active.clone();
2236
2237 Rc::new(move |d: Vec2| -> Vec2 {
2238 let dir = wheel_to_steps(d.y);
2239 if dir == 0 {
2240 return d;
2241 }
2242
2243 let use_end = *active_map.borrow().get(&id).unwrap_or(&true);
2245
2246 let (mut a, mut b) = (cur_a, cur_b);
2247 if use_end {
2248 b = apply_step(b, dir, minv, maxv, stepv).max(a);
2249 } else {
2250 a = apply_step(a, dir, minv, maxv, stepv).min(b);
2251 }
2252
2253 if (a - cur_a).abs() > 1e-6 || (b - cur_b).abs() > 1e-6 {
2254 (cb)(a, b);
2255 Vec2 { x: d.x, y: 0.0 }
2256 } else {
2257 d
2258 }
2259 }) as Rc<dyn Fn(Vec2) -> Vec2>
2260 };
2261
2262 hits.push(HitRegion {
2263 id: view_id,
2264 rect,
2265 on_click: None,
2266 on_scroll: Some(on_scroll),
2267 focusable: true,
2268 on_pointer_down: Some(on_pd),
2269 on_pointer_move: Some(on_pm),
2270 on_pointer_up: Some(on_pu),
2271 on_pointer_enter: modifier.on_pointer_enter.clone(),
2272 on_pointer_leave: modifier.on_pointer_leave.clone(),
2273 z_index: modifier.z_index,
2274 on_text_change: None,
2275 on_text_submit: None,
2276 tf_state_key: None,
2277 tf_multiline: false,
2278 on_action: modifier.on_action.clone(),
2279 cursor: modifier.cursor,
2280
2281 on_drag_start: modifier.on_drag_start.clone(),
2282 on_drag_end: modifier.on_drag_end.clone(),
2283 on_drag_enter: modifier.on_drag_enter.clone(),
2284 on_drag_over: modifier.on_drag_over.clone(),
2285 on_drag_leave: modifier.on_drag_leave.clone(),
2286 on_drop: modifier.on_drop.clone(),
2287 });
2288 }
2289 }
2290 ViewKind::ProgressBar {
2291 value, min, max, ..
2292 } => {
2293 let th = locals::theme();
2294 let th_h = dp_to_px(6.0);
2295 let cy = rect.y + rect.h * 0.5;
2296 scene.nodes.push(SceneNode::Rect {
2297 rect: repose_core::Rect {
2298 x: rect.x,
2299 y: cy - th_h * 0.5,
2300 w: rect.w,
2301 h: th_h,
2302 },
2303 brush: Brush::Solid(th.outline),
2304 radius: th_h * 0.5,
2305 });
2306 let t = norm(*value, *min, *max).clamp(0.0, 1.0);
2307 scene.nodes.push(SceneNode::Rect {
2308 rect: repose_core::Rect {
2309 x: rect.x,
2310 y: cy - th_h * 0.5,
2311 w: rect.w * t,
2312 h: th_h,
2313 },
2314 brush: Brush::Solid(th.primary),
2315 radius: th_h * 0.5,
2316 });
2317 sems.push(SemNode {
2318 id: view_id,
2319 parent: sem_parent,
2320 role: Role::ProgressBar,
2321 label: None,
2322 rect,
2323 focused: is_focused,
2324 enabled: true,
2325 });
2326 next_sem_parent = Some(view_id);
2327 }
2328 _ => {}
2329 }
2330
2331 let child_offset_px = base_px;
2333 match &kind {
2334 ViewKind::ScrollV {
2335 on_scroll,
2336 set_viewport_height,
2337 set_content_height,
2338 get_scroll_offset,
2339 set_scroll_offset,
2340 } => {
2341 hits.push(HitRegion {
2342 id: view_id,
2343 rect,
2344 on_click: None,
2345 on_scroll: on_scroll.clone(),
2346 focusable: false,
2347 on_pointer_down: modifier.on_pointer_down.clone(),
2348 on_pointer_move: modifier.on_pointer_move.clone(),
2349 on_pointer_up: modifier.on_pointer_up.clone(),
2350 on_pointer_enter: modifier.on_pointer_enter.clone(),
2351 on_pointer_leave: modifier.on_pointer_leave.clone(),
2352 z_index: modifier.z_index,
2353 on_text_change: None,
2354 on_text_submit: None,
2355 tf_state_key: None,
2356 tf_multiline: false,
2357 on_action: modifier.on_action.clone(),
2358 cursor: modifier.cursor,
2359
2360 on_drag_start: modifier.on_drag_start.clone(),
2361 on_drag_end: modifier.on_drag_end.clone(),
2362 on_drag_enter: modifier.on_drag_enter.clone(),
2363 on_drag_over: modifier.on_drag_over.clone(),
2364 on_drag_leave: modifier.on_drag_leave.clone(),
2365 on_drop: modifier.on_drop.clone(),
2366 });
2367 let vp = content_rect;
2368 if let Some(s) = set_viewport_height {
2369 s(vp.h.max(0.0));
2370 }
2371 let mut ch = 0.0f32;
2372 for &c in &children {
2373 let l = self.taffy.layout(self.taffy_map[&c]).unwrap();
2374 ch = ch.max(l.location.y + l.size.height);
2375 }
2376 if let Some(s) = set_content_height {
2377 s(ch);
2378 }
2379 let off = get_scroll_offset.as_ref().map(|f| f()).unwrap_or(0.0);
2380
2381 scene.nodes.push(SceneNode::PushClip {
2382 rect: vp,
2383 radius: 0.0,
2384 });
2385
2386 let hits_start = hits.len();
2387 let scrolled_offset = (child_offset_px.0, child_offset_px.1 - off);
2388
2389 for &child_id in &children {
2391 let l = self.taffy.layout(self.taffy_map[&child_id]).unwrap();
2392 let child_rect = repose_core::Rect {
2393 x: scrolled_offset.0 + l.location.x,
2394 y: scrolled_offset.1 + l.location.y,
2395 w: l.size.width,
2396 h: l.size.height,
2397 };
2398 if intersect_rect(child_rect, vp).is_none() {
2399 self.stats.paint_culled += 1;
2400 continue;
2401 }
2402
2403 self.walk_paint(
2404 child_id,
2405 scene,
2406 hits,
2407 sems,
2408 textfield_states,
2409 interactions,
2410 focused,
2411 scrolled_offset,
2412 alpha_accum,
2413 next_sem_parent,
2414 font_px,
2415 allow_cache,
2416 deferred,
2417 skip_defer,
2418 );
2419 }
2420
2421 let mut i = hits_start;
2423 while i < hits.len() {
2424 if let Some(r) = intersect_rect(hits[i].rect, vp) {
2425 hits[i].rect = r;
2426 i += 1;
2427 } else {
2428 hits.remove(i);
2429 }
2430 }
2431
2432 push_scrollbar_v(
2433 scene,
2434 hits,
2435 interactions,
2436 view_id,
2437 vp,
2438 ch,
2439 off,
2440 modifier.z_index,
2441 set_scroll_offset.clone(),
2442 );
2443
2444 scene.nodes.push(SceneNode::PopClip);
2445 }
2446 ViewKind::ScrollXY {
2447 on_scroll,
2448 set_viewport_width,
2449 set_viewport_height,
2450 set_content_width,
2451 set_content_height,
2452 get_scroll_offset_xy,
2453 set_scroll_offset_xy,
2454 } => {
2455 hits.push(HitRegion {
2456 id: view_id,
2457 rect,
2458 on_click: None,
2459 on_scroll: on_scroll.clone(),
2460 focusable: false,
2461 on_pointer_down: modifier.on_pointer_down.clone(),
2462 on_pointer_move: modifier.on_pointer_move.clone(),
2463 on_pointer_up: modifier.on_pointer_up.clone(),
2464 on_pointer_enter: modifier.on_pointer_enter.clone(),
2465 on_pointer_leave: modifier.on_pointer_leave.clone(),
2466 z_index: modifier.z_index,
2467 on_text_change: None,
2468 on_text_submit: None,
2469 tf_state_key: None,
2470 tf_multiline: false,
2471 on_action: modifier.on_action.clone(),
2472 cursor: modifier.cursor,
2473
2474 on_drag_start: modifier.on_drag_start.clone(),
2475 on_drag_end: modifier.on_drag_end.clone(),
2476 on_drag_enter: modifier.on_drag_enter.clone(),
2477 on_drag_over: modifier.on_drag_over.clone(),
2478 on_drag_leave: modifier.on_drag_leave.clone(),
2479 on_drop: modifier.on_drop.clone(),
2480 });
2481 let vp = content_rect;
2482 if let Some(s) = set_viewport_width {
2483 s(vp.w.max(0.0));
2484 }
2485 if let Some(s) = set_viewport_height {
2486 s(vp.h.max(0.0));
2487 }
2488 let mut cw = 0.0f32;
2489 let mut ch = 0.0f32;
2490 for &c in &children {
2491 let l = self.taffy.layout(self.taffy_map[&c]).unwrap();
2492 cw = cw.max(l.location.x + l.size.width);
2493 ch = ch.max(l.location.y + l.size.height);
2494 }
2495 if let Some(s) = set_content_width {
2496 s(cw);
2497 }
2498 if let Some(s) = set_content_height {
2499 s(ch);
2500 }
2501 let (ox, oy) = get_scroll_offset_xy
2502 .as_ref()
2503 .map(|f| f())
2504 .unwrap_or((0.0, 0.0));
2505
2506 scene.nodes.push(SceneNode::PushClip {
2507 rect: vp,
2508 radius: 0.0,
2509 });
2510 let hits_start = hits.len();
2511 let scrolled_offset = (child_offset_px.0 - ox, child_offset_px.1 - oy);
2512 for &child_id in &children {
2513 self.walk_paint(
2514 child_id,
2515 scene,
2516 hits,
2517 sems,
2518 textfield_states,
2519 interactions,
2520 focused,
2521 scrolled_offset,
2522 alpha_accum,
2523 next_sem_parent,
2524 font_px,
2525 allow_cache,
2526 deferred,
2527 skip_defer,
2528 );
2529 }
2530 let mut i = hits_start;
2531 while i < hits.len() {
2532 if let Some(r) = intersect_rect(hits[i].rect, vp) {
2533 hits[i].rect = r;
2534 i += 1;
2535 } else {
2536 hits.remove(i);
2537 }
2538 }
2539 let set_y = set_scroll_offset_xy.clone().map(|s| {
2540 let ox = ox;
2541 Rc::new(move |y| s(ox, y)) as Rc<dyn Fn(f32)>
2542 });
2543 push_scrollbar_v(
2544 scene,
2545 hits,
2546 interactions,
2547 view_id,
2548 vp,
2549 ch,
2550 oy,
2551 modifier.z_index,
2552 set_y,
2553 );
2554 push_scrollbar_h(
2555 scene,
2556 hits,
2557 interactions,
2558 view_id,
2559 vp,
2560 cw,
2561 ox,
2562 modifier.z_index,
2563 set_scroll_offset_xy.clone(),
2564 oy,
2565 );
2566 scene.nodes.push(SceneNode::PopClip);
2567 }
2568 ViewKind::OverlayHost => {
2569 for &child_id in &children {
2570 self.walk_paint(
2571 child_id,
2572 scene,
2573 hits,
2574 sems,
2575 textfield_states,
2576 interactions,
2577 focused,
2578 child_offset_px,
2579 alpha_accum,
2580 next_sem_parent,
2581 font_px,
2582 allow_cache,
2583 deferred,
2584 skip_defer,
2585 );
2586 }
2587 }
2588 _ => {
2589 for &child_id in &children {
2590 self.walk_paint(
2591 child_id,
2592 scene,
2593 hits,
2594 sems,
2595 textfield_states,
2596 interactions,
2597 focused,
2598 child_offset_px,
2599 alpha_accum,
2600 next_sem_parent,
2601 font_px,
2602 allow_cache,
2603 deferred,
2604 skip_defer,
2605 );
2606 }
2607 }
2608 }
2609
2610 if push_round_clip {
2611 scene.nodes.push(SceneNode::PopClip);
2612 }
2613 if modifier.transform.is_some() {
2614 scene.nodes.push(SceneNode::PopTransform);
2615 }
2616 }
2617}
2618
2619fn infer_label(tree: &ViewTree, node_id: NodeId) -> Option<String> {
2621 let mut stack = vec![node_id];
2622 while let Some(id) = stack.pop() {
2623 let n = tree.get(id)?;
2624 if let ViewKind::Text { text, .. } = &n.kind {
2625 if !text.is_empty() {
2626 return Some(text.clone());
2627 }
2628 }
2629 for &ch in n.children.iter().rev() {
2630 stack.push(ch);
2631 }
2632 }
2633 None
2634}
2635
2636fn intersect_rect(a: repose_core::Rect, b: repose_core::Rect) -> Option<repose_core::Rect> {
2637 let x0 = a.x.max(b.x);
2638 let y0 = a.y.max(b.y);
2639 let x1 = (a.x + a.w).min(b.x + b.w);
2640 let y1 = (a.y + a.h).min(b.y + b.h);
2641 let w = (x1 - x0).max(0.0);
2642 let h = (y1 - y0).max(0.0);
2643 if w <= 0.0 || h <= 0.0 {
2644 None
2645 } else {
2646 Some(repose_core::Rect { x: x0, y: y0, w, h })
2647 }
2648}
2649
2650fn mul_alpha_color(c: Color, a: f32) -> Color {
2651 Color(c.0, c.1, c.2, ((c.3 as f32) * a).clamp(0.0, 255.0) as u8)
2652}
2653fn mul_alpha_brush(b: Brush, a: f32) -> Brush {
2654 match b {
2655 Brush::Solid(c) => Brush::Solid(mul_alpha_color(c, a)),
2656 Brush::Linear {
2657 start,
2658 end,
2659 start_color,
2660 end_color,
2661 } => Brush::Linear {
2662 start,
2663 end,
2664 start_color: mul_alpha_color(start_color, a),
2665 end_color: mul_alpha_color(end_color, a),
2666 },
2667 }
2668}
2669
2670fn clamp_radius(r: f32, w: f32, h: f32) -> f32 {
2671 r.max(0.0).min(0.5 * w.max(0.0)).min(0.5 * h.max(0.0))
2672}
2673fn norm(v: f32, min: f32, max: f32) -> f32 {
2674 if max > min {
2675 (v - min) / (max - min)
2676 } else {
2677 0.0
2678 }
2679}
2680
2681fn push_scrollbar_v(
2682 scene: &mut Scene,
2683 hits: &mut Vec<HitRegion>,
2684 interactions: &Interactions,
2685 vid: u64,
2686 vp: repose_core::Rect,
2687 ch: f32,
2688 off: f32,
2689 z: f32,
2690 set: Option<Rc<dyn Fn(f32)>>,
2691) {
2692 if ch <= vp.h + 0.5 {
2693 return;
2694 }
2695 let thick = dp_to_px(6.0);
2696 let m = dp_to_px(2.0);
2697 let tx = vp.x + vp.w - m - thick;
2698 let ty = vp.y + m;
2699 let th = (vp.h - 2.0 * m).max(0.0);
2700 if th <= 0.5 {
2701 return;
2702 }
2703 let ratio = (vp.h / ch).clamp(0.0, 1.0);
2704 let thumb_h = (th * ratio).max(dp_to_px(24.0)).min(th);
2705 let tpos = (off / (ch - vp.h).max(1.0)).clamp(0.0, 1.0);
2706 let thumb_y = ty + tpos * (th - thumb_h);
2707 let color = locals::theme().scrollbar_thumb;
2708
2709 scene.nodes.push(SceneNode::Rect {
2710 rect: repose_core::Rect {
2711 x: tx,
2712 y: ty,
2713 w: thick,
2714 h: th,
2715 },
2716 brush: Brush::Solid(locals::theme().scrollbar_track),
2717 radius: thick * 0.5,
2718 });
2719 scene.nodes.push(SceneNode::Rect {
2720 rect: repose_core::Rect {
2721 x: tx,
2722 y: thumb_y,
2723 w: thick,
2724 h: thumb_h,
2725 },
2726 brush: Brush::Solid(color),
2727 radius: thick * 0.5,
2728 });
2729
2730 if let Some(s) = set {
2731 let tid = vid ^ 0x8000_0001;
2732 let map = Rc::new(move |py: f32| -> f32 {
2733 let max_p = (th - thumb_h).max(0.0);
2734 let p = ((py - ty) - thumb_h * 0.5).clamp(0.0, max_p);
2735 (if max_p > 0.0 { p / max_p } else { 0.0 }) * (ch - vp.h).max(1.0)
2736 });
2737 let on_pd = {
2738 let s = s.clone();
2739 let m = map.clone();
2740 Rc::new(move |pe: PointerEvent| s(m(pe.position.y)))
2741 };
2742 let on_pm = if interactions.pressed.contains(&tid) {
2743 let s = s.clone();
2744 let m = map.clone();
2745 Some(Rc::new(move |pe: PointerEvent| s(m(pe.position.y))) as Rc<dyn Fn(PointerEvent)>)
2746 } else {
2747 None
2748 };
2749 hits.push(HitRegion {
2750 id: tid,
2751 rect: repose_core::Rect {
2752 x: tx,
2753 y: thumb_y,
2754 w: thick,
2755 h: thumb_h,
2756 },
2757 on_click: None,
2758 on_scroll: None,
2759 focusable: false,
2760 on_pointer_down: Some(on_pd),
2761 on_pointer_move: on_pm,
2762 on_pointer_up: Some(Rc::new(|_| {})),
2763 on_pointer_enter: None,
2764 on_action: None,
2765 cursor: None,
2766 tf_multiline: false,
2767
2768 on_drag_start: None,
2769 on_drag_end: None,
2770 on_drag_enter: None,
2771 on_drag_over: None,
2772 on_drag_leave: None,
2773 on_drop: None,
2774
2775 on_pointer_leave: None,
2776 z_index: z + 1000.0,
2777 on_text_change: None,
2778 on_text_submit: None,
2779 tf_state_key: None,
2780 });
2781 }
2782}
2783
2784fn push_scrollbar_h(
2785 scene: &mut Scene,
2786 hits: &mut Vec<HitRegion>,
2787 interactions: &Interactions,
2788 vid: u64,
2789 vp: repose_core::Rect,
2790 cw: f32,
2791 off: f32,
2792 z: f32,
2793 set: Option<Rc<dyn Fn(f32, f32)>>,
2794 keep_y: f32,
2795) {
2796 if cw <= vp.w + 0.5 {
2797 return;
2798 }
2799 let thick = dp_to_px(6.0);
2800 let m = dp_to_px(2.0);
2801 let tx = vp.x + m;
2802 let ty = vp.y + vp.h - m - thick;
2803 let tw = (vp.w - 2.0 * m).max(0.0);
2804 if tw <= 0.5 {
2805 return;
2806 }
2807 let ratio = (vp.w / cw).clamp(0.0, 1.0);
2808 let thumb_w = (tw * ratio).max(dp_to_px(24.0)).min(tw);
2809 let tpos = (off / (cw - vp.w).max(1.0)).clamp(0.0, 1.0);
2810 let thumb_x = tx + tpos * (tw - thumb_w);
2811 let color = locals::theme().scrollbar_thumb;
2812
2813 scene.nodes.push(SceneNode::Rect {
2814 rect: repose_core::Rect {
2815 x: tx,
2816 y: ty,
2817 w: tw,
2818 h: thick,
2819 },
2820 brush: Brush::Solid(locals::theme().scrollbar_track),
2821 radius: thick * 0.5,
2822 });
2823 scene.nodes.push(SceneNode::Rect {
2824 rect: repose_core::Rect {
2825 x: thumb_x,
2826 y: ty,
2827 w: thumb_w,
2828 h: thick,
2829 },
2830 brush: Brush::Solid(color),
2831 radius: thick * 0.5,
2832 });
2833 if let Some(s) = set {
2834 let tid = vid ^ 0x8000_0002;
2835 let map = Rc::new(move |px: f32| -> f32 {
2836 let max_p = (tw - thumb_w).max(0.0);
2837 let p = ((px - tx) - thumb_w * 0.5).clamp(0.0, max_p);
2838 (if max_p > 0.0 { p / max_p } else { 0.0 }) * (cw - vp.w).max(1.0)
2839 });
2840 let on_pd = {
2841 let s = s.clone();
2842 let m = map.clone();
2843 Rc::new(move |pe: PointerEvent| s(m(pe.position.x), keep_y))
2844 };
2845 let on_pm = if interactions.pressed.contains(&tid) {
2846 let s = s.clone();
2847 let m = map.clone();
2848 Some(Rc::new(move |pe: PointerEvent| s(m(pe.position.x), keep_y))
2849 as Rc<dyn Fn(PointerEvent)>)
2850 } else {
2851 None
2852 };
2853 hits.push(HitRegion {
2854 id: tid,
2855 rect: repose_core::Rect {
2856 x: thumb_x,
2857 y: ty,
2858 w: thumb_w,
2859 h: thick,
2860 },
2861 on_click: None,
2862 on_scroll: None,
2863 focusable: false,
2864 on_pointer_down: Some(on_pd),
2865 on_pointer_move: on_pm,
2866 on_pointer_up: Some(Rc::new(|_| {})),
2867 on_pointer_enter: None,
2868 on_action: None,
2869 cursor: None,
2870 tf_multiline: false,
2871
2872 on_drag_start: None,
2873 on_drag_end: None,
2874 on_drag_enter: None,
2875 on_drag_over: None,
2876 on_drag_leave: None,
2877 on_drop: None,
2878
2879 on_pointer_leave: None,
2880 z_index: z + 1000.0,
2881 on_text_change: None,
2882 on_text_submit: None,
2883 tf_state_key: None,
2884 });
2885 }
2886}
2887
2888fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
2889 let v = v.clamp(min, max);
2890 if let Some(s) = step.filter(|s| *s > 0.0) {
2891 let t = ((v - min) / s).round();
2892 (min + t * s).clamp(min, max)
2893 } else {
2894 v
2895 }
2896}
2897
2898fn value_from_x(x: f32, rect: repose_core::Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
2899 let w = rect.w.max(1.0);
2900 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
2901 let v = min + t * (max - min);
2902 snap_step(v, min, max, step)
2903}
2904
2905fn wheel_to_steps(dy_px: f32) -> i32 {
2906 if dy_px < -0.5 {
2907 1
2908 } else if dy_px > 0.5 {
2909 -1
2910 } else {
2911 0
2912 }
2913}
2914
2915fn apply_step(v: f32, dir: i32, min: f32, max: f32, step: Option<f32>) -> f32 {
2916 if dir == 0 {
2917 return v;
2918 }
2919 let s = step.unwrap_or(1.0).max(1e-6);
2920 snap_step(v + (dir as f32) * s, min, max, step)
2921}
2922
2923#[cfg(test)]
2924mod tests {
2925 use super::*;
2926 use crate::{Box as RBox, Column, Stack, ViewExt};
2927
2928 fn font_px(dp: f32) -> f32 {
2929 dp }
2931
2932 #[test]
2933 fn test_render_z_index_paints_last() {
2934 let red = Color::from_rgb(255, 0, 0);
2939 let blue = Color::from_rgb(0, 0, 255);
2940
2941 let red_box = RBox(Modifier::new().size(100.0, 100.0).background(red));
2942 let blue_box = RBox(
2943 Modifier::new()
2944 .size(100.0, 100.0)
2945 .background(blue)
2946 .render_z_index(100.0),
2947 );
2948
2949 let root = Stack(Modifier::new().size(200.0, 200.0)).child((red_box, blue_box));
2950
2951 let mut engine = LayoutEngine::new();
2952 let (scene, _hits, _sems) = engine.layout_frame(
2953 &root,
2954 (200, 200),
2955 &HashMap::new(),
2956 &Interactions::default(),
2957 None,
2958 );
2959
2960 let rects: Vec<_> = scene
2962 .nodes
2963 .iter()
2964 .filter_map(|n| {
2965 if let SceneNode::Rect { brush, .. } = n {
2966 Some(brush.clone())
2967 } else {
2968 None
2969 }
2970 })
2971 .collect();
2972
2973 assert!(
2974 rects.len() >= 2,
2975 "Expected at least 2 rect nodes, got {}",
2976 rects.len()
2977 );
2978
2979 let last_rect_brush = rects.last().unwrap();
2982 assert!(
2983 matches!(last_rect_brush, Brush::Solid(c) if *c == blue),
2984 "Expected blue box to be painted last, but got {:?}",
2985 last_rect_brush
2986 );
2987
2988 let second_to_last = rects.get(rects.len() - 2);
2990 assert!(second_to_last.is_some(), "Expected at least 2 rect nodes");
2991 let second_brush = second_to_last.unwrap();
2992 assert!(
2993 matches!(second_brush, Brush::Solid(c) if *c == red),
2994 "Expected red box to be painted before blue, but got {:?}",
2995 second_brush
2996 );
2997 }
2998
2999 #[test]
3000 fn test_render_z_index_order_by_value() {
3001 let red = Color::from_rgb(255, 0, 0);
3004 let green = Color::from_rgb(0, 255, 0);
3005 let blue = Color::from_rgb(0, 0, 255);
3006
3007 let box1 = RBox(
3008 Modifier::new()
3009 .size(50.0, 50.0)
3010 .background(red)
3011 .render_z_index(10.0),
3012 );
3013 let box2 = RBox(
3014 Modifier::new()
3015 .size(50.0, 50.0)
3016 .background(green)
3017 .render_z_index(20.0),
3018 );
3019 let box3 = RBox(
3020 Modifier::new()
3021 .size(50.0, 50.0)
3022 .background(blue)
3023 .render_z_index(5.0),
3024 );
3025
3026 let root = Stack(Modifier::new().size(200.0, 200.0)).child((box1, box2, box3));
3027
3028 let mut engine = LayoutEngine::new();
3029 let (scene, _hits, _sems) = engine.layout_frame(
3030 &root,
3031 (200, 200),
3032 &HashMap::new(),
3033 &Interactions::default(),
3034 None,
3035 );
3036
3037 let rects: Vec<_> = scene
3038 .nodes
3039 .iter()
3040 .filter_map(|n| {
3041 if let SceneNode::Rect { brush, .. } = n {
3042 Some(brush.clone())
3043 } else {
3044 None
3045 }
3046 })
3047 .collect();
3048
3049 assert!(rects.len() >= 3, "Expected at least 3 rects");
3051
3052 let len = rects.len();
3053 assert!(
3054 matches!(&rects[len - 3], Brush::Solid(c) if *c == blue),
3055 "Expected BLUE (z=5) third from last"
3056 );
3057 assert!(
3058 matches!(&rects[len - 2], Brush::Solid(c) if *c == red),
3059 "Expected RED (z=10) second from last"
3060 );
3061 assert!(
3062 matches!(&rects[len - 1], Brush::Solid(c) if *c == green),
3063 "Expected GREEN (z=20) last"
3064 );
3065 }
3066
3067 #[test]
3068 fn test_render_z_index_with_nested_children() {
3069 let red = Color::from_rgb(255, 0, 0);
3077 let green = Color::from_rgb(0, 255, 0);
3078 let blue = Color::from_rgb(0, 0, 255);
3079
3080 let red_box = RBox(Modifier::new().size(50.0, 50.0).background(red));
3081 let green_box = RBox(Modifier::new().size(50.0, 50.0).background(green));
3082
3083 let content = Column(Modifier::new()).child((red_box, green_box));
3084
3085 let overlay = RBox(
3086 Modifier::new()
3087 .size(30.0, 30.0)
3088 .background(blue)
3089 .render_z_index(1000.0),
3090 );
3091
3092 let root = Stack(Modifier::new().size(200.0, 200.0)).child((content, overlay));
3093
3094 let mut engine = LayoutEngine::new();
3095 let (scene, _hits, _sems) = engine.layout_frame(
3096 &root,
3097 (200, 200),
3098 &HashMap::new(),
3099 &Interactions::default(),
3100 None,
3101 );
3102
3103 let rects: Vec<_> = scene
3104 .nodes
3105 .iter()
3106 .filter_map(|n| {
3107 if let SceneNode::Rect { brush, .. } = n {
3108 Some(brush.clone())
3109 } else {
3110 None
3111 }
3112 })
3113 .collect();
3114
3115 assert!(
3117 rects.len() >= 3,
3118 "Expected at least 3 rects, got {}",
3119 rects.len()
3120 );
3121
3122 let len = rects.len();
3123 assert!(
3125 matches!(&rects[len - 1], Brush::Solid(c) if *c == blue),
3126 "Expected BLUE (z=1000) to be painted last, but got {:?}",
3127 &rects[len - 1]
3128 );
3129
3130 let blue_pos = rects
3133 .iter()
3134 .position(|b| matches!(b, Brush::Solid(c) if *c == blue))
3135 .unwrap();
3136 let red_pos = rects
3137 .iter()
3138 .position(|b| matches!(b, Brush::Solid(c) if *c == red))
3139 .unwrap();
3140 let green_pos = rects
3141 .iter()
3142 .position(|b| matches!(b, Brush::Solid(c) if *c == green))
3143 .unwrap();
3144
3145 assert!(red_pos < blue_pos, "Red should be painted before blue");
3146 assert!(green_pos < blue_pos, "Green should be painted before blue");
3147 }
3148
3149 #[test]
3150 fn test_render_z_index_paints_over_scrollbars() {
3151 use crate::Scroll;
3158
3159 let content_color = Color::from_rgb(100, 100, 100);
3160 let overlay_color = Color::from_rgb(0, 0, 255);
3161
3162 let tall_content = RBox(Modifier::new().size(180.0, 500.0).background(content_color));
3164
3165 let scroll = Scroll(Modifier::new().size(200.0, 200.0)).child(tall_content);
3166
3167 let overlay = RBox(
3168 Modifier::new()
3169 .size(50.0, 50.0)
3170 .background(overlay_color)
3171 .render_z_index(1000.0),
3172 );
3173
3174 let root = Stack(Modifier::new().size(200.0, 200.0)).child((scroll, overlay));
3175
3176 let mut engine = LayoutEngine::new();
3177 let (scene, _hits, _sems) = engine.layout_frame(
3178 &root,
3179 (200, 200),
3180 &HashMap::new(),
3181 &Interactions::default(),
3182 None,
3183 );
3184
3185 let rects: Vec<_> = scene
3187 .nodes
3188 .iter()
3189 .filter_map(|n| {
3190 if let SceneNode::Rect { brush, .. } = n {
3191 Some(brush.clone())
3192 } else {
3193 None
3194 }
3195 })
3196 .collect();
3197
3198 let overlay_pos = rects
3200 .iter()
3201 .position(|b| matches!(b, Brush::Solid(c) if *c == overlay_color));
3202 assert!(
3203 overlay_pos.is_some(),
3204 "Overlay should be present in scene, rects: {:?}",
3205 rects
3206 );
3207 let overlay_pos = overlay_pos.unwrap();
3208
3209 assert_eq!(
3211 overlay_pos,
3212 rects.len() - 1,
3213 "Overlay should be the last rect, but it's at position {} of {}. Rects: {:?}",
3214 overlay_pos,
3215 rects.len(),
3216 rects
3217 );
3218 }
3219
3220 #[test]
3221 fn test_render_z_index_with_overlay_host() {
3222 use crate::Scroll;
3228 use crate::overlay::OverlayHandle;
3229
3230 let content_color = Color::from_rgb(100, 100, 100);
3231 let overlay_color = Color::from_rgb(0, 0, 255);
3232
3233 let tall_content = RBox(Modifier::new().size(180.0, 500.0).background(content_color));
3235 let scroll = Scroll(Modifier::new().size(200.0, 200.0)).child(tall_content);
3236
3237 let overlay_handle = OverlayHandle::new();
3239 let overlay_host = overlay_handle.host(Modifier::new().fill_max_size(), scroll);
3240
3241 let hint_box = RBox(
3243 Modifier::new()
3244 .size(50.0, 50.0)
3245 .background(overlay_color)
3246 .render_z_index(1000.0),
3247 );
3248
3249 let root = Stack(Modifier::new().size(200.0, 200.0)).child((overlay_host, hint_box));
3251
3252 let mut engine = LayoutEngine::new();
3253 let (scene, _hits, _sems) = engine.layout_frame(
3254 &root,
3255 (200, 200),
3256 &HashMap::new(),
3257 &Interactions::default(),
3258 None,
3259 );
3260
3261 let rects: Vec<_> = scene
3263 .nodes
3264 .iter()
3265 .filter_map(|n| {
3266 if let SceneNode::Rect { brush, .. } = n {
3267 Some(brush.clone())
3268 } else {
3269 None
3270 }
3271 })
3272 .collect();
3273
3274 let overlay_pos = rects
3276 .iter()
3277 .position(|b| matches!(b, Brush::Solid(c) if *c == overlay_color));
3278 assert!(
3279 overlay_pos.is_some(),
3280 "Hint box should be present in scene, rects: {:?}",
3281 rects
3282 );
3283 let overlay_pos = overlay_pos.unwrap();
3284
3285 assert_eq!(
3287 overlay_pos,
3288 rects.len() - 1,
3289 "Hint box should be the last rect, but it's at position {} of {}. Rects: {:?}",
3290 overlay_pos,
3291 rects.len(),
3292 rects
3293 );
3294 }
3295}