1#![allow(non_snake_case)]
6pub mod anim;
7pub mod lazy;
8
9use std::collections::{HashMap, HashSet};
10use std::rc::Rc;
11use std::{cell::RefCell, cmp::Ordering};
12
13use repose_core::*;
14use taffy::Overflow;
15use taffy::style::{AlignItems, Dimension, Display, FlexDirection, JustifyContent, Style};
16
17use taffy::prelude::{Position, Size, auto, length, percent};
18
19pub mod textfield;
20pub use textfield::{TextField, TextFieldState};
21
22use crate::textfield::{TF_FONT_PX, TF_PADDING_X, byte_to_char_index, measure_text, positions_for};
23use repose_core::locals;
24
25#[derive(Default)]
26pub struct Interactions {
27 pub hover: Option<u64>,
28 pub pressed: HashSet<u64>,
29}
30
31pub fn Surface(modifier: Modifier, child: View) -> View {
32 let mut v = View::new(0, ViewKind::Surface).modifier(modifier);
33 v.children = vec![child];
34 v
35}
36
37pub fn Box(modifier: Modifier) -> View {
38 View::new(0, ViewKind::Box).modifier(modifier)
39}
40
41pub fn Row(modifier: Modifier) -> View {
42 View::new(0, ViewKind::Row).modifier(modifier)
43}
44
45pub fn Column(modifier: Modifier) -> View {
46 View::new(0, ViewKind::Column).modifier(modifier)
47}
48
49pub fn Stack(modifier: Modifier) -> View {
50 View::new(0, ViewKind::Stack).modifier(modifier)
51}
52
53pub fn Scroll(modifier: Modifier) -> View {
54 View::new(
55 0,
56 ViewKind::ScrollV {
57 on_scroll: None,
58 set_viewport_height: None,
59 get_scroll_offset: None,
60 },
61 )
62 .modifier(modifier)
63}
64
65pub fn Text(text: impl Into<String>) -> View {
66 View::new(
67 0,
68 ViewKind::Text {
69 text: text.into(),
70 color: Color::WHITE,
71 font_size: 16.0,
72 },
73 )
74}
75
76pub fn Spacer() -> View {
77 Box(Modifier::new().flex_grow(1.0))
78}
79
80pub fn Grid(columns: usize, modifier: Modifier, children: Vec<View>) -> View {
81 Column(modifier.grid(columns, 0.0, 0.0)).with_children(children)
82}
83
84#[allow(non_snake_case)]
85pub fn TextColor(mut v: View, color: Color) -> View {
86 if let ViewKind::Text {
87 color: text_color, ..
88 } = &mut v.kind
89 {
90 *text_color = color;
91 }
92 v
93}
94
95#[allow(non_snake_case)]
96pub fn TextSize(mut v: View, size: f32) -> View {
97 if let ViewKind::Text {
98 font_size: text_size,
99 ..
100 } = &mut v.kind
101 {
102 *text_size = size;
103 }
104 v
105}
106
107pub fn Button(text: impl Into<String>, on_click: impl Fn() + 'static) -> View {
108 View::new(
109 0,
110 ViewKind::Button {
111 text: text.into(),
112 on_click: Some(Rc::new(on_click)),
113 },
114 )
115 .semantics(Semantics {
116 role: Role::Button,
117 label: None,
118 focused: false,
119 enabled: true,
120 })
121}
122
123pub fn Checkbox(
124 checked: bool,
125 label: impl Into<String>,
126 on_change: impl Fn(bool) + 'static,
127) -> View {
128 View::new(
129 0,
130 ViewKind::Checkbox {
131 checked,
132 label: label.into(),
133 on_change: Some(Rc::new(on_change)),
134 },
135 )
136 .semantics(Semantics {
137 role: Role::Checkbox,
138 label: None,
139 focused: false,
140 enabled: true,
141 })
142}
143
144pub fn RadioButton(
145 selected: bool,
146 label: impl Into<String>,
147 on_select: impl Fn() + 'static,
148) -> View {
149 View::new(
150 0,
151 ViewKind::RadioButton {
152 selected,
153 label: label.into(),
154 on_select: Some(Rc::new(on_select)),
155 },
156 )
157 .semantics(Semantics {
158 role: Role::RadioButton,
159 label: None,
160 focused: false,
161 enabled: true,
162 })
163}
164
165pub fn Switch(checked: bool, label: impl Into<String>, on_change: impl Fn(bool) + 'static) -> View {
166 View::new(
167 0,
168 ViewKind::Switch {
169 checked,
170 label: label.into(),
171 on_change: Some(Rc::new(on_change)),
172 },
173 )
174 .semantics(Semantics {
175 role: Role::Switch,
176 label: None,
177 focused: false,
178 enabled: true,
179 })
180}
181
182pub fn Slider(
183 value: f32,
184 range: (f32, f32),
185 step: Option<f32>,
186 label: impl Into<String>,
187 on_change: impl Fn(f32) + 'static,
188) -> View {
189 View::new(
190 0,
191 ViewKind::Slider {
192 value,
193 min: range.0,
194 max: range.1,
195 step,
196 label: label.into(),
197 on_change: Some(Rc::new(on_change)),
198 },
199 )
200 .semantics(Semantics {
201 role: Role::Slider,
202 label: None,
203 focused: false,
204 enabled: true,
205 })
206}
207
208pub fn RangeSlider(
209 start: f32,
210 end: f32,
211 range: (f32, f32),
212 step: Option<f32>,
213 label: impl Into<String>,
214 on_change: impl Fn(f32, f32) + 'static,
215) -> View {
216 View::new(
217 0,
218 ViewKind::RangeSlider {
219 start,
220 end,
221 min: range.0,
222 max: range.1,
223 step,
224 label: label.into(),
225 on_change: Some(Rc::new(on_change)),
226 },
227 )
228 .semantics(Semantics {
229 role: Role::Slider,
230 label: None,
231 focused: false,
232 enabled: true,
233 })
234}
235
236pub fn ProgressBar(value: f32, range: (f32, f32), label: impl Into<String>) -> View {
237 View::new(
238 0,
239 ViewKind::ProgressBar {
240 value,
241 min: range.0,
242 max: range.1,
243 label: label.into(),
244 circular: false,
245 },
246 )
247 .semantics(Semantics {
248 role: Role::ProgressBar,
249 label: None,
250 focused: false,
251 enabled: true,
252 })
253}
254
255fn flex_dir_for(kind: &ViewKind) -> Option<FlexDirection> {
256 match kind {
257 ViewKind::Row => {
258 if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
259 Some(FlexDirection::RowReverse)
260 } else {
261 Some(FlexDirection::Row)
262 }
263 }
264 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => {
265 Some(FlexDirection::Column)
266 }
267 _ => None,
268 }
269}
270
271pub trait ViewExt: Sized {
273 fn child(self, children: impl IntoChildren) -> Self;
274}
275
276impl ViewExt for View {
277 fn child(self, children: impl IntoChildren) -> Self {
278 self.with_children(children.into_children())
279 }
280}
281
282pub trait IntoChildren {
283 fn into_children(self) -> Vec<View>;
284}
285
286impl IntoChildren for View {
287 fn into_children(self) -> Vec<View> {
288 vec![self]
289 }
290}
291
292impl IntoChildren for Vec<View> {
293 fn into_children(self) -> Vec<View> {
294 self
295 }
296}
297
298impl<const N: usize> IntoChildren for [View; N] {
299 fn into_children(self) -> Vec<View> {
300 self.into()
301 }
302}
303
304macro_rules! impl_into_children_tuple {
306 ($($idx:tt $t:ident),+) => {
307 impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
308 fn into_children(self) -> Vec<View> {
309 let mut v = Vec::new();
310 $(v.extend(self.$idx.into_children());)+
311 v
312 }
313 }
314 };
315}
316
317impl_into_children_tuple!(0 A, 1 B);
318impl_into_children_tuple!(0 A, 1 B, 2 C);
319impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
320impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
321impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
322impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
323impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
324
325pub fn layout_and_paint(
327 root: &View,
328 size: (u32, u32),
329 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
330 interactions: &Interactions,
331 focused: Option<u64>,
332) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
333 let mut id = 1u64;
335 fn stamp(mut v: View, id: &mut u64) -> View {
336 v.id = *id;
337 *id += 1;
338 v.children = v.children.into_iter().map(|c| stamp(c, id)).collect();
339 v
340 }
341 let root = stamp(root.clone(), &mut id);
342
343 use taffy::prelude::*;
345 #[derive(Clone)]
346 enum NodeCtx {
347 Text { text: String, font_px: f32 },
348 Button { label: String },
349 TextField,
350 Container,
351 Checkbox { label: String },
352 Radio { label: String },
353 Switch { label: String },
354 Slider { label: String },
355 Range { label: String },
356 Progress { label: String },
357 }
358
359 let mut taffy: TaffyTree<NodeCtx> = TaffyTree::new();
360 let mut nodes_map = HashMap::new();
361
362 fn style_from_modifier(m: &Modifier, kind: &ViewKind) -> Style {
363 let mut s = Style::default();
364 s.display = match kind {
365 ViewKind::Row => Display::Flex,
366 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => Display::Flex,
367 ViewKind::Stack => Display::Grid, _ => Display::Flex,
369 };
370 if matches!(kind, ViewKind::Row) {
371 s.flex_direction = FlexDirection::Row;
372 }
373 if matches!(
374 kind,
375 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. }
376 ) {
377 s.align_items = Some(AlignItems::Stretch);
378 } else {
379 s.align_items = Some(AlignItems::FlexStart);
380 }
381 s.justify_content = Some(JustifyContent::FlexStart);
382
383 if let Some(r) = m.aspect_ratio {
384 s.aspect_ratio = Some(r);
385 }
386
387 if let Some(g) = m.flex_grow {
389 s.flex_grow = g;
390 }
391 if let Some(sh) = m.flex_shrink {
392 s.flex_shrink = sh;
393 }
394 if let Some(b) = m.flex_basis {
395 s.flex_basis = length(b);
396 }
397
398 if let Some(a) = m.align_self {
400 s.align_self = Some(a);
401 }
402
403 if let Some(repose_core::modifier::PositionType::Absolute) = m.position_type {
405 s.position = Position::Absolute;
406 s.inset = taffy::geometry::Rect {
407 left: m.offset_left.map(length).unwrap_or_else(auto),
408 right: m.offset_right.map(length).unwrap_or_else(auto),
409 top: m.offset_top.map(length).unwrap_or_else(auto),
410 bottom: m.offset_bottom.map(length).unwrap_or_else(auto),
411 };
412 }
413
414 if let Some(cfg) = &m.grid {
416 s.display = Display::Grid;
417
418 s.grid_template_columns = (0..cfg.columns)
420 .map(|_| GridTemplateComponent::Single(flex(1.0f32)))
421 .collect();
422
423 s.gap = Size {
425 width: length(cfg.column_gap),
426 height: length(cfg.row_gap),
427 };
428 }
429
430 if matches!(kind, ViewKind::ScrollV { .. }) {
432 s.overflow = taffy::Point {
433 x: Overflow::Hidden,
434 y: Overflow::Hidden,
435 };
436 }
437
438 if let Some(dir) = flex_dir_for(kind) {
439 s.flex_direction = dir;
440 }
441 if let Some(p) = m.padding {
442 let v = length(p);
443 s.padding = taffy::geometry::Rect {
444 left: v,
445 right: v,
446 top: v,
447 bottom: v,
448 };
449 }
450
451 if let Some(sz) = m.size {
452 if sz.width.is_finite() {
453 s.size.width = length(sz.width);
454 }
455 if sz.height.is_finite() {
456 s.size.height = length(sz.height);
457 }
458 }
459
460 if m.fill_max {
461 s.size.width = percent(1.0);
462 s.size.height = percent(1.0);
463 s.flex_grow = 1.0;
464 s.flex_shrink = 1.0;
465 }
466
467 s.align_items = Some(AlignItems::FlexStart);
468 s.justify_content = Some(JustifyContent::FlexStart);
469 s
470 }
471
472 fn build_node(
473 v: &View,
474 t: &mut TaffyTree<NodeCtx>,
475 nodes_map: &mut HashMap<ViewId, taffy::NodeId>,
476 ) -> taffy::NodeId {
477 let mut style = style_from_modifier(&v.modifier, &v.kind);
478
479 if v.modifier.grid_col_span.is_some() || v.modifier.grid_row_span.is_some() {
480 use taffy::prelude::{GridPlacement, Line};
481
482 if let Some(cs) = v.modifier.grid_col_span {
483 style.grid_column = Line {
484 start: GridPlacement::Auto,
485 end: GridPlacement::Span(cs as u16),
486 };
487 }
488 if let Some(rs) = v.modifier.grid_row_span {
489 style.grid_row = Line {
490 start: GridPlacement::Auto,
491 end: GridPlacement::Span(rs as u16),
492 };
493 }
494 }
495
496 let children: Vec<_> = v
497 .children
498 .iter()
499 .map(|c| build_node(c, t, nodes_map))
500 .collect();
501
502 let node = match &v.kind {
503 ViewKind::Text {
504 text, font_size, ..
505 } => t
506 .new_leaf_with_context(
507 style,
508 NodeCtx::Text {
509 text: text.clone(),
510 font_px: *font_size,
511 },
512 )
513 .unwrap(),
514 ViewKind::Button { text, .. } => t
515 .new_leaf_with_context(
516 style,
517 NodeCtx::Button {
518 label: text.clone(),
519 },
520 )
521 .unwrap(),
522 ViewKind::TextField { .. } => {
523 t.new_leaf_with_context(style, NodeCtx::TextField).unwrap()
524 }
525 ViewKind::Checkbox { label, .. } => t
526 .new_leaf_with_context(
527 style,
528 NodeCtx::Checkbox {
529 label: label.clone(),
530 },
531 )
532 .unwrap(),
533 ViewKind::RadioButton { label, .. } => t
534 .new_leaf_with_context(
535 style,
536 NodeCtx::Radio {
537 label: label.clone(),
538 },
539 )
540 .unwrap(),
541 ViewKind::Switch { label, .. } => t
542 .new_leaf_with_context(
543 style,
544 NodeCtx::Switch {
545 label: label.clone(),
546 },
547 )
548 .unwrap(),
549 ViewKind::Slider { label, .. } => t
550 .new_leaf_with_context(
551 style,
552 NodeCtx::Slider {
553 label: label.clone(),
554 },
555 )
556 .unwrap(),
557 ViewKind::RangeSlider { label, .. } => t
558 .new_leaf_with_context(
559 style,
560 NodeCtx::Range {
561 label: label.clone(),
562 },
563 )
564 .unwrap(),
565 ViewKind::ProgressBar { label, .. } => t
566 .new_leaf_with_context(
567 style,
568 NodeCtx::Progress {
569 label: label.clone(),
570 },
571 )
572 .unwrap(),
573 _ => {
574 let n = t.new_with_children(style, &children).unwrap();
575 t.set_node_context(n, Some(NodeCtx::Container)).ok();
576 n
577 }
578 };
579
580 nodes_map.insert(v.id, node);
581 node
582 }
583
584 let root_node = build_node(&root, &mut taffy, &mut nodes_map);
585
586 let available = taffy::geometry::Size {
587 width: AvailableSpace::Definite(size.0 as f32),
588 height: AvailableSpace::Definite(size.1 as f32),
589 };
590
591 taffy
593 .compute_layout_with_measure(root_node, available, |known, _avail, _node, ctx, _style| {
594 match ctx {
595 Some(NodeCtx::Text { text, font_px }) => {
596 let approx_w = text.len() as f32 * *font_px * 0.6;
597 let w = known.width.unwrap_or(approx_w);
598 taffy::geometry::Size {
599 width: w,
600 height: *font_px * 1.3,
601 }
602 }
603 Some(NodeCtx::Button { label }) => taffy::geometry::Size {
604 width: (label.len() as f32 * 16.0 * 0.6) + 24.0,
605 height: 36.0,
606 },
607 Some(NodeCtx::TextField) => {
608 let w = known.width.unwrap_or(220.0);
609 taffy::geometry::Size {
610 width: w,
611 height: 36.0,
612 }
613 }
614 Some(NodeCtx::Checkbox { label }) => {
615 let label_w = (label.len() as f32) * 16.0 * 0.6;
616 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
618 width: known.width.unwrap_or(w),
619 height: 24.0,
620 }
621 }
622 Some(NodeCtx::Radio { label }) => {
623 let label_w = (label.len() as f32) * 16.0 * 0.6;
624 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
626 width: known.width.unwrap_or(w),
627 height: 24.0,
628 }
629 }
630 Some(NodeCtx::Switch { label }) => {
631 let label_w = (label.len() as f32) * 16.0 * 0.6;
632 let w = 46.0 + 8.0 + label_w; taffy::geometry::Size {
634 width: known.width.unwrap_or(w),
635 height: 28.0,
636 }
637 }
638 Some(NodeCtx::Slider { label }) => {
639 let label_w = (label.len() as f32) * 16.0 * 0.6;
640 let w = (known.width).unwrap_or(200.0f32.max(46.0 + 8.0 + label_w));
641 taffy::geometry::Size {
642 width: w,
643 height: 28.0,
644 }
645 }
646 Some(NodeCtx::Range { label }) => {
647 let label_w = (label.len() as f32) * 16.0 * 0.6;
648 let w = (known.width).unwrap_or(220.0f32.max(46.0 + 8.0 + label_w));
649 taffy::geometry::Size {
650 width: w,
651 height: 28.0,
652 }
653 }
654 Some(NodeCtx::Progress { label }) => {
655 let label_w = (label.len() as f32) * 16.0 * 0.6;
656 let w = (known.width).unwrap_or(200.0f32.max(100.0 + 8.0 + label_w));
657 taffy::geometry::Size {
658 width: w,
659 height: 12.0 + 8.0,
660 } }
662 Some(NodeCtx::Container) | None => taffy::geometry::Size::ZERO,
663 }
664 })
665 .unwrap();
666
667 fn layout_of(node: taffy::NodeId, t: &TaffyTree<impl Clone>) -> repose_core::Rect {
675 let l = t.layout(node).unwrap();
676 repose_core::Rect {
677 x: l.location.x,
678 y: l.location.y,
679 w: l.size.width,
680 h: l.size.height,
681 }
682 }
683
684 fn add_offset(mut r: repose_core::Rect, off: (f32, f32)) -> repose_core::Rect {
685 r.x += off.0;
686 r.y += off.1;
687 r
688 }
689
690 fn clamp01(x: f32) -> f32 {
691 x.max(0.0).min(1.0)
692 }
693 fn norm(value: f32, min: f32, max: f32) -> f32 {
694 if max > min {
695 (value - min) / (max - min)
696 } else {
697 0.0
698 }
699 }
700 fn denorm(t: f32, min: f32, max: f32) -> f32 {
701 min + t * (max - min)
702 }
703 fn snap_step(v: f32, step: Option<f32>, min: f32, max: f32) -> f32 {
704 match step {
705 Some(s) if s > 0.0 => {
706 let k = ((v - min) / s).round();
707 (min + k * s).clamp(min, max)
708 }
709 _ => v.clamp(min, max),
710 }
711 }
712
713 let mut scene = Scene {
714 clear_color: locals::theme().background,
715 nodes: vec![],
716 };
717 let mut hits: Vec<HitRegion> = vec![];
718 let mut sems: Vec<SemNode> = vec![];
719
720 fn walk(
721 v: &View,
722 t: &TaffyTree<NodeCtx>,
723 nodes: &HashMap<ViewId, taffy::NodeId>,
724 scene: &mut Scene,
725 hits: &mut Vec<HitRegion>,
726 sems: &mut Vec<SemNode>,
727 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
728 interactions: &Interactions,
729 focused: Option<u64>,
730 parent_offset: (f32, f32),
731 ) {
732 let local = layout_of(nodes[&v.id], t);
733 let rect = add_offset(local, parent_offset);
734 let base = (parent_offset.0 + local.x, parent_offset.1 + local.y);
735
736 let is_hovered = interactions.hover == Some(v.id);
737 let is_pressed = interactions.pressed.contains(&v.id);
738 let is_focused = focused == Some(v.id);
739
740 if let Some(bg) = v.modifier.background {
742 scene.nodes.push(SceneNode::Rect {
743 rect,
744 color: bg,
745 radius: v.modifier.clip_rounded.unwrap_or(0.0),
746 });
747 }
748
749 if let Some(b) = &v.modifier.border {
751 scene.nodes.push(SceneNode::Border {
752 rect,
753 color: b.color,
754 width: b.width,
755 radius: b.radius.max(v.modifier.clip_rounded.unwrap_or(0.0)),
756 });
757 }
758
759 let has_pointer = v.modifier.on_pointer_down.is_some()
760 || v.modifier.on_pointer_move.is_some()
761 || v.modifier.on_pointer_up.is_some()
762 || v.modifier.on_pointer_enter.is_some()
763 || v.modifier.on_pointer_leave.is_some();
764
765 if has_pointer || v.modifier.click {
766 hits.push(HitRegion {
767 id: v.id,
768 rect,
769 on_click: None, on_scroll: None, focusable: false,
772 on_pointer_down: v.modifier.on_pointer_down.clone(),
773 on_pointer_move: v.modifier.on_pointer_move.clone(),
774 on_pointer_up: v.modifier.on_pointer_up.clone(),
775 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
776 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
777 z_index: v.modifier.z_index,
778 on_text_change: None,
779 });
780 }
781
782 match &v.kind {
783 ViewKind::Text {
784 text,
785 color,
786 font_size,
787 } => {
788 let scaled_size = *font_size * locals::text_scale().0;
790 scene.nodes.push(SceneNode::Text {
791 rect,
792 text: text.clone(),
793 color: *color,
794 size: scaled_size,
795 });
796 sems.push(SemNode {
797 id: v.id,
798 role: Role::Text,
799 label: Some(text.clone()),
800 rect,
801 focused: is_focused,
802 enabled: true,
803 });
804 }
805
806 ViewKind::Button { text, on_click } => {
807 if v.modifier.background.is_none() {
809 let base = if is_pressed {
810 Color::from_hex("#1f7556")
811 } else if is_hovered {
812 Color::from_hex("#2a8f6a")
813 } else {
814 Color::from_hex("#34af82")
815 };
816 scene.nodes.push(SceneNode::Rect {
817 rect,
818 color: base,
819 radius: v.modifier.clip_rounded.unwrap_or(6.0),
820 });
821 }
822 scene.nodes.push(SceneNode::Text {
824 rect: repose_core::Rect {
825 x: rect.x + 12.0,
826 y: rect.y + 10.0,
827 w: rect.w - 24.0,
828 h: rect.h - 20.0,
829 },
830 text: text.clone(),
831 color: Color::WHITE,
832 size: 16.0,
833 });
834
835 if v.modifier.click || on_click.is_some() {
836 hits.push(HitRegion {
837 id: v.id,
838 rect,
839 on_click: on_click.clone(),
840 on_scroll: None,
841 focusable: true,
842 on_pointer_down: v.modifier.on_pointer_down.clone(),
843 on_pointer_move: v.modifier.on_pointer_move.clone(),
844 on_pointer_up: v.modifier.on_pointer_up.clone(),
845 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
846 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
847 z_index: v.modifier.z_index,
848 on_text_change: None,
849 });
850 }
851 sems.push(SemNode {
852 id: v.id,
853 role: Role::Button,
854 label: Some(text.clone()),
855 rect,
856 focused: is_focused,
857 enabled: true,
858 });
859 if is_focused {
861 scene.nodes.push(SceneNode::Border {
862 rect,
863 color: Color::from_hex("#88CCFF"),
864 width: 2.0,
865 radius: v.modifier.clip_rounded.unwrap_or(6.0),
866 });
867 }
868 }
869
870 ViewKind::TextField {
871 hint, on_change, ..
872 } => {
873 hits.push(HitRegion {
874 id: v.id,
875 rect,
876 on_click: None,
877 on_scroll: None,
878 focusable: true,
879 on_pointer_down: None,
880 on_pointer_move: None,
881 on_pointer_up: None,
882 on_pointer_enter: None,
883 on_pointer_leave: None,
884 z_index: v.modifier.z_index,
885 on_text_change: on_change.clone(),
886 });
887
888 let inner = repose_core::Rect {
890 x: rect.x + TF_PADDING_X,
891 y: rect.y + 8.0,
892 w: rect.w - 2.0 * TF_PADDING_X,
893 h: rect.h - 16.0,
894 };
895 scene.nodes.push(SceneNode::PushClip {
896 rect: inner,
897 radius: 0.0,
898 });
899 if is_focused {
901 scene.nodes.push(SceneNode::Border {
902 rect,
903 color: Color::from_hex("#88CCFF"),
904 width: 2.0,
905 radius: v.modifier.clip_rounded.unwrap_or(6.0),
906 });
907 }
908 if let Some(state_rc) = textfield_states.get(&v.id) {
909 state_rc.borrow_mut().set_inner_width(inner.w);
910
911 let state = state_rc.borrow();
912 let text = &state.text;
913 let px = TF_FONT_PX as u32;
914 let m = measure_text(text, px);
915
916 if state.selection.start != state.selection.end {
918 let i0 = byte_to_char_index(&m, state.selection.start);
919 let i1 = byte_to_char_index(&m, state.selection.end);
920 let sx = m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
921 let ex = m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
922 let sel_x = inner.x + sx.max(0.0);
923 let sel_w = (ex - sx).max(0.0);
924 scene.nodes.push(SceneNode::Rect {
925 rect: repose_core::Rect {
926 x: sel_x,
927 y: inner.y,
928 w: sel_w,
929 h: inner.h,
930 },
931 color: Color::from_hex("#3B7BFF55"),
932 radius: 0.0,
933 });
934 }
935
936 if let Some(range) = &state.composition {
938 if range.start < range.end && !text.is_empty() {
939 let i0 = byte_to_char_index(&m, range.start);
940 let i1 = byte_to_char_index(&m, range.end);
941 let sx =
942 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
943 let ex =
944 m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
945 let ux = inner.x + sx.max(0.0);
946 let uw = (ex - sx).max(0.0);
947 scene.nodes.push(SceneNode::Rect {
948 rect: repose_core::Rect {
949 x: ux,
950 y: inner.y + inner.h - 2.0,
951 w: uw,
952 h: 2.0,
953 },
954 color: Color::from_hex("#88CCFF"),
955 radius: 0.0,
956 });
957 }
958 }
959
960 scene.nodes.push(SceneNode::Text {
962 rect: repose_core::Rect {
963 x: inner.x - state.scroll_offset,
964 y: inner.y,
965 w: inner.w,
966 h: inner.h,
967 },
968 text: if text.is_empty() {
969 hint.clone()
970 } else {
971 text.clone()
972 },
973 color: if text.is_empty() {
974 Color::from_hex("#666666")
975 } else {
976 Color::from_hex("#CCCCCC")
977 },
978 size: TF_FONT_PX,
979 });
980
981 if state.selection.start == state.selection.end && state.caret_visible() {
983 let i = byte_to_char_index(&m, state.selection.end);
984 let cx = m.positions.get(i).copied().unwrap_or(0.0) - state.scroll_offset;
985 let caret_x = inner.x + cx.max(0.0);
986 scene.nodes.push(SceneNode::Rect {
987 rect: repose_core::Rect {
988 x: caret_x,
989 y: inner.y,
990 w: 1.0,
991 h: inner.h,
992 },
993 color: Color::WHITE,
994 radius: 0.0,
995 });
996 }
997 scene.nodes.push(SceneNode::PopClip);
999
1000 sems.push(SemNode {
1001 id: v.id,
1002 role: Role::TextField,
1003 label: Some(text.clone()),
1004 rect,
1005 focused: is_focused,
1006 enabled: true,
1007 });
1008 } else {
1009 scene.nodes.push(SceneNode::Text {
1011 rect: repose_core::Rect {
1012 x: inner.x,
1013 y: inner.y,
1014 w: inner.w,
1015 h: inner.h,
1016 },
1017 text: hint.clone(),
1018 color: Color::from_hex("#666666"),
1019 size: TF_FONT_PX,
1020 });
1021 sems.push(SemNode {
1022 id: v.id,
1023 role: Role::TextField,
1024 label: Some(hint.clone()),
1025 rect,
1026 focused: is_focused,
1027 enabled: true,
1028 });
1029 }
1030 }
1031 ViewKind::ScrollV {
1032 on_scroll,
1033 set_viewport_height,
1034 get_scroll_offset,
1035 } => {
1036 hits.push(HitRegion {
1038 id: v.id,
1039 rect, on_click: None,
1041 on_scroll: on_scroll.clone(),
1042 focusable: false,
1043 on_pointer_down: v.modifier.on_pointer_down.clone(),
1044 on_pointer_move: v.modifier.on_pointer_move.clone(),
1045 on_pointer_up: v.modifier.on_pointer_up.clone(),
1046 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1047 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1048 z_index: v.modifier.z_index,
1049 on_text_change: None,
1050 });
1051
1052 if let Some(set_vh) = set_viewport_height {
1054 set_vh(local.h); }
1056
1057 scene.nodes.push(SceneNode::PushClip {
1059 rect,
1060 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1061 });
1062
1063 let mut child_offset = base;
1065 if let Some(get) = get_scroll_offset {
1066 child_offset.1 -= get();
1067 }
1068
1069 for c in &v.children {
1070 walk(
1071 c,
1072 t,
1073 nodes,
1074 scene,
1075 hits,
1076 sems,
1077 textfield_states,
1078 interactions,
1079 focused,
1080 child_offset,
1081 );
1082 }
1083
1084 scene.nodes.push(SceneNode::PopClip);
1085 return; }
1087 ViewKind::Checkbox {
1088 checked,
1089 label,
1090 on_change,
1091 } => {
1092 let theme = locals::theme();
1093 let box_size = 18.0f32;
1095 let bx = rect.x;
1096 let by = rect.y + (rect.h - box_size) * 0.5;
1097 scene.nodes.push(SceneNode::Rect {
1099 rect: repose_core::Rect {
1100 x: bx,
1101 y: by,
1102 w: box_size,
1103 h: box_size,
1104 },
1105 color: if *checked {
1106 theme.primary
1107 } else {
1108 theme.surface
1109 },
1110 radius: 3.0,
1111 });
1112 scene.nodes.push(SceneNode::Border {
1113 rect: repose_core::Rect {
1114 x: bx,
1115 y: by,
1116 w: box_size,
1117 h: box_size,
1118 },
1119 color: Color::from_hex("#555555"),
1120 width: 1.0,
1121 radius: 3.0,
1122 });
1123 if *checked {
1125 scene.nodes.push(SceneNode::Text {
1126 rect: repose_core::Rect {
1127 x: bx + 3.0,
1128 y: by + 1.0,
1129 w: box_size,
1130 h: box_size,
1131 },
1132 text: "✓".to_string(),
1133 color: theme.on_primary,
1134 size: 16.0,
1135 });
1136 }
1137 scene.nodes.push(SceneNode::Text {
1139 rect: repose_core::Rect {
1140 x: bx + box_size + 8.0,
1141 y: rect.y,
1142 w: rect.w - (box_size + 8.0),
1143 h: rect.h,
1144 },
1145 text: label.clone(),
1146 color: theme.on_surface,
1147 size: 16.0,
1148 });
1149
1150 let toggled = !*checked;
1152 let on_click = on_change.as_ref().map(|cb| {
1153 let cb = cb.clone();
1154 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1155 });
1156 hits.push(HitRegion {
1157 id: v.id,
1158 rect,
1159 on_click,
1160 on_scroll: None,
1161 focusable: true,
1162 on_pointer_down: v.modifier.on_pointer_down.clone(),
1163 on_pointer_move: v.modifier.on_pointer_move.clone(),
1164 on_pointer_up: v.modifier.on_pointer_up.clone(),
1165 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1166 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1167 z_index: v.modifier.z_index,
1168 on_text_change: None,
1169 });
1170 sems.push(SemNode {
1171 id: v.id,
1172 role: Role::Checkbox,
1173 label: Some(label.clone()),
1174 rect,
1175 focused: is_focused,
1176 enabled: true,
1177 });
1178 if is_focused {
1179 scene.nodes.push(SceneNode::Border {
1180 rect,
1181 color: Color::from_hex("#88CCFF"),
1182 width: 2.0,
1183 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1184 });
1185 }
1186 }
1187
1188 ViewKind::RadioButton {
1189 selected,
1190 label,
1191 on_select,
1192 } => {
1193 let theme = locals::theme();
1194 let d = 18.0f32;
1195 let cx = rect.x;
1196 let cy = rect.y + (rect.h - d) * 0.5;
1197
1198 scene.nodes.push(SceneNode::Border {
1200 rect: repose_core::Rect {
1201 x: cx,
1202 y: cy,
1203 w: d,
1204 h: d,
1205 },
1206 color: Color::from_hex("#888888"),
1207 width: 1.5,
1208 radius: d * 0.5,
1209 });
1210 if *selected {
1212 scene.nodes.push(SceneNode::Rect {
1213 rect: repose_core::Rect {
1214 x: cx + 4.0,
1215 y: cy + 4.0,
1216 w: d - 8.0,
1217 h: d - 8.0,
1218 },
1219 color: theme.primary,
1220 radius: (d - 8.0) * 0.5,
1221 });
1222 }
1223 scene.nodes.push(SceneNode::Text {
1224 rect: repose_core::Rect {
1225 x: cx + d + 8.0,
1226 y: rect.y,
1227 w: rect.w - (d + 8.0),
1228 h: rect.h,
1229 },
1230 text: label.clone(),
1231 color: theme.on_surface,
1232 size: 16.0,
1233 });
1234
1235 hits.push(HitRegion {
1236 id: v.id,
1237 rect,
1238 on_click: on_select.clone(),
1239 on_scroll: None,
1240 focusable: true,
1241 on_pointer_down: v.modifier.on_pointer_down.clone(),
1242 on_pointer_move: v.modifier.on_pointer_move.clone(),
1243 on_pointer_up: v.modifier.on_pointer_up.clone(),
1244 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1245 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1246 z_index: v.modifier.z_index,
1247 on_text_change: None,
1248 });
1249 sems.push(SemNode {
1250 id: v.id,
1251 role: Role::RadioButton,
1252 label: Some(label.clone()),
1253 rect,
1254 focused: is_focused,
1255 enabled: true,
1256 });
1257 if is_focused {
1258 scene.nodes.push(SceneNode::Border {
1259 rect,
1260 color: Color::from_hex("#88CCFF"),
1261 width: 2.0,
1262 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1263 });
1264 }
1265 }
1266
1267 ViewKind::Switch {
1268 checked,
1269 label,
1270 on_change,
1271 } => {
1272 let theme = locals::theme();
1273 let track_w = 46.0f32;
1275 let track_h = 26.0f32;
1276 let tx = rect.x;
1277 let ty = rect.y + (rect.h - track_h) * 0.5;
1278 let knob = 22.0f32;
1279 let on_col = theme.primary;
1280 let off_col = Color::from_hex("#333333");
1281
1282 scene.nodes.push(SceneNode::Rect {
1284 rect: repose_core::Rect {
1285 x: tx,
1286 y: ty,
1287 w: track_w,
1288 h: track_h,
1289 },
1290 color: if *checked { on_col } else { off_col },
1291 radius: track_h * 0.5,
1292 });
1293 let kx = if *checked {
1295 tx + track_w - knob - 2.0
1296 } else {
1297 tx + 2.0
1298 };
1299 let ky = ty + (track_h - knob) * 0.5;
1300 scene.nodes.push(SceneNode::Rect {
1301 rect: repose_core::Rect {
1302 x: kx,
1303 y: ky,
1304 w: knob,
1305 h: knob,
1306 },
1307 color: Color::from_hex("#EEEEEE"),
1308 radius: knob * 0.5,
1309 });
1310
1311 scene.nodes.push(SceneNode::Text {
1313 rect: repose_core::Rect {
1314 x: tx + track_w + 8.0,
1315 y: rect.y,
1316 w: rect.w - (track_w + 8.0),
1317 h: rect.h,
1318 },
1319 text: label.clone(),
1320 color: theme.on_surface,
1321 size: 16.0,
1322 });
1323
1324 let toggled = !*checked;
1325 let on_click = on_change.as_ref().map(|cb| {
1326 let cb = cb.clone();
1327 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1328 });
1329 hits.push(HitRegion {
1330 id: v.id,
1331 rect,
1332 on_click,
1333 on_scroll: None,
1334 focusable: true,
1335 on_pointer_down: v.modifier.on_pointer_down.clone(),
1336 on_pointer_move: v.modifier.on_pointer_move.clone(),
1337 on_pointer_up: v.modifier.on_pointer_up.clone(),
1338 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1339 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1340 z_index: v.modifier.z_index,
1341 on_text_change: None,
1342 });
1343 sems.push(SemNode {
1344 id: v.id,
1345 role: Role::Switch,
1346 label: Some(label.clone()),
1347 rect,
1348 focused: is_focused,
1349 enabled: true,
1350 });
1351 if is_focused {
1352 scene.nodes.push(SceneNode::Border {
1353 rect,
1354 color: Color::from_hex("#88CCFF"),
1355 width: 2.0,
1356 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1357 });
1358 }
1359 }
1360 ViewKind::Slider {
1361 value,
1362 min,
1363 max,
1364 step,
1365 label,
1366 on_change,
1367 } => {
1368 let theme = locals::theme();
1369 let track_h = 4.0f32;
1371 let knob_d = 20.0f32;
1372 let gap = 8.0f32;
1373 let label_x = rect.x + rect.w * 0.6; let track_x = rect.x;
1375 let track_w = (label_x - track_x).max(60.0);
1376 let cy = rect.y + rect.h * 0.5;
1377
1378 scene.nodes.push(SceneNode::Rect {
1380 rect: repose_core::Rect {
1381 x: track_x,
1382 y: cy - track_h * 0.5,
1383 w: track_w,
1384 h: track_h,
1385 },
1386 color: Color::from_hex("#333333"),
1387 radius: track_h * 0.5,
1388 });
1389
1390 let t = clamp01(norm(*value, *min, *max));
1392 let kx = track_x + t * track_w;
1393 scene.nodes.push(SceneNode::Rect {
1394 rect: repose_core::Rect {
1395 x: kx - knob_d * 0.5,
1396 y: cy - knob_d * 0.5,
1397 w: knob_d,
1398 h: knob_d,
1399 },
1400 color: theme.surface,
1401 radius: knob_d * 0.5,
1402 });
1403 scene.nodes.push(SceneNode::Border {
1404 rect: repose_core::Rect {
1405 x: kx - knob_d * 0.5,
1406 y: cy - knob_d * 0.5,
1407 w: knob_d,
1408 h: knob_d,
1409 },
1410 color: Color::from_hex("#888888"),
1411 width: 1.0,
1412 radius: knob_d * 0.5,
1413 });
1414
1415 scene.nodes.push(SceneNode::Text {
1417 rect: repose_core::Rect {
1418 x: label_x + gap,
1419 y: rect.y,
1420 w: rect.x + rect.w - (label_x + gap),
1421 h: rect.h,
1422 },
1423 text: format!("{}: {:.2}", label, *value),
1424 color: theme.on_surface,
1425 size: 16.0,
1426 });
1427
1428 let on_change_cb: Option<Rc<dyn Fn(f32)>> = on_change.as_ref().cloned();
1430 let minv = *min;
1431 let maxv = *max;
1432 let stepv = *step;
1433
1434 let current = Rc::new(RefCell::new(*value));
1436
1437 let update_at = {
1439 let on_change_cb = on_change_cb.clone();
1440 let current = current.clone();
1441 Rc::new(move |px: f32| {
1442 let tt = clamp01((px - track_x) / track_w);
1443 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
1444 *current.borrow_mut() = v;
1445 if let Some(cb) = &on_change_cb {
1446 cb(v);
1447 }
1448 })
1449 };
1450
1451 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1453 let f = update_at.clone();
1454 Rc::new(move |pe| {
1455 f(pe.position.x);
1456 })
1457 };
1458
1459 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1461 let f = update_at.clone();
1462 Rc::new(move |pe| {
1463 f(pe.position.x);
1464 })
1465 };
1466
1467 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1469
1470 let on_scroll = {
1472 let on_change_cb = on_change_cb.clone();
1473 let current = current.clone();
1474 Rc::new(move |dy: f32| -> f32 {
1475 let base = *current.borrow();
1476 let delta = stepv.unwrap_or((maxv - minv) * 0.01);
1477 let dir = if dy.is_sign_negative() { 1.0 } else { -1.0 };
1479 let new_v = snap_step(base + dir * delta, stepv, minv, maxv);
1480 *current.borrow_mut() = new_v;
1481 if let Some(cb) = &on_change_cb {
1482 cb(new_v);
1483 }
1484 0.0
1485 })
1486 };
1487
1488 hits.push(HitRegion {
1490 id: v.id,
1491 rect,
1492 on_click: None,
1493 on_scroll: Some(on_scroll),
1494 focusable: true,
1495 on_pointer_down: Some(on_pd),
1496 on_pointer_move: if is_pressed { Some(on_pm) } else { None },
1497 on_pointer_up: Some(on_pu),
1498 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1499 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1500 z_index: v.modifier.z_index,
1501 on_text_change: None,
1502 });
1503
1504 sems.push(SemNode {
1505 id: v.id,
1506 role: Role::Slider,
1507 label: Some(label.clone()),
1508 rect,
1509 focused: is_focused,
1510 enabled: true,
1511 });
1512 if is_focused {
1513 scene.nodes.push(SceneNode::Border {
1514 rect,
1515 color: Color::from_hex("#88CCFF"),
1516 width: 2.0,
1517 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1518 });
1519 }
1520 }
1521 ViewKind::RangeSlider {
1522 start,
1523 end,
1524 min,
1525 max,
1526 step,
1527 label,
1528 on_change,
1529 } => {
1530 let theme = locals::theme();
1531 let track_h = 4.0f32;
1532 let knob_d = 20.0f32;
1533 let gap = 8.0f32;
1534 let label_x = rect.x + rect.w * 0.6;
1535 let track_x = rect.x;
1536 let track_w = (label_x - track_x).max(80.0);
1537 let cy = rect.y + rect.h * 0.5;
1538
1539 scene.nodes.push(SceneNode::Rect {
1541 rect: repose_core::Rect {
1542 x: track_x,
1543 y: cy - track_h * 0.5,
1544 w: track_w,
1545 h: track_h,
1546 },
1547 color: Color::from_hex("#333333"),
1548 radius: track_h * 0.5,
1549 });
1550
1551 let t0 = clamp01(norm(*start, *min, *max));
1553 let t1 = clamp01(norm(*end, *min, *max));
1554 let k0x = track_x + t0 * track_w;
1555 let k1x = track_x + t1 * track_w;
1556
1557 scene.nodes.push(SceneNode::Rect {
1559 rect: repose_core::Rect {
1560 x: k0x.min(k1x),
1561 y: cy - track_h * 0.5,
1562 w: (k1x - k0x).abs(),
1563 h: track_h,
1564 },
1565 color: theme.primary,
1566 radius: track_h * 0.5,
1567 });
1568
1569 for &kx in &[k0x, k1x] {
1571 scene.nodes.push(SceneNode::Rect {
1572 rect: repose_core::Rect {
1573 x: kx - knob_d * 0.5,
1574 y: cy - knob_d * 0.5,
1575 w: knob_d,
1576 h: knob_d,
1577 },
1578 color: theme.surface,
1579 radius: knob_d * 0.5,
1580 });
1581 scene.nodes.push(SceneNode::Border {
1582 rect: repose_core::Rect {
1583 x: kx - knob_d * 0.5,
1584 y: cy - knob_d * 0.5,
1585 w: knob_d,
1586 h: knob_d,
1587 },
1588 color: Color::from_hex("#888888"),
1589 width: 1.0,
1590 radius: knob_d * 0.5,
1591 });
1592 }
1593
1594 scene.nodes.push(SceneNode::Text {
1596 rect: repose_core::Rect {
1597 x: label_x + gap,
1598 y: rect.y,
1599 w: rect.x + rect.w - (label_x + gap),
1600 h: rect.h,
1601 },
1602 text: format!("{}: {:.2} – {:.2}", label, *start, *end),
1603 color: theme.on_surface,
1604 size: 16.0,
1605 });
1606
1607 let on_change_cb = on_change.as_ref().cloned();
1609 let minv = *min;
1610 let maxv = *max;
1611 let stepv = *step;
1612 let start_val = *start;
1613 let end_val = *end;
1614
1615 let active = Rc::new(RefCell::new(None::<u8>));
1617
1618 let update = {
1620 let active = active.clone();
1621 let on_change_cb = on_change_cb.clone();
1622 Rc::new(move |px: f32| {
1623 if let Some(thumb) = *active.borrow() {
1624 let tt = clamp01((px - track_x) / track_w);
1625 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
1626 match thumb {
1627 0 => {
1628 let new_start = v.min(end_val).min(maxv).max(minv);
1629 if let Some(cb) = &on_change_cb {
1630 cb(new_start, end_val);
1631 }
1632 }
1633 _ => {
1634 let new_end = v.max(start_val).max(minv).min(maxv);
1635 if let Some(cb) = &on_change_cb {
1636 cb(start_val, new_end);
1637 }
1638 }
1639 }
1640 }
1641 })
1642 };
1643
1644 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1646 let active = active.clone();
1647 let update = update.clone();
1648 let k0x0 = k0x;
1650 let k1x0 = k1x;
1651 Rc::new(move |pe| {
1652 let px = pe.position.x;
1653 let d0 = (px - k0x0).abs();
1654 let d1 = (px - k1x0).abs();
1655 *active.borrow_mut() = Some(if d0 <= d1 { 0 } else { 1 });
1656 update(px);
1657 })
1658 };
1659
1660 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1662 let active = active.clone();
1663 let update = update.clone();
1664 Rc::new(move |pe| {
1665 if active.borrow().is_some() {
1666 update(pe.position.x);
1667 }
1668 })
1669 };
1670
1671 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1673 let active = active.clone();
1674 Rc::new(move |_pe| {
1675 *active.borrow_mut() = None;
1676 })
1677 };
1678
1679 hits.push(HitRegion {
1680 id: v.id,
1681 rect,
1682 on_click: None,
1683 on_scroll: None,
1684 focusable: true,
1685 on_pointer_down: Some(on_pd),
1686 on_pointer_move: Some(on_pm),
1687 on_pointer_up: Some(on_pu),
1688 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1689 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1690 z_index: v.modifier.z_index,
1691 on_text_change: None,
1692 });
1693 sems.push(SemNode {
1694 id: v.id,
1695 role: Role::Slider,
1696 label: Some(label.clone()),
1697 rect,
1698 focused: is_focused,
1699 enabled: true,
1700 });
1701 if is_focused {
1702 scene.nodes.push(SceneNode::Border {
1703 rect,
1704 color: Color::from_hex("#88CCFF"),
1705 width: 2.0,
1706 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1707 });
1708 }
1709 }
1710 ViewKind::ProgressBar {
1711 value,
1712 min,
1713 max,
1714 label,
1715 circular,
1716 } => {
1717 let theme = locals::theme();
1718 let track_h = 6.0f32;
1719 let gap = 8.0f32;
1720 let label_w_split = rect.w * 0.6;
1721 let track_x = rect.x;
1722 let track_w = (label_w_split - track_x).max(60.0);
1723 let cy = rect.y + rect.h * 0.5;
1724
1725 scene.nodes.push(SceneNode::Rect {
1726 rect: repose_core::Rect {
1727 x: track_x,
1728 y: cy - track_h * 0.5,
1729 w: track_w,
1730 h: track_h,
1731 },
1732 color: Color::from_hex("#333333"),
1733 radius: track_h * 0.5,
1734 });
1735
1736 let t = clamp01(norm(*value, *min, *max));
1737 scene.nodes.push(SceneNode::Rect {
1738 rect: repose_core::Rect {
1739 x: track_x,
1740 y: cy - track_h * 0.5,
1741 w: track_w * t,
1742 h: track_h,
1743 },
1744 color: theme.primary,
1745 radius: track_h * 0.5,
1746 });
1747
1748 scene.nodes.push(SceneNode::Text {
1749 rect: repose_core::Rect {
1750 x: rect.x + label_w_split + gap,
1751 y: rect.y,
1752 w: rect.w - (label_w_split + gap),
1753 h: rect.h,
1754 },
1755 text: format!("{}: {:.0}%", label, t * 100.0),
1756 color: theme.on_surface,
1757 size: 16.0,
1758 });
1759
1760 sems.push(SemNode {
1761 id: v.id,
1762 role: Role::ProgressBar,
1763 label: Some(label.clone()),
1764 rect,
1765 focused: is_focused,
1766 enabled: true,
1767 });
1768 }
1769
1770 _ => {}
1771 }
1772
1773 for c in &v.children {
1775 walk(
1776 c,
1777 t,
1778 nodes,
1779 scene,
1780 hits,
1781 sems,
1782 textfield_states,
1783 interactions,
1784 focused,
1785 base,
1786 );
1787 }
1788 }
1789
1790 walk(
1792 &root,
1793 &taffy,
1794 &nodes_map,
1795 &mut scene,
1796 &mut hits,
1797 &mut sems,
1798 textfield_states,
1799 interactions,
1800 focused,
1801 (0.0, 0.0),
1802 );
1803
1804 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
1806
1807 (scene, hits, sems)
1808}