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::Overflow;
21use taffy::style::{AlignItems, Dimension, Display, FlexDirection, JustifyContent, Style};
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 },
68 )
69 .modifier(modifier)
70}
71
72pub fn Text(text: impl Into<String>) -> View {
73 View::new(
74 0,
75 ViewKind::Text {
76 text: text.into(),
77 color: Color::WHITE,
78 font_size: 16.0,
79 },
80 )
81}
82
83pub fn Spacer() -> View {
84 Box(Modifier::new().flex_grow(1.0))
85}
86
87pub fn Grid(columns: usize, modifier: Modifier, children: Vec<View>) -> View {
88 Column(modifier.grid(columns, 0.0, 0.0)).with_children(children)
89}
90
91#[allow(non_snake_case)]
92pub fn TextColor(mut v: View, color: Color) -> View {
93 if let ViewKind::Text {
94 color: text_color, ..
95 } = &mut v.kind
96 {
97 *text_color = color;
98 }
99 v
100}
101
102#[allow(non_snake_case)]
103pub fn TextSize(mut v: View, size: f32) -> View {
104 if let ViewKind::Text {
105 font_size: text_size,
106 ..
107 } = &mut v.kind
108 {
109 *text_size = size;
110 }
111 v
112}
113
114pub fn Button(text: impl Into<String>, on_click: impl Fn() + 'static) -> View {
115 View::new(
116 0,
117 ViewKind::Button {
118 text: text.into(),
119 on_click: Some(Rc::new(on_click)),
120 },
121 )
122 .semantics(Semantics {
123 role: Role::Button,
124 label: None,
125 focused: false,
126 enabled: true,
127 })
128}
129
130pub fn Checkbox(
131 checked: bool,
132 label: impl Into<String>,
133 on_change: impl Fn(bool) + 'static,
134) -> View {
135 View::new(
136 0,
137 ViewKind::Checkbox {
138 checked,
139 label: label.into(),
140 on_change: Some(Rc::new(on_change)),
141 },
142 )
143 .semantics(Semantics {
144 role: Role::Checkbox,
145 label: None,
146 focused: false,
147 enabled: true,
148 })
149}
150
151pub fn RadioButton(
152 selected: bool,
153 label: impl Into<String>,
154 on_select: impl Fn() + 'static,
155) -> View {
156 View::new(
157 0,
158 ViewKind::RadioButton {
159 selected,
160 label: label.into(),
161 on_select: Some(Rc::new(on_select)),
162 },
163 )
164 .semantics(Semantics {
165 role: Role::RadioButton,
166 label: None,
167 focused: false,
168 enabled: true,
169 })
170}
171
172pub fn Switch(checked: bool, label: impl Into<String>, on_change: impl Fn(bool) + 'static) -> View {
173 View::new(
174 0,
175 ViewKind::Switch {
176 checked,
177 label: label.into(),
178 on_change: Some(Rc::new(on_change)),
179 },
180 )
181 .semantics(Semantics {
182 role: Role::Switch,
183 label: None,
184 focused: false,
185 enabled: true,
186 })
187}
188
189pub fn Slider(
190 value: f32,
191 range: (f32, f32),
192 step: Option<f32>,
193 label: impl Into<String>,
194 on_change: impl Fn(f32) + 'static,
195) -> View {
196 View::new(
197 0,
198 ViewKind::Slider {
199 value,
200 min: range.0,
201 max: range.1,
202 step,
203 label: label.into(),
204 on_change: Some(Rc::new(on_change)),
205 },
206 )
207 .semantics(Semantics {
208 role: Role::Slider,
209 label: None,
210 focused: false,
211 enabled: true,
212 })
213}
214
215pub fn RangeSlider(
216 start: f32,
217 end: f32,
218 range: (f32, f32),
219 step: Option<f32>,
220 label: impl Into<String>,
221 on_change: impl Fn(f32, f32) + 'static,
222) -> View {
223 View::new(
224 0,
225 ViewKind::RangeSlider {
226 start,
227 end,
228 min: range.0,
229 max: range.1,
230 step,
231 label: label.into(),
232 on_change: Some(Rc::new(on_change)),
233 },
234 )
235 .semantics(Semantics {
236 role: Role::Slider,
237 label: None,
238 focused: false,
239 enabled: true,
240 })
241}
242
243pub fn ProgressBar(value: f32, range: (f32, f32), label: impl Into<String>) -> View {
244 View::new(
245 0,
246 ViewKind::ProgressBar {
247 value,
248 min: range.0,
249 max: range.1,
250 label: label.into(),
251 circular: false,
252 },
253 )
254 .semantics(Semantics {
255 role: Role::ProgressBar,
256 label: None,
257 focused: false,
258 enabled: true,
259 })
260}
261
262fn flex_dir_for(kind: &ViewKind) -> Option<FlexDirection> {
263 match kind {
264 ViewKind::Row => {
265 if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
266 Some(FlexDirection::RowReverse)
267 } else {
268 Some(FlexDirection::Row)
269 }
270 }
271 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => {
272 Some(FlexDirection::Column)
273 }
274 _ => None,
275 }
276}
277
278pub trait ViewExt: Sized {
280 fn child(self, children: impl IntoChildren) -> Self;
281}
282
283impl ViewExt for View {
284 fn child(self, children: impl IntoChildren) -> Self {
285 self.with_children(children.into_children())
286 }
287}
288
289pub trait IntoChildren {
290 fn into_children(self) -> Vec<View>;
291}
292
293impl IntoChildren for View {
294 fn into_children(self) -> Vec<View> {
295 vec![self]
296 }
297}
298
299impl IntoChildren for Vec<View> {
300 fn into_children(self) -> Vec<View> {
301 self
302 }
303}
304
305impl<const N: usize> IntoChildren for [View; N] {
306 fn into_children(self) -> Vec<View> {
307 self.into()
308 }
309}
310
311macro_rules! impl_into_children_tuple {
313 ($($idx:tt $t:ident),+) => {
314 impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
315 fn into_children(self) -> Vec<View> {
316 let mut v = Vec::new();
317 $(v.extend(self.$idx.into_children());)+
318 v
319 }
320 }
321 };
322}
323
324impl_into_children_tuple!(0 A, 1 B);
325impl_into_children_tuple!(0 A, 1 B, 2 C);
326impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
327impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
328impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
329impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
330impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
331
332pub fn layout_and_paint(
334 root: &View,
335 size: (u32, u32),
336 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
337 interactions: &Interactions,
338 focused: Option<u64>,
339) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
340 let mut id = 1u64;
342 fn stamp(mut v: View, id: &mut u64) -> View {
343 v.id = *id;
344 *id += 1;
345 v.children = v.children.into_iter().map(|c| stamp(c, id)).collect();
346 v
347 }
348 let root = stamp(root.clone(), &mut id);
349
350 use taffy::prelude::*;
352 #[derive(Clone)]
353 enum NodeCtx {
354 Text { text: String, font_px: f32 },
355 Button { label: String },
356 TextField,
357 Container,
358 ScrollContainer,
359 Checkbox { label: String },
360 Radio { label: String },
361 Switch { label: String },
362 Slider { label: String },
363 Range { label: String },
364 Progress { label: String },
365 }
366
367 let mut taffy: TaffyTree<NodeCtx> = TaffyTree::new();
368 let mut nodes_map = HashMap::new();
369
370 fn style_from_modifier(m: &Modifier, kind: &ViewKind) -> Style {
371 let mut s = Style::default();
372 s.display = match kind {
373 ViewKind::Row => Display::Flex,
374 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => Display::Flex,
375 ViewKind::Stack => Display::Grid, _ => Display::Flex,
377 };
378 if matches!(kind, ViewKind::Row) {
379 s.flex_direction = FlexDirection::Row;
380 }
381 if matches!(
382 kind,
383 ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. }
384 ) {
385 s.align_items = Some(AlignItems::Stretch);
386 } else {
387 s.align_items = Some(AlignItems::FlexStart);
388 }
389 s.justify_content = Some(JustifyContent::FlexStart);
390
391 if let Some(r) = m.aspect_ratio {
392 s.aspect_ratio = Some(r);
393 }
394
395 if let Some(g) = m.flex_grow {
397 s.flex_grow = g;
398 }
399 if let Some(sh) = m.flex_shrink {
400 s.flex_shrink = sh;
401 }
402 if let Some(b) = m.flex_basis {
403 s.flex_basis = length(b);
404 }
405
406 if let Some(a) = m.align_self {
408 s.align_self = Some(a);
409 }
410
411 if let Some(repose_core::modifier::PositionType::Absolute) = m.position_type {
413 s.position = Position::Absolute;
414 s.inset = taffy::geometry::Rect {
415 left: m.offset_left.map(length).unwrap_or_else(auto),
416 right: m.offset_right.map(length).unwrap_or_else(auto),
417 top: m.offset_top.map(length).unwrap_or_else(auto),
418 bottom: m.offset_bottom.map(length).unwrap_or_else(auto),
419 };
420 }
421
422 if let Some(cfg) = &m.grid {
424 s.display = Display::Grid;
425
426 s.grid_template_columns = (0..cfg.columns)
428 .map(|_| GridTemplateComponent::Single(flex(1.0f32)))
429 .collect();
430
431 s.gap = Size {
433 width: length(cfg.column_gap),
434 height: length(cfg.row_gap),
435 };
436 }
437
438 if matches!(kind, ViewKind::ScrollV { .. }) {
440 s.overflow = taffy::Point {
441 x: Overflow::Hidden,
442 y: Overflow::Hidden,
443 };
444 }
445
446 if let Some(dir) = flex_dir_for(kind) {
447 s.flex_direction = dir;
448 }
449 if let Some(p) = m.padding {
450 let v = length(p);
451 s.padding = taffy::geometry::Rect {
452 left: v,
453 right: v,
454 top: v,
455 bottom: v,
456 };
457 }
458
459 let mut width_set = false;
460 let mut height_set = false;
461
462 if let Some(sz) = m.size {
463 if sz.width.is_finite() {
464 s.size.width = length(sz.width);
465 width_set = true;
466 }
467 if sz.height.is_finite() {
468 s.size.height = length(sz.height);
469 height_set = true;
470 }
471 }
472
473 if let Some(w) = m.width {
475 s.size.width = length(w);
476 width_set = true;
477 }
478 if let Some(h) = m.height {
479 s.size.height = length(h);
480 height_set = true;
481 }
482
483 if m.fill_max || m.fill_max_w {
485 if !width_set {
486 s.size.width = percent(1.0);
487 s.flex_grow = s.flex_grow.max(1.0);
488 s.flex_shrink = s.flex_shrink.max(1.0);
489 }
490 }
491 if m.fill_max || m.fill_max_h {
492 if !height_set {
493 s.size.height = percent(1.0);
494 s.flex_grow = s.flex_grow.max(1.0);
495 s.flex_shrink = s.flex_shrink.max(1.0);
496 }
497 }
498
499 if let Some(pv) = m.padding_values {
501 s.padding = taffy::geometry::Rect {
502 left: length(pv.left),
503 right: length(pv.right),
504 top: length(pv.top),
505 bottom: length(pv.bottom),
506 };
507 } else if let Some(p) = m.padding {
508 let v = length(p);
509 s.padding = taffy::geometry::Rect {
510 left: v,
511 right: v,
512 top: v,
513 bottom: v,
514 };
515 }
516
517 if m.fill_max {
518 s.size.width = percent(1.0);
519 s.size.height = percent(1.0);
520 s.flex_grow = 1.0;
521 s.flex_shrink = 1.0;
522 }
523 s
524 }
525
526 fn build_node(
527 v: &View,
528 t: &mut TaffyTree<NodeCtx>,
529 nodes_map: &mut HashMap<ViewId, taffy::NodeId>,
530 ) -> taffy::NodeId {
531 let mut style = style_from_modifier(&v.modifier, &v.kind);
532
533 if v.modifier.grid_col_span.is_some() || v.modifier.grid_row_span.is_some() {
534 use taffy::prelude::{GridPlacement, Line};
535
536 let col_span = v.modifier.grid_col_span.unwrap_or(1).max(1);
537 let row_span = v.modifier.grid_row_span.unwrap_or(1).max(1);
538
539 style.grid_column = Line {
540 start: GridPlacement::Auto,
541 end: GridPlacement::Span(col_span),
542 };
543 style.grid_row = Line {
544 start: GridPlacement::Auto,
545 end: GridPlacement::Span(row_span),
546 };
547 }
548
549 let children: Vec<_> = v
550 .children
551 .iter()
552 .map(|c| build_node(c, t, nodes_map))
553 .collect();
554
555 let node = match &v.kind {
556 ViewKind::Text {
557 text, font_size, ..
558 } => t
559 .new_leaf_with_context(
560 style,
561 NodeCtx::Text {
562 text: text.clone(),
563 font_px: *font_size,
564 },
565 )
566 .unwrap(),
567 ViewKind::Button { text, .. } => t
568 .new_leaf_with_context(
569 style,
570 NodeCtx::Button {
571 label: text.clone(),
572 },
573 )
574 .unwrap(),
575 ViewKind::TextField { .. } => {
576 t.new_leaf_with_context(style, NodeCtx::TextField).unwrap()
577 }
578 ViewKind::Checkbox { label, .. } => t
579 .new_leaf_with_context(
580 style,
581 NodeCtx::Checkbox {
582 label: label.clone(),
583 },
584 )
585 .unwrap(),
586 ViewKind::RadioButton { label, .. } => t
587 .new_leaf_with_context(
588 style,
589 NodeCtx::Radio {
590 label: label.clone(),
591 },
592 )
593 .unwrap(),
594 ViewKind::Switch { label, .. } => t
595 .new_leaf_with_context(
596 style,
597 NodeCtx::Switch {
598 label: label.clone(),
599 },
600 )
601 .unwrap(),
602 ViewKind::Slider { label, .. } => t
603 .new_leaf_with_context(
604 style,
605 NodeCtx::Slider {
606 label: label.clone(),
607 },
608 )
609 .unwrap(),
610 ViewKind::RangeSlider { label, .. } => t
611 .new_leaf_with_context(
612 style,
613 NodeCtx::Range {
614 label: label.clone(),
615 },
616 )
617 .unwrap(),
618 ViewKind::ProgressBar { label, .. } => t
619 .new_leaf_with_context(
620 style,
621 NodeCtx::Progress {
622 label: label.clone(),
623 },
624 )
625 .unwrap(),
626 ViewKind::ScrollV { .. } => {
627 let children: Vec<_> = v
628 .children
629 .iter()
630 .map(|c| build_node(c, t, nodes_map))
631 .collect();
632
633 let n = t.new_with_children(style, &children).unwrap();
634 t.set_node_context(n, Some(NodeCtx::ScrollContainer)).ok();
635 n
636 }
637 _ => {
638 let n = t.new_with_children(style, &children).unwrap();
639 t.set_node_context(n, Some(NodeCtx::Container)).ok();
640 n
641 }
642 };
643
644 nodes_map.insert(v.id, node);
645 node
646 }
647
648 let root_node = build_node(&root, &mut taffy, &mut nodes_map);
649
650 let available = taffy::geometry::Size {
651 width: AvailableSpace::Definite(size.0 as f32),
652 height: AvailableSpace::Definite(size.1 as f32),
653 };
654
655 taffy
657 .compute_layout_with_measure(root_node, available, |known, avail, _node, ctx, _style| {
658 match ctx {
659 Some(NodeCtx::Text { text, font_px }) => {
660 let approx_w = text.len() as f32 * *font_px * 0.6;
661 let w = known.width.unwrap_or(approx_w);
662 taffy::geometry::Size {
663 width: w,
664 height: *font_px * 1.3,
665 }
666 }
667 Some(NodeCtx::Button { label }) => taffy::geometry::Size {
668 width: (label.len() as f32 * 16.0 * 0.6) + 24.0,
669 height: 36.0,
670 },
671 Some(NodeCtx::TextField) => {
672 let w = known.width.unwrap_or(220.0);
673 taffy::geometry::Size {
674 width: w,
675 height: 36.0,
676 }
677 }
678 Some(NodeCtx::Checkbox { label }) => {
679 let label_w = (label.len() as f32) * 16.0 * 0.6;
680 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
682 width: known.width.unwrap_or(w),
683 height: 24.0,
684 }
685 }
686 Some(NodeCtx::Radio { label }) => {
687 let label_w = (label.len() as f32) * 16.0 * 0.6;
688 let w = 24.0 + 8.0 + label_w; taffy::geometry::Size {
690 width: known.width.unwrap_or(w),
691 height: 24.0,
692 }
693 }
694 Some(NodeCtx::Switch { label }) => {
695 let label_w = (label.len() as f32) * 16.0 * 0.6;
696 let w = 46.0 + 8.0 + label_w; taffy::geometry::Size {
698 width: known.width.unwrap_or(w),
699 height: 28.0,
700 }
701 }
702 Some(NodeCtx::Slider { label }) => {
703 let label_w = (label.len() as f32) * 16.0 * 0.6;
704 let w = (known.width).unwrap_or(200.0f32.max(46.0 + 8.0 + label_w));
705 taffy::geometry::Size {
706 width: w,
707 height: 28.0,
708 }
709 }
710 Some(NodeCtx::Range { label }) => {
711 let label_w = (label.len() as f32) * 16.0 * 0.6;
712 let w = (known.width).unwrap_or(220.0f32.max(46.0 + 8.0 + label_w));
713 taffy::geometry::Size {
714 width: w,
715 height: 28.0,
716 }
717 }
718 Some(NodeCtx::Progress { label }) => {
719 let label_w = (label.len() as f32) * 16.0 * 0.6;
720 let w = (known.width).unwrap_or(200.0f32.max(100.0 + 8.0 + label_w));
721 taffy::geometry::Size {
722 width: w,
723 height: 12.0 + 8.0,
724 } }
726 Some(NodeCtx::ScrollContainer) => {
727 taffy::geometry::Size {
728 width: known.width.unwrap_or_else(|| {
729 match avail.width {
730 AvailableSpace::Definite(w) => w,
731 _ => 300.0, }
733 }),
734 height: known.height.unwrap_or_else(|| {
735 match avail.height {
736 AvailableSpace::Definite(h) => h,
737 _ => 600.0, }
739 }),
740 }
741 }
742 Some(NodeCtx::Container) | None => taffy::geometry::Size::ZERO,
743 }
744 })
745 .unwrap();
746
747 fn layout_of(node: taffy::NodeId, t: &TaffyTree<impl Clone>) -> repose_core::Rect {
755 let l = t.layout(node).unwrap();
756 repose_core::Rect {
757 x: l.location.x,
758 y: l.location.y,
759 w: l.size.width,
760 h: l.size.height,
761 }
762 }
763
764 fn add_offset(mut r: repose_core::Rect, off: (f32, f32)) -> repose_core::Rect {
765 r.x += off.0;
766 r.y += off.1;
767 r
768 }
769
770 fn clamp01(x: f32) -> f32 {
771 x.max(0.0).min(1.0)
772 }
773 fn norm(value: f32, min: f32, max: f32) -> f32 {
774 if max > min {
775 (value - min) / (max - min)
776 } else {
777 0.0
778 }
779 }
780 fn denorm(t: f32, min: f32, max: f32) -> f32 {
781 min + t * (max - min)
782 }
783 fn snap_step(v: f32, step: Option<f32>, min: f32, max: f32) -> f32 {
784 match step {
785 Some(s) if s > 0.0 => {
786 let k = ((v - min) / s).round();
787 (min + k * s).clamp(min, max)
788 }
789 _ => v.clamp(min, max),
790 }
791 }
792 fn mul_alpha(c: Color, a: f32) -> Color {
793 let mut out = c;
794 let na = ((c.3 as f32) * a).clamp(0.0, 255.0) as u8;
795 out.3 = na;
796 out
797 }
798
799 let mut scene = Scene {
800 clear_color: locals::theme().background,
801 nodes: vec![],
802 };
803 let mut hits: Vec<HitRegion> = vec![];
804 let mut sems: Vec<SemNode> = vec![];
805
806 fn walk(
807 v: &View,
808 t: &TaffyTree<NodeCtx>,
809 nodes: &HashMap<ViewId, taffy::NodeId>,
810 scene: &mut Scene,
811 hits: &mut Vec<HitRegion>,
812 sems: &mut Vec<SemNode>,
813 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
814 interactions: &Interactions,
815 focused: Option<u64>,
816 parent_offset: (f32, f32),
817 alpha_accum: f32,
818 ) {
819 let local = layout_of(nodes[&v.id], t);
820 let rect = add_offset(local, parent_offset);
821 let base = (parent_offset.0 + local.x, parent_offset.1 + local.y);
822
823 let is_hovered = interactions.hover == Some(v.id);
824 let is_pressed = interactions.pressed.contains(&v.id);
825 let is_focused = focused == Some(v.id);
826
827 if let Some(bg) = v.modifier.background {
829 scene.nodes.push(SceneNode::Rect {
830 rect,
831 color: mul_alpha(bg, alpha_accum),
832 radius: v.modifier.clip_rounded.unwrap_or(0.0),
833 });
834 }
835
836 if let Some(b) = &v.modifier.border {
838 scene.nodes.push(SceneNode::Border {
839 rect,
840 color: mul_alpha(b.color, alpha_accum),
841 width: b.width,
842 radius: b.radius.max(v.modifier.clip_rounded.unwrap_or(0.0)),
843 });
844 }
845
846 let this_alpha = v.modifier.alpha.unwrap_or(1.0);
848 let alpha_accum = (alpha_accum * this_alpha).clamp(0.0, 1.0);
849
850 if let Some(tf) = v.modifier.transform {
851 scene.nodes.push(SceneNode::PushTransform { transform: tf });
852 }
853
854 if let Some(p) = &v.modifier.painter {
856 (p)(scene, rect);
857 }
858
859 let has_pointer = v.modifier.on_pointer_down.is_some()
860 || v.modifier.on_pointer_move.is_some()
861 || v.modifier.on_pointer_up.is_some()
862 || v.modifier.on_pointer_enter.is_some()
863 || v.modifier.on_pointer_leave.is_some();
864
865 if has_pointer || v.modifier.click {
866 hits.push(HitRegion {
867 id: v.id,
868 rect,
869 on_click: None, on_scroll: None, focusable: false,
872 on_pointer_down: v.modifier.on_pointer_down.clone(),
873 on_pointer_move: v.modifier.on_pointer_move.clone(),
874 on_pointer_up: v.modifier.on_pointer_up.clone(),
875 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
876 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
877 z_index: v.modifier.z_index,
878 on_text_change: None,
879 on_text_submit: None,
880 });
881 }
882
883 match &v.kind {
884 ViewKind::Text {
885 text,
886 color,
887 font_size,
888 } => {
889 let scaled_size = *font_size * locals::text_scale().0;
891 scene.nodes.push(SceneNode::Text {
892 rect,
893 text: text.clone(),
894 color: mul_alpha(*color, alpha_accum),
895 size: scaled_size,
896 });
897 sems.push(SemNode {
898 id: v.id,
899 role: Role::Text,
900 label: Some(text.clone()),
901 rect,
902 focused: is_focused,
903 enabled: true,
904 });
905 }
906
907 ViewKind::Button { text, on_click } => {
908 if v.modifier.background.is_none() {
910 let base = if is_pressed {
911 Color::from_hex("#1f7556")
912 } else if is_hovered {
913 Color::from_hex("#2a8f6a")
914 } else {
915 Color::from_hex("#34af82")
916 };
917 scene.nodes.push(SceneNode::Rect {
918 rect,
919 color: mul_alpha(base, alpha_accum),
920 radius: v.modifier.clip_rounded.unwrap_or(6.0),
921 });
922 }
923 let px = 16.0;
925 let approx_w = (text.len() as f32) * px * 0.6;
926 let tx = rect.x + (rect.w - approx_w).max(0.0) * 0.5;
927 let ty = rect.y + (rect.h - px).max(0.0) * 0.5;
928 scene.nodes.push(SceneNode::Text {
929 rect: repose_core::Rect {
930 x: tx,
931 y: ty,
932 w: approx_w,
933 h: px,
934 },
935 text: text.clone(),
936 color: mul_alpha(Color::WHITE, alpha_accum),
937 size: px,
938 });
939
940 if v.modifier.click || on_click.is_some() {
941 hits.push(HitRegion {
942 id: v.id,
943 rect,
944 on_click: on_click.clone(),
945 on_scroll: None,
946 focusable: true,
947 on_pointer_down: v.modifier.on_pointer_down.clone(),
948 on_pointer_move: v.modifier.on_pointer_move.clone(),
949 on_pointer_up: v.modifier.on_pointer_up.clone(),
950 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
951 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
952 z_index: v.modifier.z_index,
953 on_text_change: None,
954 on_text_submit: None,
955 });
956 }
957 sems.push(SemNode {
958 id: v.id,
959 role: Role::Button,
960 label: Some(text.clone()),
961 rect,
962 focused: is_focused,
963 enabled: true,
964 });
965 if is_focused {
967 scene.nodes.push(SceneNode::Border {
968 rect,
969 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
970 width: 2.0,
971 radius: v.modifier.clip_rounded.unwrap_or(6.0),
972 });
973 }
974 }
975
976 ViewKind::TextField {
977 hint,
978 on_change,
979 on_submit,
980 ..
981 } => {
982 hits.push(HitRegion {
983 id: v.id,
984 rect,
985 on_click: None,
986 on_scroll: None,
987 focusable: true,
988 on_pointer_down: None,
989 on_pointer_move: None,
990 on_pointer_up: None,
991 on_pointer_enter: None,
992 on_pointer_leave: None,
993 z_index: v.modifier.z_index,
994 on_text_change: on_change.clone(),
995 on_text_submit: on_submit.clone(),
996 });
997
998 let inner = repose_core::Rect {
1000 x: rect.x + TF_PADDING_X,
1001 y: rect.y + 8.0,
1002 w: rect.w - 2.0 * TF_PADDING_X,
1003 h: rect.h - 16.0,
1004 };
1005 scene.nodes.push(SceneNode::PushClip {
1006 rect: inner,
1007 radius: 0.0,
1008 });
1009 if is_focused {
1011 scene.nodes.push(SceneNode::Border {
1012 rect,
1013 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1014 width: 2.0,
1015 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1016 });
1017 }
1018 if let Some(state_rc) = textfield_states.get(&v.id) {
1019 state_rc.borrow_mut().set_inner_width(inner.w);
1020
1021 let state = state_rc.borrow();
1022 let text = &state.text;
1023 let px = TF_FONT_PX as u32;
1024 let m = measure_text(text, px);
1025
1026 if state.selection.start != state.selection.end {
1028 let i0 = byte_to_char_index(&m, state.selection.start);
1029 let i1 = byte_to_char_index(&m, state.selection.end);
1030 let sx = m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1031 let ex = m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
1032 let sel_x = inner.x + sx.max(0.0);
1033 let sel_w = (ex - sx).max(0.0);
1034 scene.nodes.push(SceneNode::Rect {
1035 rect: repose_core::Rect {
1036 x: sel_x,
1037 y: inner.y,
1038 w: sel_w,
1039 h: inner.h,
1040 },
1041 color: mul_alpha(Color::from_hex("#3B7BFF55"), alpha_accum),
1042 radius: 0.0,
1043 });
1044 }
1045
1046 if let Some(range) = &state.composition {
1048 if range.start < range.end && !text.is_empty() {
1049 let i0 = byte_to_char_index(&m, range.start);
1050 let i1 = byte_to_char_index(&m, range.end);
1051 let sx =
1052 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1053 let ex =
1054 m.positions.get(i1).copied().unwrap_or(sx) - state.scroll_offset;
1055 let ux = inner.x + sx.max(0.0);
1056 let uw = (ex - sx).max(0.0);
1057 scene.nodes.push(SceneNode::Rect {
1058 rect: repose_core::Rect {
1059 x: ux,
1060 y: inner.y + inner.h - 2.0,
1061 w: uw,
1062 h: 2.0,
1063 },
1064 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1065 radius: 0.0,
1066 });
1067 }
1068 }
1069
1070 scene.nodes.push(SceneNode::Text {
1072 rect: repose_core::Rect {
1073 x: inner.x - state.scroll_offset,
1074 y: inner.y,
1075 w: inner.w,
1076 h: inner.h,
1077 },
1078 text: if text.is_empty() {
1079 hint.clone()
1080 } else {
1081 text.clone()
1082 },
1083 color: if text.is_empty() {
1084 mul_alpha(Color::from_hex("#666666"), alpha_accum)
1085 } else {
1086 mul_alpha(Color::from_hex("#CCCCCC"), alpha_accum)
1087 },
1088 size: TF_FONT_PX,
1089 });
1090
1091 if state.selection.start == state.selection.end && state.caret_visible() {
1093 let i = byte_to_char_index(&m, state.selection.end);
1094 let cx = m.positions.get(i).copied().unwrap_or(0.0) - state.scroll_offset;
1095 let caret_x = inner.x + cx.max(0.0);
1096 scene.nodes.push(SceneNode::Rect {
1097 rect: repose_core::Rect {
1098 x: caret_x,
1099 y: inner.y,
1100 w: 1.0,
1101 h: inner.h,
1102 },
1103 color: mul_alpha(Color::WHITE, alpha_accum),
1104 radius: 0.0,
1105 });
1106 }
1107 scene.nodes.push(SceneNode::PopClip);
1109
1110 sems.push(SemNode {
1111 id: v.id,
1112 role: Role::TextField,
1113 label: Some(text.clone()),
1114 rect,
1115 focused: is_focused,
1116 enabled: true,
1117 });
1118 } else {
1119 scene.nodes.push(SceneNode::Text {
1121 rect: repose_core::Rect {
1122 x: inner.x,
1123 y: inner.y,
1124 w: inner.w,
1125 h: inner.h,
1126 },
1127 text: hint.clone(),
1128 color: mul_alpha(Color::from_hex("#666666"), alpha_accum),
1129 size: TF_FONT_PX,
1130 });
1131 sems.push(SemNode {
1132 id: v.id,
1133 role: Role::TextField,
1134 label: Some(hint.clone()),
1135 rect,
1136 focused: is_focused,
1137 enabled: true,
1138 });
1139 }
1140 }
1141 ViewKind::ScrollV {
1142 on_scroll,
1143 set_viewport_height,
1144 set_content_height,
1145 get_scroll_offset,
1146 } => {
1147 log::debug!("ScrollV: registering hit region at rect {:?}", rect);
1148
1149 hits.push(HitRegion {
1150 id: v.id,
1151 rect,
1152 on_click: None,
1153 on_scroll: on_scroll.clone(),
1154 focusable: false,
1155 on_pointer_down: v.modifier.on_pointer_down.clone(),
1156 on_pointer_move: v.modifier.on_pointer_move.clone(),
1157 on_pointer_up: v.modifier.on_pointer_up.clone(),
1158 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1159 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1160 z_index: v.modifier.z_index,
1161 on_text_change: None,
1162 on_text_submit: None,
1163 });
1164
1165 if let Some(set_vh) = set_viewport_height {
1166 set_vh(rect.h);
1167 }
1168
1169 let mut content_h = 0.0f32;
1171 for c in &v.children {
1172 let nid = nodes[&c.id];
1173 let l = t.layout(nid).unwrap();
1174 content_h = content_h.max(l.location.y + l.size.height);
1175 }
1176 if let Some(set_ch) = set_content_height {
1177 set_ch(content_h);
1178 }
1179
1180 scene.nodes.push(SceneNode::PushClip {
1181 rect,
1182 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1183 });
1184
1185 let scroll_offset = if let Some(get) = get_scroll_offset {
1186 let offset = get();
1187 log::debug!("ScrollV walk: applying scroll offset = {}", offset);
1188 offset
1189 } else {
1190 0.0
1191 };
1192
1193 let child_offset = (base.0, base.1 - scroll_offset);
1195 for c in &v.children {
1196 walk(
1197 c,
1198 t,
1199 nodes,
1200 scene,
1201 hits,
1202 sems,
1203 textfield_states,
1204 interactions,
1205 focused,
1206 child_offset,
1207 alpha_accum,
1208 );
1209 }
1210
1211 scene.nodes.push(SceneNode::PopClip);
1212 return;
1213 }
1214 ViewKind::ScrollXY {
1215 on_scroll,
1216 set_viewport_width,
1217 set_viewport_height,
1218 set_content_width,
1219 set_content_height,
1220 get_scroll_offset_xy,
1221 } => {
1222 hits.push(HitRegion {
1223 id: v.id,
1224 rect,
1225 on_click: None,
1226 on_scroll: on_scroll.clone(),
1227 focusable: false,
1228 on_pointer_down: v.modifier.on_pointer_down.clone(),
1229 on_pointer_move: v.modifier.on_pointer_move.clone(),
1230 on_pointer_up: v.modifier.on_pointer_up.clone(),
1231 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1232 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1233 z_index: v.modifier.z_index,
1234 on_text_change: None,
1235 on_text_submit: None,
1236 });
1237
1238 if let Some(set_w) = set_viewport_width {
1239 set_w(rect.w);
1240 }
1241 if let Some(set_h) = set_viewport_height {
1242 set_h(rect.h);
1243 }
1244
1245 let mut content_w = 0.0f32;
1247 let mut content_h = 0.0f32;
1248 for c in &v.children {
1249 let nid = nodes[&c.id];
1250 let l = t.layout(nid).unwrap();
1251 content_w = content_w.max(l.location.x + l.size.width);
1252 content_h = content_h.max(l.location.y + l.size.height);
1253 }
1254 if let Some(set_cw) = set_content_width {
1255 set_cw(content_w);
1256 }
1257 if let Some(set_ch) = set_content_height {
1258 set_ch(content_h);
1259 }
1260
1261 scene.nodes.push(SceneNode::PushClip {
1263 rect,
1264 radius: v.modifier.clip_rounded.unwrap_or(0.0),
1265 });
1266
1267 let (ox, oy) = if let Some(get) = get_scroll_offset_xy {
1269 let (x, y) = get();
1270 (x, y)
1271 } else {
1272 (0.0, 0.0)
1273 };
1274
1275 let child_offset = (base.0 - ox, base.1 - oy);
1277 for c in &v.children {
1278 walk(
1279 c,
1280 t,
1281 nodes,
1282 scene,
1283 hits,
1284 sems,
1285 textfield_states,
1286 interactions,
1287 focused,
1288 child_offset,
1289 alpha_accum,
1290 );
1291 }
1292
1293 scene.nodes.push(SceneNode::PopClip);
1294 return;
1295 }
1296 ViewKind::Checkbox {
1297 checked,
1298 label,
1299 on_change,
1300 } => {
1301 let theme = locals::theme();
1302 let box_size = 18.0f32;
1304 let bx = rect.x;
1305 let by = rect.y + (rect.h - box_size) * 0.5;
1306 scene.nodes.push(SceneNode::Rect {
1308 rect: repose_core::Rect {
1309 x: bx,
1310 y: by,
1311 w: box_size,
1312 h: box_size,
1313 },
1314 color: if *checked {
1315 mul_alpha(theme.primary, alpha_accum)
1316 } else {
1317 mul_alpha(theme.surface, alpha_accum)
1318 },
1319 radius: 3.0,
1320 });
1321 scene.nodes.push(SceneNode::Border {
1322 rect: repose_core::Rect {
1323 x: bx,
1324 y: by,
1325 w: box_size,
1326 h: box_size,
1327 },
1328 color: mul_alpha(Color::from_hex("#555555"), alpha_accum),
1329 width: 1.0,
1330 radius: 3.0,
1331 });
1332 if *checked {
1334 scene.nodes.push(SceneNode::Text {
1335 rect: repose_core::Rect {
1336 x: bx + 3.0,
1337 y: by + 1.0,
1338 w: box_size,
1339 h: box_size,
1340 },
1341 text: "✓".to_string(),
1342 color: mul_alpha(theme.on_primary, alpha_accum),
1343 size: 16.0,
1344 });
1345 }
1346 scene.nodes.push(SceneNode::Text {
1348 rect: repose_core::Rect {
1349 x: bx + box_size + 8.0,
1350 y: rect.y,
1351 w: rect.w - (box_size + 8.0),
1352 h: rect.h,
1353 },
1354 text: label.clone(),
1355 color: mul_alpha(theme.on_surface, alpha_accum),
1356 size: 16.0,
1357 });
1358
1359 let toggled = !*checked;
1361 let on_click = on_change.as_ref().map(|cb| {
1362 let cb = cb.clone();
1363 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1364 });
1365 hits.push(HitRegion {
1366 id: v.id,
1367 rect,
1368 on_click,
1369 on_scroll: None,
1370 focusable: true,
1371 on_pointer_down: v.modifier.on_pointer_down.clone(),
1372 on_pointer_move: v.modifier.on_pointer_move.clone(),
1373 on_pointer_up: v.modifier.on_pointer_up.clone(),
1374 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1375 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1376 z_index: v.modifier.z_index,
1377 on_text_change: None,
1378 on_text_submit: None,
1379 });
1380 sems.push(SemNode {
1381 id: v.id,
1382 role: Role::Checkbox,
1383 label: Some(label.clone()),
1384 rect,
1385 focused: is_focused,
1386 enabled: true,
1387 });
1388 if is_focused {
1389 scene.nodes.push(SceneNode::Border {
1390 rect,
1391 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1392 width: 2.0,
1393 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1394 });
1395 }
1396 }
1397
1398 ViewKind::RadioButton {
1399 selected,
1400 label,
1401 on_select,
1402 } => {
1403 let theme = locals::theme();
1404 let d = 18.0f32;
1405 let cx = rect.x;
1406 let cy = rect.y + (rect.h - d) * 0.5;
1407
1408 scene.nodes.push(SceneNode::Border {
1410 rect: repose_core::Rect {
1411 x: cx,
1412 y: cy,
1413 w: d,
1414 h: d,
1415 },
1416 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
1417 width: 1.5,
1418 radius: d * 0.5,
1419 });
1420 if *selected {
1422 scene.nodes.push(SceneNode::Rect {
1423 rect: repose_core::Rect {
1424 x: cx + 4.0,
1425 y: cy + 4.0,
1426 w: d - 8.0,
1427 h: d - 8.0,
1428 },
1429 color: mul_alpha(theme.primary, alpha_accum),
1430 radius: (d - 8.0) * 0.5,
1431 });
1432 }
1433 scene.nodes.push(SceneNode::Text {
1434 rect: repose_core::Rect {
1435 x: cx + d + 8.0,
1436 y: rect.y,
1437 w: rect.w - (d + 8.0),
1438 h: rect.h,
1439 },
1440 text: label.clone(),
1441 color: mul_alpha(theme.on_surface, alpha_accum),
1442 size: 16.0,
1443 });
1444
1445 hits.push(HitRegion {
1446 id: v.id,
1447 rect,
1448 on_click: on_select.clone(),
1449 on_scroll: None,
1450 focusable: true,
1451 on_pointer_down: v.modifier.on_pointer_down.clone(),
1452 on_pointer_move: v.modifier.on_pointer_move.clone(),
1453 on_pointer_up: v.modifier.on_pointer_up.clone(),
1454 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1455 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1456 z_index: v.modifier.z_index,
1457 on_text_change: None,
1458 on_text_submit: None,
1459 });
1460 sems.push(SemNode {
1461 id: v.id,
1462 role: Role::RadioButton,
1463 label: Some(label.clone()),
1464 rect,
1465 focused: is_focused,
1466 enabled: true,
1467 });
1468 if is_focused {
1469 scene.nodes.push(SceneNode::Border {
1470 rect,
1471 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1472 width: 2.0,
1473 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1474 });
1475 }
1476 }
1477
1478 ViewKind::Switch {
1479 checked,
1480 label,
1481 on_change,
1482 } => {
1483 let theme = locals::theme();
1484 let track_w = 46.0f32;
1486 let track_h = 26.0f32;
1487 let tx = rect.x;
1488 let ty = rect.y + (rect.h - track_h) * 0.5;
1489 let knob = 22.0f32;
1490 let on_col = theme.primary;
1491 let off_col = Color::from_hex("#333333");
1492
1493 scene.nodes.push(SceneNode::Rect {
1495 rect: repose_core::Rect {
1496 x: tx,
1497 y: ty,
1498 w: track_w,
1499 h: track_h,
1500 },
1501 color: if *checked {
1502 mul_alpha(on_col, alpha_accum)
1503 } else {
1504 mul_alpha(off_col, alpha_accum)
1505 },
1506 radius: track_h * 0.5,
1507 });
1508 let kx = if *checked {
1510 tx + track_w - knob - 2.0
1511 } else {
1512 tx + 2.0
1513 };
1514 let ky = ty + (track_h - knob) * 0.5;
1515 scene.nodes.push(SceneNode::Rect {
1516 rect: repose_core::Rect {
1517 x: kx,
1518 y: ky,
1519 w: knob,
1520 h: knob,
1521 },
1522 color: mul_alpha(Color::from_hex("#EEEEEE"), alpha_accum),
1523 radius: knob * 0.5,
1524 });
1525
1526 scene.nodes.push(SceneNode::Text {
1528 rect: repose_core::Rect {
1529 x: tx + track_w + 8.0,
1530 y: rect.y,
1531 w: rect.w - (track_w + 8.0),
1532 h: rect.h,
1533 },
1534 text: label.clone(),
1535 color: mul_alpha(theme.on_surface, alpha_accum),
1536 size: 16.0,
1537 });
1538
1539 let toggled = !*checked;
1540 let on_click = on_change.as_ref().map(|cb| {
1541 let cb = cb.clone();
1542 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1543 });
1544 hits.push(HitRegion {
1545 id: v.id,
1546 rect,
1547 on_click,
1548 on_scroll: None,
1549 focusable: true,
1550 on_pointer_down: v.modifier.on_pointer_down.clone(),
1551 on_pointer_move: v.modifier.on_pointer_move.clone(),
1552 on_pointer_up: v.modifier.on_pointer_up.clone(),
1553 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1554 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1555 z_index: v.modifier.z_index,
1556 on_text_change: None,
1557 on_text_submit: None,
1558 });
1559 sems.push(SemNode {
1560 id: v.id,
1561 role: Role::Switch,
1562 label: Some(label.clone()),
1563 rect,
1564 focused: is_focused,
1565 enabled: true,
1566 });
1567 if is_focused {
1568 scene.nodes.push(SceneNode::Border {
1569 rect,
1570 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1571 width: 2.0,
1572 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1573 });
1574 }
1575 }
1576 ViewKind::Slider {
1577 value,
1578 min,
1579 max,
1580 step,
1581 label,
1582 on_change,
1583 } => {
1584 let theme = locals::theme();
1585 let track_h = 4.0f32;
1587 let knob_d = 20.0f32;
1588 let gap = 8.0f32;
1589 let label_x = rect.x + rect.w * 0.6; let track_x = rect.x;
1591 let track_w = (label_x - track_x).max(60.0);
1592 let cy = rect.y + rect.h * 0.5;
1593
1594 scene.nodes.push(SceneNode::Rect {
1596 rect: repose_core::Rect {
1597 x: track_x,
1598 y: cy - track_h * 0.5,
1599 w: track_w,
1600 h: track_h,
1601 },
1602 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
1603 radius: track_h * 0.5,
1604 });
1605
1606 let t = clamp01(norm(*value, *min, *max));
1608 let kx = track_x + t * track_w;
1609 scene.nodes.push(SceneNode::Rect {
1610 rect: repose_core::Rect {
1611 x: kx - knob_d * 0.5,
1612 y: cy - knob_d * 0.5,
1613 w: knob_d,
1614 h: knob_d,
1615 },
1616 color: mul_alpha(theme.surface, alpha_accum),
1617 radius: knob_d * 0.5,
1618 });
1619 scene.nodes.push(SceneNode::Border {
1620 rect: repose_core::Rect {
1621 x: kx - knob_d * 0.5,
1622 y: cy - knob_d * 0.5,
1623 w: knob_d,
1624 h: knob_d,
1625 },
1626 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
1627 width: 1.0,
1628 radius: knob_d * 0.5,
1629 });
1630
1631 scene.nodes.push(SceneNode::Text {
1633 rect: repose_core::Rect {
1634 x: label_x + gap,
1635 y: rect.y,
1636 w: rect.x + rect.w - (label_x + gap),
1637 h: rect.h,
1638 },
1639 text: format!("{}: {:.2}", label, *value),
1640 color: mul_alpha(theme.on_surface, alpha_accum),
1641 size: 16.0,
1642 });
1643
1644 let on_change_cb: Option<Rc<dyn Fn(f32)>> = on_change.as_ref().cloned();
1646 let minv = *min;
1647 let maxv = *max;
1648 let stepv = *step;
1649
1650 let current = Rc::new(RefCell::new(*value));
1652
1653 let update_at = {
1655 let on_change_cb = on_change_cb.clone();
1656 let current = current.clone();
1657 Rc::new(move |px: f32| {
1658 let tt = clamp01((px - track_x) / track_w);
1659 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
1660 *current.borrow_mut() = v;
1661 if let Some(cb) = &on_change_cb {
1662 cb(v);
1663 }
1664 })
1665 };
1666
1667 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1669 let f = update_at.clone();
1670 Rc::new(move |pe| {
1671 f(pe.position.x);
1672 })
1673 };
1674
1675 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1677 let f = update_at.clone();
1678 Rc::new(move |pe| {
1679 f(pe.position.x);
1680 })
1681 };
1682
1683 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1685
1686 let on_scroll = {
1688 let on_change_cb = on_change_cb.clone();
1689 let current = current.clone();
1690 Rc::new(move |d: Vec2| -> Vec2 {
1691 let base = *current.borrow();
1692 let delta = stepv.unwrap_or((maxv - minv) * 0.01);
1693 let dir = if d.y.is_sign_negative() { 1.0 } else { -1.0 };
1695 let new_v = snap_step(base + dir * delta, stepv, minv, maxv);
1696 *current.borrow_mut() = new_v;
1697 if let Some(cb) = &on_change_cb {
1698 cb(new_v);
1699 }
1700 Vec2 { x: d.x, y: 0.0 } })
1702 };
1703
1704 hits.push(HitRegion {
1706 id: v.id,
1707 rect,
1708 on_click: None,
1709 on_scroll: Some(on_scroll),
1710 focusable: true,
1711 on_pointer_down: Some(on_pd),
1712 on_pointer_move: if is_pressed { Some(on_pm) } else { None },
1713 on_pointer_up: Some(on_pu),
1714 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1715 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1716 z_index: v.modifier.z_index,
1717 on_text_change: None,
1718 on_text_submit: None,
1719 });
1720
1721 sems.push(SemNode {
1722 id: v.id,
1723 role: Role::Slider,
1724 label: Some(label.clone()),
1725 rect,
1726 focused: is_focused,
1727 enabled: true,
1728 });
1729 if is_focused {
1730 scene.nodes.push(SceneNode::Border {
1731 rect,
1732 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1733 width: 2.0,
1734 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1735 });
1736 }
1737 }
1738 ViewKind::RangeSlider {
1739 start,
1740 end,
1741 min,
1742 max,
1743 step,
1744 label,
1745 on_change,
1746 } => {
1747 let theme = locals::theme();
1748 let track_h = 4.0f32;
1749 let knob_d = 20.0f32;
1750 let gap = 8.0f32;
1751 let label_x = rect.x + rect.w * 0.6;
1752 let track_x = rect.x;
1753 let track_w = (label_x - track_x).max(80.0);
1754 let cy = rect.y + rect.h * 0.5;
1755
1756 scene.nodes.push(SceneNode::Rect {
1758 rect: repose_core::Rect {
1759 x: track_x,
1760 y: cy - track_h * 0.5,
1761 w: track_w,
1762 h: track_h,
1763 },
1764 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
1765 radius: track_h * 0.5,
1766 });
1767
1768 let t0 = clamp01(norm(*start, *min, *max));
1770 let t1 = clamp01(norm(*end, *min, *max));
1771 let k0x = track_x + t0 * track_w;
1772 let k1x = track_x + t1 * track_w;
1773
1774 scene.nodes.push(SceneNode::Rect {
1776 rect: repose_core::Rect {
1777 x: k0x.min(k1x),
1778 y: cy - track_h * 0.5,
1779 w: (k1x - k0x).abs(),
1780 h: track_h,
1781 },
1782 color: mul_alpha(theme.primary, alpha_accum),
1783 radius: track_h * 0.5,
1784 });
1785
1786 for &kx in &[k0x, k1x] {
1788 scene.nodes.push(SceneNode::Rect {
1789 rect: repose_core::Rect {
1790 x: kx - knob_d * 0.5,
1791 y: cy - knob_d * 0.5,
1792 w: knob_d,
1793 h: knob_d,
1794 },
1795 color: mul_alpha(theme.surface, alpha_accum),
1796 radius: knob_d * 0.5,
1797 });
1798 scene.nodes.push(SceneNode::Border {
1799 rect: repose_core::Rect {
1800 x: kx - knob_d * 0.5,
1801 y: cy - knob_d * 0.5,
1802 w: knob_d,
1803 h: knob_d,
1804 },
1805 color: mul_alpha(Color::from_hex("#888888"), alpha_accum),
1806 width: 1.0,
1807 radius: knob_d * 0.5,
1808 });
1809 }
1810
1811 scene.nodes.push(SceneNode::Text {
1813 rect: repose_core::Rect {
1814 x: label_x + gap,
1815 y: rect.y,
1816 w: rect.x + rect.w - (label_x + gap),
1817 h: rect.h,
1818 },
1819 text: format!("{}: {:.2} – {:.2}", label, *start, *end),
1820 color: mul_alpha(theme.on_surface, alpha_accum),
1821 size: 16.0,
1822 });
1823
1824 let on_change_cb = on_change.as_ref().cloned();
1826 let minv = *min;
1827 let maxv = *max;
1828 let stepv = *step;
1829 let start_val = *start;
1830 let end_val = *end;
1831
1832 let active = Rc::new(RefCell::new(None::<u8>));
1834
1835 let update = {
1837 let active = active.clone();
1838 let on_change_cb = on_change_cb.clone();
1839 Rc::new(move |px: f32| {
1840 if let Some(thumb) = *active.borrow() {
1841 let tt = clamp01((px - track_x) / track_w);
1842 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
1843 match thumb {
1844 0 => {
1845 let new_start = v.min(end_val).min(maxv).max(minv);
1846 if let Some(cb) = &on_change_cb {
1847 cb(new_start, end_val);
1848 }
1849 }
1850 _ => {
1851 let new_end = v.max(start_val).max(minv).min(maxv);
1852 if let Some(cb) = &on_change_cb {
1853 cb(start_val, new_end);
1854 }
1855 }
1856 }
1857 }
1858 })
1859 };
1860
1861 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1863 let active = active.clone();
1864 let update = update.clone();
1865 let k0x0 = k0x;
1867 let k1x0 = k1x;
1868 Rc::new(move |pe| {
1869 let px = pe.position.x;
1870 let d0 = (px - k0x0).abs();
1871 let d1 = (px - k1x0).abs();
1872 *active.borrow_mut() = Some(if d0 <= d1 { 0 } else { 1 });
1873 update(px);
1874 })
1875 };
1876
1877 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1879 let active = active.clone();
1880 let update = update.clone();
1881 Rc::new(move |pe| {
1882 if active.borrow().is_some() {
1883 update(pe.position.x);
1884 }
1885 })
1886 };
1887
1888 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1890 let active = active.clone();
1891 Rc::new(move |_pe| {
1892 *active.borrow_mut() = None;
1893 })
1894 };
1895
1896 hits.push(HitRegion {
1897 id: v.id,
1898 rect,
1899 on_click: None,
1900 on_scroll: None,
1901 focusable: true,
1902 on_pointer_down: Some(on_pd),
1903 on_pointer_move: Some(on_pm),
1904 on_pointer_up: Some(on_pu),
1905 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1906 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1907 z_index: v.modifier.z_index,
1908 on_text_change: None,
1909 on_text_submit: None,
1910 });
1911 sems.push(SemNode {
1912 id: v.id,
1913 role: Role::Slider,
1914 label: Some(label.clone()),
1915 rect,
1916 focused: is_focused,
1917 enabled: true,
1918 });
1919 if is_focused {
1920 scene.nodes.push(SceneNode::Border {
1921 rect,
1922 color: mul_alpha(Color::from_hex("#88CCFF"), alpha_accum),
1923 width: 2.0,
1924 radius: v.modifier.clip_rounded.unwrap_or(6.0),
1925 });
1926 }
1927 }
1928 ViewKind::ProgressBar {
1929 value,
1930 min,
1931 max,
1932 label,
1933 circular,
1934 } => {
1935 let theme = locals::theme();
1936 let track_h = 6.0f32;
1937 let gap = 8.0f32;
1938 let label_w_split = rect.w * 0.6;
1939 let track_x = rect.x;
1940 let track_w = (label_w_split - track_x).max(60.0);
1941 let cy = rect.y + rect.h * 0.5;
1942
1943 scene.nodes.push(SceneNode::Rect {
1944 rect: repose_core::Rect {
1945 x: track_x,
1946 y: cy - track_h * 0.5,
1947 w: track_w,
1948 h: track_h,
1949 },
1950 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
1951 radius: track_h * 0.5,
1952 });
1953
1954 let t = clamp01(norm(*value, *min, *max));
1955 scene.nodes.push(SceneNode::Rect {
1956 rect: repose_core::Rect {
1957 x: track_x,
1958 y: cy - track_h * 0.5,
1959 w: track_w * t,
1960 h: track_h,
1961 },
1962 color: mul_alpha(theme.primary, alpha_accum),
1963 radius: track_h * 0.5,
1964 });
1965
1966 scene.nodes.push(SceneNode::Text {
1967 rect: repose_core::Rect {
1968 x: rect.x + label_w_split + gap,
1969 y: rect.y,
1970 w: rect.w - (label_w_split + gap),
1971 h: rect.h,
1972 },
1973 text: format!("{}: {:.0}%", label, t * 100.0),
1974 color: mul_alpha(theme.on_surface, alpha_accum),
1975 size: 16.0,
1976 });
1977
1978 sems.push(SemNode {
1979 id: v.id,
1980 role: Role::ProgressBar,
1981 label: Some(label.clone()),
1982 rect,
1983 focused: is_focused,
1984 enabled: true,
1985 });
1986 }
1987
1988 _ => {}
1989 }
1990
1991 for c in &v.children {
1993 walk(
1994 c,
1995 t,
1996 nodes,
1997 scene,
1998 hits,
1999 sems,
2000 textfield_states,
2001 interactions,
2002 focused,
2003 base,
2004 alpha_accum,
2005 );
2006 }
2007
2008 if v.modifier.transform.is_some() {
2009 scene.nodes.push(SceneNode::PopTransform);
2010 }
2011 }
2012
2013 walk(
2015 &root,
2016 &taffy,
2017 &nodes_map,
2018 &mut scene,
2019 &mut hits,
2020 &mut sems,
2021 textfield_states,
2022 interactions,
2023 focused,
2024 (0.0, 0.0),
2025 1.0,
2026 );
2027
2028 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
2030
2031 (scene, hits, sems)
2032}
2033
2034pub trait TextStyleExt {
2036 fn color(self, c: Color) -> View;
2037 fn size(self, px: f32) -> View;
2038}
2039impl TextStyleExt for View {
2040 fn color(self, c: Color) -> View {
2041 TextColor(self, c)
2042 }
2043 fn size(self, px: f32) -> View {
2044 TextSize(self, px)
2045 }
2046}