1#![allow(non_snake_case)]
6pub mod anim;
7pub mod anim_ext;
8pub mod canvas;
9pub mod gestures;
10pub mod lazy;
11pub mod material3;
12pub mod navigation;
13pub mod scroll;
14
15use std::collections::{HashMap, HashSet};
16use std::rc::Rc;
17use std::{cell::RefCell, cmp::Ordering};
18
19use repose_core::*;
20use taffy::style::{AlignItems, Dimension, Display, FlexDirection, JustifyContent, Style};
21use taffy::{Overflow, Point, ResolveOrZero};
22
23use taffy::prelude::{Position, Size, auto, length, percent};
24
25pub mod textfield;
26pub use textfield::{TextField, TextFieldState};
27
28use crate::textfield::{TF_FONT_PX, TF_PADDING_X, byte_to_char_index, measure_text, positions_for};
29use repose_core::locals;
30
31#[derive(Default)]
32pub struct Interactions {
33 pub hover: Option<u64>,
34 pub pressed: HashSet<u64>,
35}
36
37pub fn Surface(modifier: Modifier, child: View) -> View {
38 let mut v = View::new(0, ViewKind::Surface).modifier(modifier);
39 v.children = vec![child];
40 v
41}
42
43pub fn Box(modifier: Modifier) -> View {
44 View::new(0, ViewKind::Box).modifier(modifier)
45}
46
47pub fn Row(modifier: Modifier) -> View {
48 View::new(0, ViewKind::Row).modifier(modifier)
49}
50
51pub fn Column(modifier: Modifier) -> View {
52 View::new(0, ViewKind::Column).modifier(modifier)
53}
54
55pub fn Stack(modifier: Modifier) -> View {
56 View::new(0, ViewKind::Stack).modifier(modifier)
57}
58
59pub fn Scroll(modifier: Modifier) -> View {
60 View::new(
61 0,
62 ViewKind::ScrollV {
63 on_scroll: None,
64 set_viewport_height: None,
65 set_content_height: None,
66 get_scroll_offset: None,
67 set_scroll_offset: None,
68 },
69 )
70 .modifier(modifier)
71}
72
73pub fn Text(text: impl Into<String>) -> View {
74 View::new(
75 0,
76 ViewKind::Text {
77 text: text.into(),
78 color: Color::WHITE,
79 font_size: 16.0,
80 soft_wrap: false,
81 max_lines: None,
82 overflow: TextOverflow::Visible,
83 },
84 )
85}
86
87pub fn Spacer() -> View {
88 Box(Modifier::new().flex_grow(1.0))
89}
90
91pub fn Grid(columns: usize, modifier: Modifier, children: Vec<View>) -> View {
92 Column(modifier.grid(columns, 0.0, 0.0)).with_children(children)
93}
94
95#[allow(non_snake_case)]
96pub fn TextColor(mut v: View, color: Color) -> View {
97 if let ViewKind::Text {
98 color: text_color, ..
99 } = &mut v.kind
100 {
101 *text_color = color;
102 }
103 v
104}
105
106#[allow(non_snake_case)]
107pub fn TextSize(mut v: View, size: f32) -> View {
108 if let ViewKind::Text {
109 font_size: text_size,
110 ..
111 } = &mut v.kind
112 {
113 *text_size = size;
114 }
115 v
116}
117
118pub fn Button(text: impl Into<String>, on_click: impl Fn() + 'static) -> View {
119 View::new(
120 0,
121 ViewKind::Button {
122 text: text.into(),
123 on_click: Some(Rc::new(on_click)),
124 },
125 )
126 .semantics(Semantics {
127 role: Role::Button,
128 label: None,
129 focused: false,
130 enabled: true,
131 })
132}
133
134pub fn Checkbox(
135 checked: bool,
136 label: impl Into<String>,
137 on_change: impl Fn(bool) + 'static,
138) -> View {
139 View::new(
140 0,
141 ViewKind::Checkbox {
142 checked,
143 label: label.into(),
144 on_change: Some(Rc::new(on_change)),
145 },
146 )
147 .semantics(Semantics {
148 role: Role::Checkbox,
149 label: None,
150 focused: false,
151 enabled: true,
152 })
153}
154
155pub fn RadioButton(
156 selected: bool,
157 label: impl Into<String>,
158 on_select: impl Fn() + 'static,
159) -> View {
160 View::new(
161 0,
162 ViewKind::RadioButton {
163 selected,
164 label: label.into(),
165 on_select: Some(Rc::new(on_select)),
166 },
167 )
168 .semantics(Semantics {
169 role: Role::RadioButton,
170 label: None,
171 focused: false,
172 enabled: true,
173 })
174}
175
176pub fn Switch(checked: bool, label: impl Into<String>, on_change: impl Fn(bool) + 'static) -> View {
177 View::new(
178 0,
179 ViewKind::Switch {
180 checked,
181 label: label.into(),
182 on_change: Some(Rc::new(on_change)),
183 },
184 )
185 .semantics(Semantics {
186 role: Role::Switch,
187 label: None,
188 focused: false,
189 enabled: true,
190 })
191}
192
193pub fn Slider(
194 value: f32,
195 range: (f32, f32),
196 step: Option<f32>,
197 label: impl Into<String>,
198 on_change: impl Fn(f32) + 'static,
199) -> View {
200 View::new(
201 0,
202 ViewKind::Slider {
203 value,
204 min: range.0,
205 max: range.1,
206 step,
207 label: label.into(),
208 on_change: Some(Rc::new(on_change)),
209 },
210 )
211 .semantics(Semantics {
212 role: Role::Slider,
213 label: None,
214 focused: false,
215 enabled: true,
216 })
217}
218
219pub fn RangeSlider(
220 start: f32,
221 end: f32,
222 range: (f32, f32),
223 step: Option<f32>,
224 label: impl Into<String>,
225 on_change: impl Fn(f32, f32) + 'static,
226) -> View {
227 View::new(
228 0,
229 ViewKind::RangeSlider {
230 start,
231 end,
232 min: range.0,
233 max: range.1,
234 step,
235 label: label.into(),
236 on_change: Some(Rc::new(on_change)),
237 },
238 )
239 .semantics(Semantics {
240 role: Role::Slider,
241 label: None,
242 focused: false,
243 enabled: true,
244 })
245}
246
247pub fn ProgressBar(value: f32, range: (f32, f32), label: impl Into<String>) -> View {
248 View::new(
249 0,
250 ViewKind::ProgressBar {
251 value,
252 min: range.0,
253 max: range.1,
254 label: label.into(),
255 circular: false,
256 },
257 )
258 .semantics(Semantics {
259 role: Role::ProgressBar,
260 label: None,
261 focused: false,
262 enabled: true,
263 })
264}
265
266pub fn Image(modifier: Modifier, handle: ImageHandle) -> View {
267 View::new(
268 0,
269 ViewKind::Image {
270 handle,
271 tint: Color::WHITE,
272 fit: ImageFit::Contain,
273 },
274 )
275 .modifier(modifier)
276}
277
278pub trait ImageExt {
279 fn image_tint(self, c: Color) -> View;
280 fn image_fit(self, fit: ImageFit) -> View;
281}
282impl ImageExt for View {
283 fn image_tint(mut self, c: Color) -> View {
284 if let ViewKind::Image { tint, .. } = &mut self.kind {
285 *tint = c;
286 }
287 self
288 }
289 fn image_fit(mut self, fit: ImageFit) -> View {
290 if let ViewKind::Image { fit: f, .. } = &mut self.kind {
291 *f = fit;
292 }
293 self
294 }
295}
296
297fn flex_dir_for(kind: &ViewKind) -> Option<FlexDirection> {
298 match kind {
299 ViewKind::Row => {
300 if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
301 Some(FlexDirection::RowReverse)
302 } else {
303 Some(FlexDirection::Row)
304 }
305 }
306 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => {
307 Some(FlexDirection::Column)
308 }
309 _ => None,
310 }
311}
312
313pub trait ViewExt: Sized {
315 fn child(self, children: impl IntoChildren) -> Self;
316}
317
318impl ViewExt for View {
319 fn child(self, children: impl IntoChildren) -> Self {
320 self.with_children(children.into_children())
321 }
322}
323
324pub trait IntoChildren {
325 fn into_children(self) -> Vec<View>;
326}
327
328impl IntoChildren for View {
329 fn into_children(self) -> Vec<View> {
330 vec![self]
331 }
332}
333
334impl IntoChildren for Vec<View> {
335 fn into_children(self) -> Vec<View> {
336 self
337 }
338}
339
340impl<const N: usize> IntoChildren for [View; N] {
341 fn into_children(self) -> Vec<View> {
342 self.into()
343 }
344}
345
346macro_rules! impl_into_children_tuple {
348 ($($idx:tt $t:ident),+) => {
349 impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
350 fn into_children(self) -> Vec<View> {
351 let mut v = Vec::new();
352 $(v.extend(self.$idx.into_children());)+
353 v
354 }
355 }
356 };
357}
358
359impl_into_children_tuple!(0 A, 1 B);
360impl_into_children_tuple!(0 A, 1 B, 2 C);
361impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
362impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
363impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
364impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
365impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
366
367pub fn layout_and_paint(
369 root: &View,
370 size: (u32, u32),
371 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
372 interactions: &Interactions,
373 focused: Option<u64>,
374) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
375 let mut id = 1u64;
377 fn stamp(mut v: View, id: &mut u64) -> View {
378 v.id = *id;
379 *id += 1;
380 v.children = v.children.into_iter().map(|c| stamp(c, id)).collect();
381 v
382 }
383 let root = stamp(root.clone(), &mut id);
384
385 use taffy::prelude::*;
387 #[derive(Clone)]
388 enum NodeCtx {
389 Text {
390 text: String,
391 font_px: f32,
392 soft_wrap: bool,
393 max_lines: Option<usize>,
394 overflow: TextOverflow,
395 },
396 Button {
397 label: String,
398 },
399 TextField,
400 Container,
401 ScrollContainer,
402 Checkbox {
403 label: String,
404 },
405 Radio {
406 label: String,
407 },
408 Switch {
409 label: String,
410 },
411 Slider {
412 label: String,
413 },
414 Range {
415 label: String,
416 },
417 Progress {
418 label: String,
419 },
420 }
421
422 let mut taffy: TaffyTree<NodeCtx> = TaffyTree::new();
423 let mut nodes_map = HashMap::new();
424
425 #[derive(Clone)]
426 struct TextLayout {
427 lines: Vec<String>,
428 size_px: f32,
429 line_h: f32,
430 }
431 use std::collections::HashMap as StdHashMap;
432 let mut text_cache: StdHashMap<taffy::NodeId, TextLayout> = StdHashMap::new();
433
434 fn style_from_modifier(m: &Modifier, kind: &ViewKind) -> Style {
435 use taffy::prelude::*;
436 let mut s = Style::default();
437
438 s.display = match kind {
440 ViewKind::Row => Display::Flex,
441 ViewKind::Column
442 | ViewKind::Surface
443 | ViewKind::ScrollV { .. }
444 | ViewKind::ScrollXY { .. } => Display::Flex,
445 ViewKind::Stack => Display::Grid,
446 _ => Display::Flex,
447 };
448
449 if matches!(kind, ViewKind::Row) {
451 s.flex_direction =
452 if crate::locals::text_direction() == crate::locals::TextDirection::Rtl {
453 FlexDirection::RowReverse
454 } else {
455 FlexDirection::Row
456 };
457 }
458 if matches!(
459 kind,
460 ViewKind::Column
461 | ViewKind::Surface
462 | ViewKind::ScrollV { .. }
463 | ViewKind::ScrollXY { .. }
464 ) {
465 s.flex_direction = FlexDirection::Column;
466 }
467
468 s.align_items = if matches!(
470 kind,
471 ViewKind::Column
472 | ViewKind::Surface
473 | ViewKind::ScrollV { .. }
474 | ViewKind::ScrollXY { .. }
475 ) {
476 Some(AlignItems::Stretch)
477 } else {
478 Some(AlignItems::FlexStart)
479 };
480 s.justify_content = Some(JustifyContent::FlexStart);
481
482 if let Some(r) = m.aspect_ratio {
484 s.aspect_ratio = Some(r.max(0.0));
485 }
486
487 if let Some(g) = m.flex_grow {
489 s.flex_grow = g;
490 }
491 if let Some(sh) = m.flex_shrink {
492 s.flex_shrink = sh;
493 }
494 if let Some(b) = m.flex_basis {
495 s.flex_basis = length(b.max(0.0));
496 }
497
498 if let Some(a) = m.align_self {
500 s.align_self = Some(a);
501 }
502
503 if let Some(crate::modifier::PositionType::Absolute) = m.position_type {
505 s.position = Position::Absolute;
506 s.inset = taffy::geometry::Rect {
507 left: m.offset_left.map(length).unwrap_or_else(auto),
508 right: m.offset_right.map(length).unwrap_or_else(auto),
509 top: m.offset_top.map(length).unwrap_or_else(auto),
510 bottom: m.offset_bottom.map(length).unwrap_or_else(auto),
511 };
512 }
513
514 if let Some(cfg) = &m.grid {
516 s.display = Display::Grid;
517 s.grid_template_columns = (0..cfg.columns.max(1))
518 .map(|_| GridTemplateComponent::Single(flex(1.0)))
519 .collect();
520 s.gap = Size {
521 width: length(cfg.column_gap),
522 height: length(cfg.row_gap),
523 };
524 }
525
526 if matches!(kind, ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. }) {
528 s.overflow = Point {
529 x: Overflow::Hidden,
530 y: Overflow::Hidden,
531 };
532 }
533
534 if let Some(pv) = m.padding_values {
536 s.padding = taffy::geometry::Rect {
537 left: length(pv.left),
538 right: length(pv.right),
539 top: length(pv.top),
540 bottom: length(pv.bottom),
541 };
542 } else if let Some(p) = m.padding {
543 let v = length(p);
544 s.padding = taffy::geometry::Rect {
545 left: v,
546 right: v,
547 top: v,
548 bottom: v,
549 };
550 }
551
552 let mut width_set = false;
554 let mut height_set = false;
555 if let Some(sz) = m.size {
556 if sz.width.is_finite() {
557 s.size.width = length(sz.width.max(0.0));
558 width_set = true;
559 }
560 if sz.height.is_finite() {
561 s.size.height = length(sz.height.max(0.0));
562 height_set = true;
563 }
564 }
565 if let Some(w) = m.width {
566 s.size.width = length(w.max(0.0));
567 width_set = true;
568 }
569 if let Some(h) = m.height {
570 s.size.height = length(h.max(0.0));
571 height_set = true;
572 }
573
574 let is_row = matches!(kind, ViewKind::Row);
576 let is_column = matches!(
577 kind,
578 ViewKind::Column
579 | ViewKind::Surface
580 | ViewKind::ScrollV { .. }
581 | ViewKind::ScrollXY { .. }
582 );
583
584 let want_fill_w = m.fill_max || m.fill_max_w;
585 let want_fill_h = m.fill_max || m.fill_max_h;
586
587 if is_column {
589 if want_fill_h && !height_set {
591 s.flex_grow = s.flex_grow.max(1.0);
592 s.flex_shrink = s.flex_shrink.max(1.0);
593 s.flex_basis = length(0.0);
594 s.min_size.height = length(0.0); }
596 if want_fill_w && !width_set {
597 s.min_size.width = percent(1.0);
598 s.max_size.width = percent(1.0);
599 }
600 } else if is_row {
601 if want_fill_w && !width_set {
603 s.flex_grow = s.flex_grow.max(1.0);
604 s.flex_shrink = s.flex_shrink.max(1.0);
605 s.flex_basis = length(0.0);
606 s.min_size.width = length(0.0);
607 }
608 if want_fill_h && !height_set {
609 s.min_size.height = percent(1.0);
610 s.max_size.height = percent(1.0);
611 }
612 } else {
613 if want_fill_h && !height_set {
615 s.flex_grow = s.flex_grow.max(1.0);
616 s.flex_shrink = s.flex_shrink.max(1.0);
617 s.flex_basis = length(0.0);
618 s.min_size.height = length(0.0);
619 }
620 if want_fill_w && !width_set {
621 s.min_size.width = percent(1.0);
622 s.max_size.width = percent(1.0);
623 }
624 }
625
626 if matches!(kind, ViewKind::Surface) {
627 if (m.fill_max || m.fill_max_w) && s.min_size.width.is_auto() && !width_set {
628 s.min_size.width = percent(1.0);
629 s.max_size.width = percent(1.0);
630 }
631 if (m.fill_max || m.fill_max_h) && s.min_size.height.is_auto() && !height_set {
632 s.min_size.height = percent(1.0);
633 s.max_size.height = percent(1.0);
634 }
635 }
636
637 if let Some(v) = m.min_width {
639 s.min_size.width = length(v.max(0.0));
640 }
641 if let Some(v) = m.min_height {
642 s.min_size.height = length(v.max(0.0));
643 }
644 if let Some(v) = m.max_width {
645 s.max_size.width = length(v.max(0.0));
646 }
647 if let Some(v) = m.max_height {
648 s.max_size.height = length(v.max(0.0));
649 }
650
651 s
652 }
653
654 fn build_node(
655 v: &View,
656 t: &mut TaffyTree<NodeCtx>,
657 nodes_map: &mut HashMap<ViewId, taffy::NodeId>,
658 ) -> taffy::NodeId {
659 let mut style = style_from_modifier(&v.modifier, &v.kind);
660
661 if v.modifier.grid_col_span.is_some() || v.modifier.grid_row_span.is_some() {
662 use taffy::prelude::{GridPlacement, Line};
663
664 let col_span = v.modifier.grid_col_span.unwrap_or(1).max(1);
665 let row_span = v.modifier.grid_row_span.unwrap_or(1).max(1);
666
667 style.grid_column = Line {
668 start: GridPlacement::Auto,
669 end: GridPlacement::Span(col_span),
670 };
671 style.grid_row = Line {
672 start: GridPlacement::Auto,
673 end: GridPlacement::Span(row_span),
674 };
675 }
676
677 let children: Vec<_> = v
678 .children
679 .iter()
680 .map(|c| build_node(c, t, nodes_map))
681 .collect();
682
683 let node = match &v.kind {
684 ViewKind::Text {
685 text,
686 font_size,
687 soft_wrap,
688 max_lines,
689 overflow,
690 ..
691 } => t
692 .new_leaf_with_context(
693 style,
694 NodeCtx::Text {
695 text: text.clone(),
696 font_px: *font_size,
697 soft_wrap: *soft_wrap,
698 max_lines: *max_lines,
699 overflow: *overflow,
700 },
701 )
702 .unwrap(),
703 ViewKind::Button { text, .. } => t
704 .new_leaf_with_context(
705 style,
706 NodeCtx::Button {
707 label: text.clone(),
708 },
709 )
710 .unwrap(),
711 ViewKind::TextField { .. } => {
712 t.new_leaf_with_context(style, NodeCtx::TextField).unwrap()
713 }
714 ViewKind::Image { .. } => t.new_leaf_with_context(style, NodeCtx::Container).unwrap(),
715 ViewKind::Checkbox { label, .. } => t
716 .new_leaf_with_context(
717 style,
718 NodeCtx::Checkbox {
719 label: label.clone(),
720 },
721 )
722 .unwrap(),
723 ViewKind::RadioButton { label, .. } => t
724 .new_leaf_with_context(
725 style,
726 NodeCtx::Radio {
727 label: label.clone(),
728 },
729 )
730 .unwrap(),
731 ViewKind::Switch { label, .. } => t
732 .new_leaf_with_context(
733 style,
734 NodeCtx::Switch {
735 label: label.clone(),
736 },
737 )
738 .unwrap(),
739 ViewKind::Slider { label, .. } => t
740 .new_leaf_with_context(
741 style,
742 NodeCtx::Slider {
743 label: label.clone(),
744 },
745 )
746 .unwrap(),
747 ViewKind::RangeSlider { label, .. } => t
748 .new_leaf_with_context(
749 style,
750 NodeCtx::Range {
751 label: label.clone(),
752 },
753 )
754 .unwrap(),
755 ViewKind::ProgressBar { label, .. } => t
756 .new_leaf_with_context(
757 style,
758 NodeCtx::Progress {
759 label: label.clone(),
760 },
761 )
762 .unwrap(),
763 ViewKind::ScrollV { .. } => {
764 let children: Vec<_> = v
765 .children
766 .iter()
767 .map(|c| build_node(c, t, nodes_map))
768 .collect();
769
770 let n = t.new_with_children(style, &children).unwrap();
771 t.set_node_context(n, Some(NodeCtx::ScrollContainer)).ok();
772 n
773 }
774 _ => {
775 let n = t.new_with_children(style, &children).unwrap();
776 t.set_node_context(n, Some(NodeCtx::Container)).ok();
777 n
778 }
779 };
780
781 nodes_map.insert(v.id, node);
782 node
783 }
784
785 let root_node = build_node(&root, &mut taffy, &mut nodes_map);
786
787 {
788 let mut rs = taffy.style(root_node).unwrap().clone();
789 rs.size.width = length(size.0 as f32);
790 rs.size.height = length(size.1 as f32);
791 taffy.set_style(root_node, rs).unwrap();
792 }
793
794 let available = taffy::geometry::Size {
795 width: AvailableSpace::Definite(size.0 as f32),
796 height: AvailableSpace::Definite(size.1 as f32),
797 };
798
799 taffy
801 .compute_layout_with_measure(root_node, available, |known, avail, node, ctx, _style| {
802 match ctx {
803 Some(NodeCtx::Text {
804 text,
805 font_px,
806 soft_wrap,
807 max_lines,
808 overflow,
809 }) => {
810 let scale = locals::text_scale().0;
812 let size_px = *font_px * scale;
813 let line_h = size_px * 1.3;
814
815 let approx_w = text.len() as f32 * *font_px * 0.6;
817 let measured_w = known.width.unwrap_or(approx_w);
818
819 let wrap_w = if *soft_wrap {
821 match avail.width {
822 AvailableSpace::Definite(w) => w,
823 _ => measured_w,
824 }
825 } else {
826 measured_w
827 };
828
829 let lines_vec: Vec<String> = if *soft_wrap {
831 let (ls, _trunc) =
832 repose_text::wrap_lines(text, size_px, wrap_w, *max_lines, true);
833 ls
834 } else {
835 match overflow {
836 TextOverflow::Ellipsis => {
837 vec![repose_text::ellipsize_line(text, size_px, wrap_w)]
838 }
839 _ => vec![text.clone()],
840 }
841 };
842 text_cache.insert(
843 node,
844 TextLayout {
845 lines: lines_vec.clone(),
846 size_px,
847 line_h,
848 },
849 );
850 let n_lines = lines_vec.len().max(1);
851
852 taffy::geometry::Size {
853 width: measured_w,
854 height: line_h * n_lines as f32,
855 }
856 }
857 Some(NodeCtx::Button { label }) => taffy::geometry::Size {
858 width: (label.len() as f32 * 16.0 * 0.6) + 24.0,
859 height: 36.0,
860 },
861 Some(NodeCtx::TextField) => {
862 let w = known.width.unwrap_or(220.0);
863 taffy::geometry::Size {
864 width: w,
865 height: 36.0,
866 }
867 }
868 Some(NodeCtx::Checkbox { label }) => {
869 let label_w = (label.len() as f32) * 16.0 * 0.6;
870 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
872 width: known.width.unwrap_or(w),
873 height: 24.0,
874 }
875 }
876 Some(NodeCtx::Radio { label }) => {
877 let label_w = (label.len() as f32) * 16.0 * 0.6;
878 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
880 width: known.width.unwrap_or(w),
881 height: 24.0,
882 }
883 }
884 Some(NodeCtx::Switch { label }) => {
885 let label_w = (label.len() as f32) * 16.0 * 0.6;
886 let w = 46.0 + 8.0 + label_w; taffy::geometry::Size {
888 width: known.width.unwrap_or(w),
889 height: 28.0,
890 }
891 }
892 Some(NodeCtx::Slider { label }) => {
893 let label_w = (label.len() as f32) * 16.0 * 0.6;
894 let w = (known.width).unwrap_or(200.0f32.max(46.0 + 8.0 + label_w));
895 taffy::geometry::Size {
896 width: w,
897 height: 28.0,
898 }
899 }
900 Some(NodeCtx::Range { label }) => {
901 let label_w = (label.len() as f32) * 16.0 * 0.6;
902 let w = (known.width).unwrap_or(220.0f32.max(46.0 + 8.0 + label_w));
903 taffy::geometry::Size {
904 width: w,
905 height: 28.0,
906 }
907 }
908 Some(NodeCtx::Progress { label }) => {
909 let label_w = (label.len() as f32) * 16.0 * 0.6;
910 let w = (known.width).unwrap_or(200.0f32.max(100.0 + 8.0 + label_w));
911 taffy::geometry::Size {
912 width: w,
913 height: 12.0 + 8.0,
914 } }
916 Some(NodeCtx::ScrollContainer) => {
917 taffy::geometry::Size {
918 width: known.width.unwrap_or_else(|| {
919 match avail.width {
920 AvailableSpace::Definite(w) => w,
921 _ => 300.0, }
923 }),
924 height: known.height.unwrap_or_else(|| {
925 match avail.height {
926 AvailableSpace::Definite(h) => h,
927 _ => 600.0, }
929 }),
930 }
931 }
932 Some(NodeCtx::Container) | None => taffy::geometry::Size::ZERO,
933 }
934 })
935 .unwrap();
936
937 fn layout_of(node: taffy::NodeId, t: &TaffyTree<impl Clone>) -> repose_core::Rect {
945 let l = t.layout(node).unwrap();
946 repose_core::Rect {
947 x: l.location.x,
948 y: l.location.y,
949 w: l.size.width,
950 h: l.size.height,
951 }
952 }
953
954 fn add_offset(mut r: repose_core::Rect, off: (f32, f32)) -> repose_core::Rect {
955 r.x += off.0;
956 r.y += off.1;
957 r
958 }
959
960 fn clamp01(x: f32) -> f32 {
961 x.max(0.0).min(1.0)
962 }
963 fn norm(value: f32, min: f32, max: f32) -> f32 {
964 if max > min {
965 (value - min) / (max - min)
966 } else {
967 0.0
968 }
969 }
970 fn denorm(t: f32, min: f32, max: f32) -> f32 {
971 min + t * (max - min)
972 }
973 fn snap_step(v: f32, step: Option<f32>, min: f32, max: f32) -> f32 {
974 match step {
975 Some(s) if s > 0.0 => {
976 let k = ((v - min) / s).round();
977 (min + k * s).clamp(min, max)
978 }
979 _ => v.clamp(min, max),
980 }
981 }
982 fn mul_alpha(c: Color, a: f32) -> Color {
983 let mut out = c;
984 let na = ((c.3 as f32) * a).clamp(0.0, 255.0) as u8;
985 out.3 = na;
986 out
987 }
988
989 let mut scene = Scene {
990 clear_color: locals::theme().background,
991 nodes: vec![],
992 };
993 let mut hits: Vec<HitRegion> = vec![];
994 let mut sems: Vec<SemNode> = vec![];
995
996 fn walk(
997 v: &View,
998 t: &TaffyTree<NodeCtx>,
999 nodes: &HashMap<ViewId, taffy::NodeId>,
1000 scene: &mut Scene,
1001 hits: &mut Vec<HitRegion>,
1002 sems: &mut Vec<SemNode>,
1003 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1004 interactions: &Interactions,
1005 focused: Option<u64>,
1006 parent_offset: (f32, f32),
1007 alpha_accum: f32,
1008 text_cache: &StdHashMap<taffy::NodeId, TextLayout>,
1009 ) {
1010 let local = layout_of(nodes[&v.id], t);
1011 let rect = add_offset(local, parent_offset);
1012
1013 let content_rect = {
1014 if let Some(pv) = v.modifier.padding_values {
1016 crate::Rect {
1017 x: rect.x + pv.left,
1018 y: rect.y + pv.top,
1019 w: (rect.w - pv.left - pv.right).max(0.0),
1020 h: (rect.h - pv.top - pv.bottom).max(0.0),
1021 }
1022 } else if let Some(p) = v.modifier.padding {
1023 crate::Rect {
1024 x: rect.x + p,
1025 y: rect.y + p,
1026 w: (rect.w - 2.0 * p).max(0.0),
1027 h: (rect.h - 2.0 * p).max(0.0),
1028 }
1029 } else {
1030 rect
1031 }
1032 };
1033
1034 let base = (parent_offset.0 + local.x, parent_offset.1 + local.y);
1035
1036 let is_hovered = interactions.hover == Some(v.id);
1037 let is_pressed = interactions.pressed.contains(&v.id);
1038 let is_focused = focused == Some(v.id);
1039
1040 if let Some(bg) = v.modifier.background {
1042 scene.nodes.push(SceneNode::Rect {
1043 rect,
1044 color: mul_alpha(bg, alpha_accum),
1045 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1046 });
1047 }
1048
1049 if let Some(b) = &v.modifier.border {
1051 scene.nodes.push(SceneNode::Border {
1052 rect,
1053 color: mul_alpha(b.color, alpha_accum),
1054 width: b.width,
1055 radius: b.radius.max(v.modifier.clip_rounded.unwrap_or(0.0)),
1056 });
1057 }
1058
1059 let this_alpha = v.modifier.alpha.unwrap_or(1.0);
1061 let alpha_accum = (alpha_accum * this_alpha).clamp(0.0, 1.0);
1062
1063 if let Some(tf) = v.modifier.transform {
1064 scene.nodes.push(SceneNode::PushTransform { transform: tf });
1065 }
1066
1067 if let Some(p) = &v.modifier.painter {
1069 (p)(scene, rect);
1070 }
1071
1072 let has_pointer = v.modifier.on_pointer_down.is_some()
1073 || v.modifier.on_pointer_move.is_some()
1074 || v.modifier.on_pointer_up.is_some()
1075 || v.modifier.on_pointer_enter.is_some()
1076 || v.modifier.on_pointer_leave.is_some();
1077
1078 if has_pointer || v.modifier.click {
1079 hits.push(HitRegion {
1080 id: v.id,
1081 rect,
1082 on_click: None, on_scroll: None, focusable: false,
1085 on_pointer_down: v.modifier.on_pointer_down.clone(),
1086 on_pointer_move: v.modifier.on_pointer_move.clone(),
1087 on_pointer_up: v.modifier.on_pointer_up.clone(),
1088 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1089 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1090 z_index: v.modifier.z_index,
1091 on_text_change: None,
1092 on_text_submit: None,
1093 });
1094 }
1095
1096 match &v.kind {
1097 ViewKind::Text {
1098 text,
1099 color,
1100 font_size,
1101 soft_wrap,
1102 max_lines,
1103 overflow,
1104 } => {
1105 let nid = nodes[&v.id];
1106 let tl = text_cache.get(&nid);
1107
1108 let (size, line_h, mut lines): (f32, f32, Vec<String>) = if let Some(tl) = tl {
1109 (tl.size_px, tl.line_h, tl.lines.clone())
1110 } else {
1111 let sz = *font_size * locals::text_scale().0;
1113 (sz, sz * 1.3, vec![text.clone()])
1114 };
1115 let mut draw_box = content_rect;
1117 let max_w = draw_box.w.max(0.0);
1118 let max_h = draw_box.h.max(0.0);
1119
1120 if lines.len() == 1 {
1122 let dy = (draw_box.h - size) * 0.5;
1123 if dy.is_finite() {
1124 draw_box.y += dy.max(0.0);
1125 draw_box.h = size;
1126 }
1127 }
1128
1129 let max_visual_lines = if max_h > 0.5 {
1131 (max_h / line_h).floor().max(1.0) as usize
1132 } else {
1133 usize::MAX
1134 };
1135
1136 if lines.len() > max_visual_lines {
1137 lines.truncate(max_visual_lines);
1138 if *overflow == TextOverflow::Ellipsis && max_w > 0.5 {
1139 if let Some(last) = lines.last_mut() {
1141 *last = repose_text::ellipsize_line(last, size, max_w);
1142 }
1143 }
1144 }
1145
1146 let need_clip = match overflow {
1147 TextOverflow::Visible => false,
1148 TextOverflow::Clip | TextOverflow::Ellipsis => true,
1149 };
1150
1151 if need_clip {
1152 scene.nodes.push(SceneNode::PushClip {
1153 rect: draw_box,
1154 radius: 0.0,
1155 });
1156 }
1157
1158 for (i, ln) in lines.iter().enumerate() {
1159 scene.nodes.push(SceneNode::Text {
1160 rect: crate::Rect {
1161 x: draw_box.x,
1162 y: draw_box.y + i as f32 * line_h,
1163 w: draw_box.w,
1164 h: line_h,
1165 },
1166 text: ln.clone(),
1167 color: mul_alpha(*color, alpha_accum),
1168 size: size,
1169 });
1170 }
1171
1172 if need_clip {
1173 scene.nodes.push(SceneNode::PopClip);
1174 }
1175
1176 sems.push(SemNode {
1177 id: v.id,
1178 role: Role::Text,
1179 label: Some(text.clone()),
1180 rect,
1181 focused: is_focused,
1182 enabled: true,
1183 });
1184 }
1185
1186 ViewKind::Button { text, on_click } => {
1187 if v.modifier.background.is_none() {
1189 let base = if is_pressed {
1190 Color::from_hex("#1f7556")
1191 } else if is_hovered {
1192 Color::from_hex("#2a8f6a")
1193 } else {
1194 Color::from_hex("#34af82")
1195 };
1196 scene.nodes.push(SceneNode::Rect {
1197 rect,
1198 color: mul_alpha(base, alpha_accum),
1199 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1200 });
1201 }
1202 let px = 16.0;
1204 let approx_w = (text.len() as f32) * px * 0.6;
1205 let tx = rect.x + (rect.w - approx_w).max(0.0) * 0.5;
1206 let ty = rect.y + (rect.h - px).max(0.0) * 0.5;
1207 scene.nodes.push(SceneNode::Text {
1208 rect: repose_core::Rect {
1209 x: tx,
1210 y: ty,
1211 w: approx_w,
1212 h: px,
1213 },
1214 text: text.clone(),
1215 color: mul_alpha(Color::WHITE, alpha_accum),
1216 size: px,
1217 });
1218
1219 if v.modifier.click || on_click.is_some() {
1220 hits.push(HitRegion {
1221 id: v.id,
1222 rect,
1223 on_click: on_click.clone(),
1224 on_scroll: None,
1225 focusable: true,
1226 on_pointer_down: v.modifier.on_pointer_down.clone(),
1227 on_pointer_move: v.modifier.on_pointer_move.clone(),
1228 on_pointer_up: v.modifier.on_pointer_up.clone(),
1229 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1230 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1231 z_index: v.modifier.z_index,
1232 on_text_change: None,
1233 on_text_submit: None,
1234 });
1235 }
1236 sems.push(SemNode {
1237 id: v.id,
1238 role: Role::Button,
1239 label: Some(text.clone()),
1240 rect,
1241 focused: is_focused,
1242 enabled: true,
1243 });
1244 if is_focused {
1246 scene.nodes.push(SceneNode::Border {
1247 rect,
1248 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1249 width: 2.0,
1250 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1251 });
1252 }
1253 }
1254 ViewKind::Image { handle, tint, fit } => {
1255 scene.nodes.push(SceneNode::Image {
1256 rect,
1257 handle: *handle,
1258 tint: mul_alpha(*tint, alpha_accum),
1259 fit: *fit,
1260 });
1261 }
1262
1263 ViewKind::TextField {
1264 hint,
1265 on_change,
1266 on_submit,
1267 ..
1268 } => {
1269 hits.push(HitRegion {
1270 id: v.id,
1271 rect,
1272 on_click: None,
1273 on_scroll: None,
1274 focusable: true,
1275 on_pointer_down: None,
1276 on_pointer_move: None,
1277 on_pointer_up: None,
1278 on_pointer_enter: None,
1279 on_pointer_leave: None,
1280 z_index: v.modifier.z_index,
1281 on_text_change: on_change.clone(),
1282 on_text_submit: on_submit.clone(),
1283 });
1284
1285 let inner = repose_core::Rect {
1287 x: rect.x + TF_PADDING_X,
1288 y: rect.y + 8.0,
1289 w: rect.w - 2.0 * TF_PADDING_X,
1290 h: rect.h - 16.0,
1291 };
1292 scene.nodes.push(SceneNode::PushClip {
1293 rect: inner,
1294 radius: 0.0,
1295 });
1296 if is_focused {
1298 scene.nodes.push(SceneNode::Border {
1299 rect,
1300 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1301 width: 2.0,
1302 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1303 });
1304 }
1305 if let Some(state_rc) = textfield_states.get(&v.id) {
1306 state_rc.borrow_mut().set_inner_width(inner.w);
1307
1308 let state = state_rc.borrow();
1309 let text = &state.text;
1310 let px = TF_FONT_PX as u32;
1311 let m = measure_text(text, px);
1312
1313 if state.selection.start != state.selection.end {
1315 let i0 = byte_to_char_index(&m, state.selection.start);
1316 let i1 = byte_to_char_index(&m, state.selection.end);
1317 let sx = m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1318 let ex = m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
1319 let sel_x = inner.x + sx.max(0.0);
1320 let sel_w = (ex - sx).max(0.0);
1321 scene.nodes.push(SceneNode::Rect {
1322 rect: repose_core::Rect {
1323 x: sel_x,
1324 y: inner.y,
1325 w: sel_w,
1326 h: inner.h,
1327 },
1328 color: mul_alpha(Color::from_hex("#3B7BFF55"), alpha_accum),
1329 radius: 0.0,
1330 });
1331 }
1332
1333 if let Some(range) = &state.composition {
1335 if range.start < range.end && !text.is_empty() {
1336 let i0 = byte_to_char_index(&m, range.start);
1337 let i1 = byte_to_char_index(&m, range.end);
1338 let sx =
1339 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1340 let ex =
1341 m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
1342 let ux = inner.x + sx.max(0.0);
1343 let uw = (ex - sx).max(0.0);
1344 scene.nodes.push(SceneNode::Rect {
1345 rect: repose_core::Rect {
1346 x: ux,
1347 y: inner.y + inner.h - 2.0,
1348 w: uw,
1349 h: 2.0,
1350 },
1351 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1352 radius: 0.0,
1353 });
1354 }
1355 }
1356
1357 scene.nodes.push(SceneNode::Text {
1359 rect: repose_core::Rect {
1360 x: inner.x - state.scroll_offset,
1361 y: inner.y,
1362 w: inner.w,
1363 h: inner.h,
1364 },
1365 text: if text.is_empty() {
1366 hint.clone()
1367 } else {
1368 text.clone()
1369 },
1370 color: if text.is_empty() {
1371 mul_alpha(Color::from_hex("#666666"), alpha_accum)
1372 } else {
1373 mul_alpha(Color::from_hex("#CCCCCC"), alpha_accum)
1374 },
1375 size: TF_FONT_PX,
1376 });
1377
1378 if state.selection.start == state.selection.end && state.caret_visible() {
1380 let i = byte_to_char_index(&m, state.selection.end);
1381 let cx = m.positions.get(i).copied().unwrap_or(0.0) - state.scroll_offset;
1382 let caret_x = inner.x + cx.max(0.0);
1383 scene.nodes.push(SceneNode::Rect {
1384 rect: repose_core::Rect {
1385 x: caret_x,
1386 y: inner.y,
1387 w: 1.0,
1388 h: inner.h,
1389 },
1390 color: mul_alpha(Color::WHITE, alpha_accum),
1391 radius: 0.0,
1392 });
1393 }
1394 scene.nodes.push(SceneNode::PopClip);
1396
1397 sems.push(SemNode {
1398 id: v.id,
1399 role: Role::TextField,
1400 label: Some(text.clone()),
1401 rect,
1402 focused: is_focused,
1403 enabled: true,
1404 });
1405 } else {
1406 scene.nodes.push(SceneNode::Text {
1408 rect: repose_core::Rect {
1409 x: inner.x,
1410 y: inner.y,
1411 w: inner.w,
1412 h: inner.h,
1413 },
1414 text: hint.clone(),
1415 color: mul_alpha(Color::from_hex("#666666"), alpha_accum),
1416 size: TF_FONT_PX,
1417 });
1418 sems.push(SemNode {
1419 id: v.id,
1420 role: Role::TextField,
1421 label: Some(hint.clone()),
1422 rect,
1423 focused: is_focused,
1424 enabled: true,
1425 });
1426 }
1427 }
1428 ViewKind::ScrollV {
1429 on_scroll,
1430 set_viewport_height,
1431 set_content_height,
1432 get_scroll_offset,
1433 set_scroll_offset,
1434 } => {
1435 log::debug!("ScrollV: registering hit region at rect {:?}", rect);
1436
1437 hits.push(HitRegion {
1438 id: v.id,
1439 rect,
1440 on_click: None,
1441 on_scroll: on_scroll.clone(),
1442 focusable: false,
1443 on_pointer_down: v.modifier.on_pointer_down.clone(),
1444 on_pointer_move: v.modifier.on_pointer_move.clone(),
1445 on_pointer_up: v.modifier.on_pointer_up.clone(),
1446 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1447 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1448 z_index: v.modifier.z_index,
1449 on_text_change: None,
1450 on_text_submit: None,
1451 });
1452
1453 if let Some(set_vh) = set_viewport_height {
1454 set_vh(rect.h);
1455 }
1456
1457 let mut content_h = 0.0f32;
1459 for c in &v.children {
1460 let nid = nodes[&c.id];
1461 let l = t.layout(nid).unwrap();
1462 content_h = content_h.max(l.location.y + l.size.height);
1463 }
1464 if let Some(set_ch) = set_content_height {
1465 set_ch(content_h);
1466 }
1467
1468 scene.nodes.push(SceneNode::PushClip {
1469 rect,
1470 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1471 });
1472
1473 let scroll_offset = if let Some(get) = get_scroll_offset {
1474 let offset = get();
1475 log::debug!("ScrollV walk: applying scroll offset = {}", offset);
1476 offset
1477 } else {
1478 0.0
1479 };
1480
1481 let child_offset = (base.0, base.1 - scroll_offset);
1483 for c in &v.children {
1484 walk(
1485 c,
1486 t,
1487 nodes,
1488 scene,
1489 hits,
1490 sems,
1491 textfield_states,
1492 interactions,
1493 focused,
1494 child_offset,
1495 alpha_accum,
1496 text_cache,
1497 );
1498 }
1499
1500 if content_h > rect.h + 0.5 {
1502 let thickness = 6.0f32;
1503 let margin = 2.0f32;
1504 let min_thumb = 24.0f32;
1505
1506 let track_x = rect.x + rect.w - margin - thickness;
1508 let track_y = rect.y + margin;
1509 let track_h = (rect.h - 2.0 * margin).max(0.0);
1510
1511 let ratio = (rect.h / content_h).clamp(0.0, 1.0);
1513 let mut thumb_h = (track_h * ratio).clamp(min_thumb, track_h);
1514
1515 let denom = (content_h - rect.h).max(1.0);
1517 let tpos = (scroll_offset / denom).clamp(0.0, 1.0);
1518 let max_pos = (track_h - thumb_h).max(0.0);
1519 let thumb_y = track_y + tpos * max_pos;
1520
1521 let th = locals::theme();
1523 let mut track_col = th.on_surface;
1524 track_col.3 = 32; let mut thumb_col = th.on_surface;
1526 thumb_col.3 = 140; scene.nodes.push(SceneNode::Rect {
1529 rect: crate::Rect {
1530 x: track_x,
1531 y: track_y,
1532 w: thickness,
1533 h: track_h,
1534 },
1535 color: track_col,
1536 radius: thickness * 0.5,
1537 });
1538
1539 scene.nodes.push(SceneNode::Rect {
1541 rect: crate::Rect {
1542 x: track_x,
1543 y: thumb_y,
1544 w: thickness,
1545 h: thumb_h,
1546 },
1547 color: thumb_col,
1548 radius: thickness * 0.5,
1549 });
1550
1551 if let Some(setter) = set_scroll_offset.clone() {
1552 let thumb_id: u64 = v.id ^ 0x8000_0001;
1553
1554 let map_to_off = Rc::new(move |py: f32| -> f32 {
1556 let denom = (content_h - rect.h).max(1.0);
1557 let max_pos = (track_h - thumb_h).max(0.0);
1558 let pos = ((py - track_y) - thumb_h * 0.5).clamp(0.0, max_pos);
1559 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1560 t * denom
1561 });
1562
1563 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1565 let setter = setter.clone();
1566 let map = map_to_off.clone();
1567 Rc::new(move |pe| {
1568 setter(map(pe.position.y)); })
1570 };
1571
1572 let is_pressed = interactions.pressed.contains(&thumb_id);
1574 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1575 if is_pressed {
1576 let setter = setter.clone();
1577 let map = map_to_off.clone();
1578 Some(Rc::new(move |pe| setter(map(pe.position.y))))
1579 } else {
1580 None
1581 };
1582
1583 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> =
1584 Rc::new(move |_pe| {});
1585
1586 hits.push(HitRegion {
1587 id: thumb_id,
1588 rect: crate::Rect {
1589 x: track_x,
1590 y: thumb_y,
1591 w: thickness,
1592 h: thumb_h,
1593 },
1594 on_click: None,
1595 on_scroll: None,
1596 focusable: false,
1597 on_pointer_down: Some(on_pd),
1598 on_pointer_move: on_pm,
1599 on_pointer_up: Some(on_pu),
1600 on_pointer_enter: None,
1601 on_pointer_leave: None,
1602 z_index: v.modifier.z_index + 1000.0,
1603 on_text_change: None,
1604 on_text_submit: None,
1605 });
1606 }
1607 }
1608
1609 scene.nodes.push(SceneNode::PopClip);
1610 return;
1611 }
1612 ViewKind::ScrollXY {
1613 on_scroll,
1614 set_viewport_width,
1615 set_viewport_height,
1616 set_content_width,
1617 set_content_height,
1618 get_scroll_offset_xy,
1619 set_scroll_offset_xy,
1620 } => {
1621 hits.push(HitRegion {
1622 id: v.id,
1623 rect,
1624 on_click: None,
1625 on_scroll: on_scroll.clone(),
1626 focusable: false,
1627 on_pointer_down: v.modifier.on_pointer_down.clone(),
1628 on_pointer_move: v.modifier.on_pointer_move.clone(),
1629 on_pointer_up: v.modifier.on_pointer_up.clone(),
1630 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1631 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1632 z_index: v.modifier.z_index,
1633 on_text_change: None,
1634 on_text_submit: None,
1635 });
1636
1637 if let Some(set_w) = set_viewport_width {
1638 set_w(rect.w);
1639 }
1640 if let Some(set_h) = set_viewport_height {
1641 set_h(rect.h);
1642 }
1643
1644 let mut content_w = 0.0f32;
1645 let mut content_h = 0.0f32;
1646 for c in &v.children {
1647 let nid = nodes[&c.id];
1648 let l = t.layout(nid).unwrap();
1649 content_w = content_w.max(l.location.x + l.size.width);
1650 content_h = content_h.max(l.location.y + l.size.height);
1651 }
1652 if let Some(set_cw) = set_content_width {
1653 set_cw(content_w);
1654 }
1655 if let Some(set_ch) = set_content_height {
1656 set_ch(content_h);
1657 }
1658
1659 scene.nodes.push(SceneNode::PushClip {
1661 rect,
1662 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1663 });
1664
1665 let (ox, oy) = if let Some(get) = get_scroll_offset_xy {
1667 get()
1668 } else {
1669 (0.0, 0.0)
1670 };
1671
1672 let child_offset = (base.0 - ox, base.1 - oy);
1674 for c in &v.children {
1675 walk(
1676 c,
1677 t,
1678 nodes,
1679 scene,
1680 hits,
1681 sems,
1682 textfield_states,
1683 interactions,
1684 focused,
1685 child_offset,
1686 alpha_accum,
1687 text_cache,
1688 );
1689 }
1690
1691 let show_v = content_h > rect.h + 0.5;
1693 let show_h = content_w > rect.w + 0.5;
1694
1695 if show_v || show_h {
1696 let thickness = 6.0f32;
1697 let margin = 2.0f32;
1698 let min_thumb = 24.0f32;
1699
1700 let thm = locals::theme();
1701 let mut track_col = thm.on_surface;
1702 track_col.3 = 32;
1703 let mut thumb_col = thm.on_surface;
1704 thumb_col.3 = 140;
1705
1706 if show_v {
1708 let track_x = rect.x + rect.w - margin - thickness;
1709 let track_y = rect.y + margin;
1710 let mut track_h = (rect.h - 2.0 * margin).max(0.0);
1711 if show_h {
1712 track_h = (track_h - (thickness + margin)).max(0.0);
1713 }
1714
1715 let ratio = (rect.h / content_h).clamp(0.0, 1.0);
1716 let mut thumb_h = (track_h * ratio).clamp(min_thumb, track_h);
1717 let denom = (content_h - rect.h).max(1.0);
1718 let tpos = (oy / denom).clamp(0.0, 1.0);
1719 let max_pos = (track_h - thumb_h).max(0.0);
1720 let thumb_y = track_y + tpos * max_pos;
1721
1722 scene.nodes.push(SceneNode::Rect {
1723 rect: crate::Rect {
1724 x: track_x,
1725 y: track_y,
1726 w: thickness,
1727 h: track_h,
1728 },
1729 color: track_col,
1730 radius: thickness * 0.5,
1731 });
1732 scene.nodes.push(SceneNode::Rect {
1733 rect: crate::Rect {
1734 x: track_x,
1735 y: thumb_y,
1736 w: thickness,
1737 h: thumb_h,
1738 },
1739 color: thumb_col,
1740 radius: thickness * 0.5,
1741 });
1742
1743 if let Some(set_xy) = set_scroll_offset_xy.clone() {
1744 let vthumb_id: u64 = v.id ^ 0x8000_0011;
1745
1746 let map_to_off_y = Rc::new(move |py: f32| -> f32 {
1747 let denom = (content_h - rect.h).max(1.0);
1748 let max_pos = (track_h - thumb_h).max(0.0);
1749 let pos = ((py - track_y) - thumb_h * 0.5).clamp(0.0, max_pos);
1750 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1751 t * denom
1752 });
1753
1754 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1755 let set_xy = set_xy.clone();
1756 let map = map_to_off_y.clone();
1757 Rc::new(move |pe| set_xy(ox, map(pe.position.y))) };
1759
1760 let is_pressed = interactions.pressed.contains(&vthumb_id);
1761 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1762 if is_pressed {
1763 let set_xy = set_xy.clone();
1764 let map = map_to_off_y.clone();
1765 Some(Rc::new(move |pe| set_xy(ox, map(pe.position.y))))
1766 } else {
1767 None
1768 };
1769
1770 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> =
1771 Rc::new(move |_pe| {});
1772
1773 hits.push(HitRegion {
1774 id: vthumb_id,
1775 rect: crate::Rect {
1776 x: track_x,
1777 y: thumb_y,
1778 w: thickness,
1779 h: thumb_h,
1780 },
1781 on_click: None,
1782 on_scroll: None,
1783 focusable: false,
1784 on_pointer_down: Some(on_pd),
1785 on_pointer_move: on_pm,
1786 on_pointer_up: Some(on_pu),
1787 on_pointer_enter: None,
1788 on_pointer_leave: None,
1789 z_index: v.modifier.z_index + 1000.0,
1790 on_text_change: None,
1791 on_text_submit: None,
1792 });
1793 }
1794 }
1795
1796 if show_h {
1798 let track_x = rect.x + margin;
1799 let track_y = rect.y + rect.h - margin - thickness;
1800 let mut track_w = (rect.w - 2.0 * margin).max(0.0);
1801 if show_v {
1802 track_w = (track_w - (thickness + margin)).max(0.0);
1803 }
1804
1805 let ratio = (rect.w / content_w).clamp(0.0, 1.0);
1806 let mut thumb_w = (track_w * ratio).clamp(min_thumb, track_w);
1807 let denom = (content_w - rect.w).max(1.0);
1808 let tpos = (ox / denom).clamp(0.0, 1.0);
1809 let max_pos = (track_w - thumb_w).max(0.0);
1810 let thumb_x = track_x + tpos * max_pos;
1811
1812 scene.nodes.push(SceneNode::Rect {
1813 rect: crate::Rect {
1814 x: track_x,
1815 y: track_y,
1816 w: track_w,
1817 h: thickness,
1818 },
1819 color: track_col,
1820 radius: thickness * 0.5,
1821 });
1822 scene.nodes.push(SceneNode::Rect {
1823 rect: crate::Rect {
1824 x: thumb_x,
1825 y: track_y,
1826 w: thumb_w,
1827 h: thickness,
1828 },
1829 color: thumb_col,
1830 radius: thickness * 0.5,
1831 });
1832
1833 if let Some(set_xy) = set_scroll_offset_xy.clone() {
1834 let hthumb_id: u64 = v.id ^ 0x8000_0012;
1835
1836 let map_to_off_x = Rc::new(move |px: f32| -> f32 {
1837 let denom = (content_w - rect.w).max(1.0);
1838 let max_pos = (track_w - thumb_w).max(0.0);
1839 let pos = ((px - track_x) - thumb_w * 0.5).clamp(0.0, max_pos);
1840 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1841 t * denom
1842 });
1843
1844 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1845 let set_xy = set_xy.clone();
1846 let map = map_to_off_x.clone();
1847 Rc::new(move |pe| set_xy(map(pe.position.x), oy)) };
1849
1850 let is_pressed = interactions.pressed.contains(&hthumb_id);
1851 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1852 if is_pressed {
1853 let set_xy = set_xy.clone();
1854 let map = map_to_off_x.clone();
1855 Some(Rc::new(move |pe| set_xy(map(pe.position.x), oy)))
1856 } else {
1857 None
1858 };
1859
1860 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> =
1861 Rc::new(move |_pe| {});
1862
1863 hits.push(HitRegion {
1864 id: hthumb_id,
1865 rect: crate::Rect {
1866 x: thumb_x,
1867 y: track_y,
1868 w: thumb_w,
1869 h: thickness,
1870 },
1871 on_click: None,
1872 on_scroll: None,
1873 focusable: false,
1874 on_pointer_down: Some(on_pd),
1875 on_pointer_move: on_pm,
1876 on_pointer_up: Some(on_pu),
1877 on_pointer_enter: None,
1878 on_pointer_leave: None,
1879 z_index: v.modifier.z_index + 1000.0,
1880 on_text_change: None,
1881 on_text_submit: None,
1882 });
1883 }
1884 }
1885 }
1886
1887 scene.nodes.push(SceneNode::PopClip);
1888 return;
1889 }
1890 ViewKind::Checkbox {
1891 checked,
1892 label,
1893 on_change,
1894 } => {
1895 let theme = locals::theme();
1896 let box_size = 18.0f32;
1898 let bx = rect.x;
1899 let by = rect.y + (rect.h - box_size) * 0.5;
1900 scene.nodes.push(SceneNode::Rect {
1902 rect: repose_core::Rect {
1903 x: bx,
1904 y: by,
1905 w: box_size,
1906 h: box_size,
1907 },
1908 color: if *checked {
1909 mul_alpha(theme.primary, alpha_accum)
1910 } else {
1911 mul_alpha(theme.surface, alpha_accum)
1912 },
1913 radius: 3.0,
1914 });
1915 scene.nodes.push(SceneNode::Border {
1916 rect: repose_core::Rect {
1917 x: bx,
1918 y: by,
1919 w: box_size,
1920 h: box_size,
1921 },
1922 color: mul_alpha(Color::from_hex("#555555"), alpha_accum),
1923 width: 1.0,
1924 radius: 3.0,
1925 });
1926 if *checked {
1928 scene.nodes.push(SceneNode::Text {
1929 rect: repose_core::Rect {
1930 x: bx + 3.0,
1931 y: by + 1.0,
1932 w: box_size,
1933 h: box_size,
1934 },
1935 text: "✓".to_string(),
1936 color: mul_alpha(theme.on_primary, alpha_accum),
1937 size: 16.0,
1938 });
1939 }
1940 scene.nodes.push(SceneNode::Text {
1942 rect: repose_core::Rect {
1943 x: bx + box_size + 8.0,
1944 y: rect.y,
1945 w: rect.w - (box_size + 8.0),
1946 h: rect.h,
1947 },
1948 text: label.clone(),
1949 color: mul_alpha(theme.on_surface, alpha_accum),
1950 size: 16.0,
1951 });
1952
1953 let toggled = !*checked;
1955 let on_click = on_change.as_ref().map(|cb| {
1956 let cb = cb.clone();
1957 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1958 });
1959 hits.push(HitRegion {
1960 id: v.id,
1961 rect,
1962 on_click,
1963 on_scroll: None,
1964 focusable: true,
1965 on_pointer_down: v.modifier.on_pointer_down.clone(),
1966 on_pointer_move: v.modifier.on_pointer_move.clone(),
1967 on_pointer_up: v.modifier.on_pointer_up.clone(),
1968 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1969 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1970 z_index: v.modifier.z_index,
1971 on_text_change: None,
1972 on_text_submit: None,
1973 });
1974 sems.push(SemNode {
1975 id: v.id,
1976 role: Role::Checkbox,
1977 label: Some(label.clone()),
1978 rect,
1979 focused: is_focused,
1980 enabled: true,
1981 });
1982 if is_focused {
1983 scene.nodes.push(SceneNode::Border {
1984 rect,
1985 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1986 width: 2.0,
1987 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1988 });
1989 }
1990 }
1991
1992 ViewKind::RadioButton {
1993 selected,
1994 label,
1995 on_select,
1996 } => {
1997 let theme = locals::theme();
1998 let d = 18.0f32;
1999 let cx = rect.x;
2000 let cy = rect.y + (rect.h - d) * 0.5;
2001
2002 scene.nodes.push(SceneNode::Border {
2004 rect: repose_core::Rect {
2005 x: cx,
2006 y: cy,
2007 w: d,
2008 h: d,
2009 },
2010 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
2011 width: 1.5,
2012 radius: d * 0.5,
2013 });
2014 if *selected {
2016 scene.nodes.push(SceneNode::Rect {
2017 rect: repose_core::Rect {
2018 x: cx + 4.0,
2019 y: cy + 4.0,
2020 w: d - 8.0,
2021 h: d - 8.0,
2022 },
2023 color: mul_alpha(theme.primary, alpha_accum),
2024 radius: (d - 8.0) * 0.5,
2025 });
2026 }
2027 scene.nodes.push(SceneNode::Text {
2028 rect: repose_core::Rect {
2029 x: cx + d + 8.0,
2030 y: rect.y,
2031 w: rect.w - (d + 8.0),
2032 h: rect.h,
2033 },
2034 text: label.clone(),
2035 color: mul_alpha(theme.on_surface, alpha_accum),
2036 size: 16.0,
2037 });
2038
2039 hits.push(HitRegion {
2040 id: v.id,
2041 rect,
2042 on_click: on_select.clone(),
2043 on_scroll: None,
2044 focusable: true,
2045 on_pointer_down: v.modifier.on_pointer_down.clone(),
2046 on_pointer_move: v.modifier.on_pointer_move.clone(),
2047 on_pointer_up: v.modifier.on_pointer_up.clone(),
2048 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2049 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2050 z_index: v.modifier.z_index,
2051 on_text_change: None,
2052 on_text_submit: None,
2053 });
2054 sems.push(SemNode {
2055 id: v.id,
2056 role: Role::RadioButton,
2057 label: Some(label.clone()),
2058 rect,
2059 focused: is_focused,
2060 enabled: true,
2061 });
2062 if is_focused {
2063 scene.nodes.push(SceneNode::Border {
2064 rect,
2065 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
2066 width: 2.0,
2067 radius: v.modifier.clip_rounded.unwrap_or(6.0),
2068 });
2069 }
2070 }
2071
2072 ViewKind::Switch {
2073 checked,
2074 label,
2075 on_change,
2076 } => {
2077 let theme = locals::theme();
2078 let track_w = 46.0f32;
2080 let track_h = 26.0f32;
2081 let tx = rect.x;
2082 let ty = rect.y + (rect.h - track_h) * 0.5;
2083 let knob = 22.0f32;
2084 let on_col = theme.primary;
2085 let off_col = Color::from_hex("#333333");
2086
2087 scene.nodes.push(SceneNode::Rect {
2089 rect: repose_core::Rect {
2090 x: tx,
2091 y: ty,
2092 w: track_w,
2093 h: track_h,
2094 },
2095 color: if *checked {
2096 mul_alpha(on_col, alpha_accum)
2097 } else {
2098 mul_alpha(off_col, alpha_accum)
2099 },
2100 radius: track_h * 0.5,
2101 });
2102 let kx = if *checked {
2104 tx + track_w - knob - 2.0
2105 } else {
2106 tx + 2.0
2107 };
2108 let ky = ty + (track_h - knob) * 0.5;
2109 scene.nodes.push(SceneNode::Rect {
2110 rect: repose_core::Rect {
2111 x: kx,
2112 y: ky,
2113 w: knob,
2114 h: knob,
2115 },
2116 color: mul_alpha(Color::from_hex("#EEEEEE"), alpha_accum),
2117 radius: knob * 0.5,
2118 });
2119
2120 scene.nodes.push(SceneNode::Text {
2122 rect: repose_core::Rect {
2123 x: tx + track_w + 8.0,
2124 y: rect.y,
2125 w: rect.w - (track_w + 8.0),
2126 h: rect.h,
2127 },
2128 text: label.clone(),
2129 color: mul_alpha(theme.on_surface, alpha_accum),
2130 size: 16.0,
2131 });
2132
2133 let toggled = !*checked;
2134 let on_click = on_change.as_ref().map(|cb| {
2135 let cb = cb.clone();
2136 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
2137 });
2138 hits.push(HitRegion {
2139 id: v.id,
2140 rect,
2141 on_click,
2142 on_scroll: None,
2143 focusable: true,
2144 on_pointer_down: v.modifier.on_pointer_down.clone(),
2145 on_pointer_move: v.modifier.on_pointer_move.clone(),
2146 on_pointer_up: v.modifier.on_pointer_up.clone(),
2147 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2148 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2149 z_index: v.modifier.z_index,
2150 on_text_change: None,
2151 on_text_submit: None,
2152 });
2153 sems.push(SemNode {
2154 id: v.id,
2155 role: Role::Switch,
2156 label: Some(label.clone()),
2157 rect,
2158 focused: is_focused,
2159 enabled: true,
2160 });
2161 if is_focused {
2162 scene.nodes.push(SceneNode::Border {
2163 rect,
2164 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
2165 width: 2.0,
2166 radius: v.modifier.clip_rounded.unwrap_or(6.0),
2167 });
2168 }
2169 }
2170 ViewKind::Slider {
2171 value,
2172 min,
2173 max,
2174 step,
2175 label,
2176 on_change,
2177 } => {
2178 let theme = locals::theme();
2179 let track_h = 4.0f32;
2181 let knob_d = 20.0f32;
2182 let gap = 8.0f32;
2183 let label_x = rect.x + rect.w * 0.6; let track_x = rect.x;
2185 let track_w = (label_x - track_x).max(60.0);
2186 let cy = rect.y + rect.h * 0.5;
2187
2188 scene.nodes.push(SceneNode::Rect {
2190 rect: repose_core::Rect {
2191 x: track_x,
2192 y: cy - track_h * 0.5,
2193 w: track_w,
2194 h: track_h,
2195 },
2196 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2197 radius: track_h * 0.5,
2198 });
2199
2200 let t = clamp01(norm(*value, *min, *max));
2202 let kx = track_x + t * track_w;
2203 scene.nodes.push(SceneNode::Rect {
2204 rect: repose_core::Rect {
2205 x: kx - knob_d * 0.5,
2206 y: cy - knob_d * 0.5,
2207 w: knob_d,
2208 h: knob_d,
2209 },
2210 color: mul_alpha(theme.surface, alpha_accum),
2211 radius: knob_d * 0.5,
2212 });
2213 scene.nodes.push(SceneNode::Border {
2214 rect: repose_core::Rect {
2215 x: kx - knob_d * 0.5,
2216 y: cy - knob_d * 0.5,
2217 w: knob_d,
2218 h: knob_d,
2219 },
2220 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
2221 width: 1.0,
2222 radius: knob_d * 0.5,
2223 });
2224
2225 scene.nodes.push(SceneNode::Text {
2227 rect: repose_core::Rect {
2228 x: label_x + gap,
2229 y: rect.y,
2230 w: rect.x + rect.w - (label_x + gap),
2231 h: rect.h,
2232 },
2233 text: format!("{}: {:.2}", label, *value),
2234 color: mul_alpha(theme.on_surface, alpha_accum),
2235 size: 16.0,
2236 });
2237
2238 let on_change_cb: Option<Rc<dyn Fn(f32)>> = on_change.as_ref().cloned();
2240 let minv = *min;
2241 let maxv = *max;
2242 let stepv = *step;
2243
2244 let current = Rc::new(RefCell::new(*value));
2246
2247 let update_at = {
2249 let on_change_cb = on_change_cb.clone();
2250 let current = current.clone();
2251 Rc::new(move |px: f32| {
2252 let tt = clamp01((px - track_x) / track_w);
2253 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2254 *current.borrow_mut() = v;
2255 if let Some(cb) = &on_change_cb {
2256 cb(v);
2257 }
2258 })
2259 };
2260
2261 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2263 let f = update_at.clone();
2264 Rc::new(move |pe| {
2265 f(pe.position.x);
2266 })
2267 };
2268
2269 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2271 let f = update_at.clone();
2272 Rc::new(move |pe| {
2273 f(pe.position.x);
2274 })
2275 };
2276
2277 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
2279
2280 let on_scroll = {
2282 let on_change_cb = on_change_cb.clone();
2283 let current = current.clone();
2284 Rc::new(move |d: Vec2| -> Vec2 {
2285 let base = *current.borrow();
2286 let delta = stepv.unwrap_or((maxv - minv) * 0.01);
2287 let dir = if d.y.is_sign_negative() { 1.0 } else { -1.0 };
2289 let new_v = snap_step(base + dir * delta, stepv, minv, maxv);
2290 *current.borrow_mut() = new_v;
2291 if let Some(cb) = &on_change_cb {
2292 cb(new_v);
2293 }
2294 Vec2 { x: d.x, y: 0.0 } })
2296 };
2297
2298 hits.push(HitRegion {
2300 id: v.id,
2301 rect,
2302 on_click: None,
2303 on_scroll: Some(on_scroll),
2304 focusable: true,
2305 on_pointer_down: Some(on_pd),
2306 on_pointer_move: if is_pressed { Some(on_pm) } else { None },
2307 on_pointer_up: Some(on_pu),
2308 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2309 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2310 z_index: v.modifier.z_index,
2311 on_text_change: None,
2312 on_text_submit: None,
2313 });
2314
2315 sems.push(SemNode {
2316 id: v.id,
2317 role: Role::Slider,
2318 label: Some(label.clone()),
2319 rect,
2320 focused: is_focused,
2321 enabled: true,
2322 });
2323 if is_focused {
2324 scene.nodes.push(SceneNode::Border {
2325 rect,
2326 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
2327 width: 2.0,
2328 radius: v.modifier.clip_rounded.unwrap_or(6.0),
2329 });
2330 }
2331 }
2332 ViewKind::RangeSlider {
2333 start,
2334 end,
2335 min,
2336 max,
2337 step,
2338 label,
2339 on_change,
2340 } => {
2341 let theme = locals::theme();
2342 let track_h = 4.0f32;
2343 let knob_d = 20.0f32;
2344 let gap = 8.0f32;
2345 let label_x = rect.x + rect.w * 0.6;
2346 let track_x = rect.x;
2347 let track_w = (label_x - track_x).max(80.0);
2348 let cy = rect.y + rect.h * 0.5;
2349
2350 scene.nodes.push(SceneNode::Rect {
2352 rect: repose_core::Rect {
2353 x: track_x,
2354 y: cy - track_h * 0.5,
2355 w: track_w,
2356 h: track_h,
2357 },
2358 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2359 radius: track_h * 0.5,
2360 });
2361
2362 let t0 = clamp01(norm(*start, *min, *max));
2364 let t1 = clamp01(norm(*end, *min, *max));
2365 let k0x = track_x + t0 * track_w;
2366 let k1x = track_x + t1 * track_w;
2367
2368 scene.nodes.push(SceneNode::Rect {
2370 rect: repose_core::Rect {
2371 x: k0x.min(k1x),
2372 y: cy - track_h * 0.5,
2373 w: (k1x - k0x).abs(),
2374 h: track_h,
2375 },
2376 color: mul_alpha(theme.primary, alpha_accum),
2377 radius: track_h * 0.5,
2378 });
2379
2380 for &kx in &[k0x, k1x] {
2382 scene.nodes.push(SceneNode::Rect {
2383 rect: repose_core::Rect {
2384 x: kx - knob_d * 0.5,
2385 y: cy - knob_d * 0.5,
2386 w: knob_d,
2387 h: knob_d,
2388 },
2389 color: mul_alpha(theme.surface, alpha_accum),
2390 radius: knob_d * 0.5,
2391 });
2392 scene.nodes.push(SceneNode::Border {
2393 rect: repose_core::Rect {
2394 x: kx - knob_d * 0.5,
2395 y: cy - knob_d * 0.5,
2396 w: knob_d,
2397 h: knob_d,
2398 },
2399 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
2400 width: 1.0,
2401 radius: knob_d * 0.5,
2402 });
2403 }
2404
2405 scene.nodes.push(SceneNode::Text {
2407 rect: repose_core::Rect {
2408 x: label_x + gap,
2409 y: rect.y,
2410 w: rect.x + rect.w - (label_x + gap),
2411 h: rect.h,
2412 },
2413 text: format!("{}: {:.2} – {:.2}", label, *start, *end),
2414 color: mul_alpha(theme.on_surface, alpha_accum),
2415 size: 16.0,
2416 });
2417
2418 let on_change_cb = on_change.as_ref().cloned();
2420 let minv = *min;
2421 let maxv = *max;
2422 let stepv = *step;
2423 let start_val = *start;
2424 let end_val = *end;
2425
2426 let active = Rc::new(RefCell::new(None::<u8>));
2428
2429 let update = {
2431 let active = active.clone();
2432 let on_change_cb = on_change_cb.clone();
2433 Rc::new(move |px: f32| {
2434 if let Some(thumb) = *active.borrow() {
2435 let tt = clamp01((px - track_x) / track_w);
2436 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2437 match thumb {
2438 0 => {
2439 let new_start = v.min(end_val).min(maxv).max(minv);
2440 if let Some(cb) = &on_change_cb {
2441 cb(new_start, end_val);
2442 }
2443 }
2444 _ => {
2445 let new_end = v.max(start_val).max(minv).min(maxv);
2446 if let Some(cb) = &on_change_cb {
2447 cb(start_val, new_end);
2448 }
2449 }
2450 }
2451 }
2452 })
2453 };
2454
2455 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2457 let active = active.clone();
2458 let update = update.clone();
2459 let k0x0 = k0x;
2461 let k1x0 = k1x;
2462 Rc::new(move |pe| {
2463 let px = pe.position.x;
2464 let d0 = (px - k0x0).abs();
2465 let d1 = (px - k1x0).abs();
2466 *active.borrow_mut() = Some(if d0 <= d1 { 0 } else { 1 });
2467 update(px);
2468 })
2469 };
2470
2471 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2473 let active = active.clone();
2474 let update = update.clone();
2475 Rc::new(move |pe| {
2476 if active.borrow().is_some() {
2477 update(pe.position.x);
2478 }
2479 })
2480 };
2481
2482 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2484 let active = active.clone();
2485 Rc::new(move |_pe| {
2486 *active.borrow_mut() = None;
2487 })
2488 };
2489
2490 hits.push(HitRegion {
2491 id: v.id,
2492 rect,
2493 on_click: None,
2494 on_scroll: None,
2495 focusable: true,
2496 on_pointer_down: Some(on_pd),
2497 on_pointer_move: Some(on_pm),
2498 on_pointer_up: Some(on_pu),
2499 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2500 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2501 z_index: v.modifier.z_index,
2502 on_text_change: None,
2503 on_text_submit: None,
2504 });
2505 sems.push(SemNode {
2506 id: v.id,
2507 role: Role::Slider,
2508 label: Some(label.clone()),
2509 rect,
2510 focused: is_focused,
2511 enabled: true,
2512 });
2513 if is_focused {
2514 scene.nodes.push(SceneNode::Border {
2515 rect,
2516 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
2517 width: 2.0,
2518 radius: v.modifier.clip_rounded.unwrap_or(6.0),
2519 });
2520 }
2521 }
2522 ViewKind::ProgressBar {
2523 value,
2524 min,
2525 max,
2526 label,
2527 circular,
2528 } => {
2529 let theme = locals::theme();
2530 let track_h = 6.0f32;
2531 let gap = 8.0f32;
2532 let label_w_split = rect.w * 0.6;
2533 let track_x = rect.x;
2534 let track_w = (label_w_split - track_x).max(60.0);
2535 let cy = rect.y + rect.h * 0.5;
2536
2537 scene.nodes.push(SceneNode::Rect {
2538 rect: repose_core::Rect {
2539 x: track_x,
2540 y: cy - track_h * 0.5,
2541 w: track_w,
2542 h: track_h,
2543 },
2544 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2545 radius: track_h * 0.5,
2546 });
2547
2548 let t = clamp01(norm(*value, *min, *max));
2549 scene.nodes.push(SceneNode::Rect {
2550 rect: repose_core::Rect {
2551 x: track_x,
2552 y: cy - track_h * 0.5,
2553 w: track_w * t,
2554 h: track_h,
2555 },
2556 color: mul_alpha(theme.primary, alpha_accum),
2557 radius: track_h * 0.5,
2558 });
2559
2560 scene.nodes.push(SceneNode::Text {
2561 rect: repose_core::Rect {
2562 x: rect.x + label_w_split + gap,
2563 y: rect.y,
2564 w: rect.w - (label_w_split + gap),
2565 h: rect.h,
2566 },
2567 text: format!("{}: {:.0}%", label, t * 100.0),
2568 color: mul_alpha(theme.on_surface, alpha_accum),
2569 size: 16.0,
2570 });
2571
2572 sems.push(SemNode {
2573 id: v.id,
2574 role: Role::ProgressBar,
2575 label: Some(label.clone()),
2576 rect,
2577 focused: is_focused,
2578 enabled: true,
2579 });
2580 }
2581
2582 _ => {}
2583 }
2584
2585 for c in &v.children {
2587 walk(
2588 c,
2589 t,
2590 nodes,
2591 scene,
2592 hits,
2593 sems,
2594 textfield_states,
2595 interactions,
2596 focused,
2597 base,
2598 alpha_accum,
2599 text_cache,
2600 );
2601 }
2602
2603 if v.modifier.transform.is_some() {
2604 scene.nodes.push(SceneNode::PopTransform);
2605 }
2606 }
2607
2608 walk(
2610 &root,
2611 &taffy,
2612 &nodes_map,
2613 &mut scene,
2614 &mut hits,
2615 &mut sems,
2616 textfield_states,
2617 interactions,
2618 focused,
2619 (0.0, 0.0),
2620 1.0,
2621 &text_cache,
2622 );
2623
2624 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
2626
2627 (scene, hits, sems)
2628}
2629
2630pub trait TextStyleExt {
2632 fn color(self, c: Color) -> View;
2633 fn size(self, px: f32) -> View;
2634 fn max_lines(self, n: usize) -> View;
2635 fn single_line(self) -> View;
2636 fn overflow_ellipsize(self) -> View;
2637 fn overflow_clip(self) -> View;
2638 fn overflow_visible(self) -> View;
2639}
2640impl TextStyleExt for View {
2641 fn color(self, c: Color) -> View {
2642 TextColor(self, c)
2643 }
2644 fn size(self, px: f32) -> View {
2645 TextSize(self, px)
2646 }
2647 fn max_lines(mut self, n: usize) -> View {
2648 if let ViewKind::Text {
2649 max_lines,
2650 soft_wrap,
2651 ..
2652 } = &mut self.kind
2653 {
2654 *max_lines = Some(n);
2655 *soft_wrap = true;
2656 }
2657 self
2658 }
2659 fn single_line(mut self) -> View {
2660 if let ViewKind::Text {
2661 soft_wrap,
2662 max_lines,
2663 ..
2664 } = &mut self.kind
2665 {
2666 *soft_wrap = false;
2667 *max_lines = Some(1);
2668 }
2669 self
2670 }
2671 fn overflow_ellipsize(mut self) -> View {
2672 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2673 *overflow = TextOverflow::Ellipsis;
2674 }
2675 self
2676 }
2677 fn overflow_clip(mut self) -> View {
2678 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2679 *overflow = TextOverflow::Clip;
2680 }
2681 self
2682 }
2683 fn overflow_visible(mut self) -> View {
2684 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2685 *overflow = TextOverflow::Visible;
2686 }
2687 self
2688 }
2689}