1#![allow(non_snake_case)]
2pub mod anim;
5pub mod anim_ext;
6pub mod canvas;
7pub mod gestures;
8pub mod lazy;
9pub mod material3;
10pub mod navigation;
11pub mod scroll;
12
13use std::collections::{HashMap, HashSet};
14use std::rc::Rc;
15use std::{cell::RefCell, cmp::Ordering};
16
17use repose_core::*;
18use taffy::style::{AlignItems, Dimension, Display, FlexDirection, JustifyContent, Style};
19use taffy::{Overflow, Point, ResolveOrZero};
20
21use taffy::prelude::{Position, Size, auto, length, percent};
22
23pub mod textfield;
24pub use textfield::{TextField, TextFieldState};
25
26use crate::textfield::{
27 TF_FONT_DP, TF_PADDING_X_DP, byte_to_char_index, measure_text, positions_for,
28};
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, 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_dp: f32) -> View {
108 if let ViewKind::Text {
109 font_size: text_size_dp,
110 ..
111 } = &mut v.kind
112 {
113 *text_size_dp = size_dp;
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_px_u32: (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 px = |dp_val: f32| dp_to_px(dp_val);
378 let font_px = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
380
381 let mut id = 1u64;
383 fn stamp(mut v: View, id: &mut u64) -> View {
384 v.id = *id;
385 *id += 1;
386 v.children = v.children.into_iter().map(|c| stamp(c, id)).collect();
387 v
388 }
389 let root = stamp(root.clone(), &mut id);
390
391 use taffy::prelude::*;
393 #[derive(Clone)]
394 enum NodeCtx {
395 Text {
396 text: String,
397 font_dp: f32, soft_wrap: bool,
399 max_lines: Option<usize>,
400 overflow: TextOverflow,
401 },
402 Button {
403 label: String,
404 },
405 TextField,
406 Container,
407 ScrollContainer,
408 Checkbox {
409 label: String,
410 },
411 Radio {
412 label: String,
413 },
414 Switch {
415 label: String,
416 },
417 Slider {
418 label: String,
419 },
420 Range {
421 label: String,
422 },
423 Progress {
424 label: String,
425 },
426 }
427
428 let mut taffy: TaffyTree<NodeCtx> = TaffyTree::new();
429 let mut nodes_map = HashMap::new();
430
431 #[derive(Clone)]
432 struct TextLayout {
433 lines: Vec<String>,
434 size_px: f32,
435 line_h_px: f32,
436 }
437 use std::collections::HashMap as StdHashMap;
438 let mut text_cache: StdHashMap<taffy::NodeId, TextLayout> = StdHashMap::new();
439
440 fn style_from_modifier(m: &Modifier, kind: &ViewKind, px: &dyn Fn(f32) -> f32) -> Style {
441 use taffy::prelude::*;
442 let mut s = Style::default();
443
444 s.display = match kind {
446 ViewKind::Row => Display::Flex,
447 ViewKind::Column
448 | ViewKind::Surface
449 | ViewKind::ScrollV { .. }
450 | ViewKind::ScrollXY { .. } => Display::Flex,
451 ViewKind::Stack => Display::Grid,
452 _ => Display::Flex,
453 };
454
455 if matches!(kind, ViewKind::Row) {
457 s.flex_direction =
458 if crate::locals::text_direction() == crate::locals::TextDirection::Rtl {
459 FlexDirection::RowReverse
460 } else {
461 FlexDirection::Row
462 };
463 }
464 if matches!(
465 kind,
466 ViewKind::Column
467 | ViewKind::Surface
468 | ViewKind::ScrollV { .. }
469 | ViewKind::ScrollXY { .. }
470 ) {
471 s.flex_direction = FlexDirection::Column;
472 }
473
474 s.align_items = if matches!(
476 kind,
477 ViewKind::Row
478 | ViewKind::Column
479 | ViewKind::Stack
480 | ViewKind::Surface
481 | ViewKind::ScrollV { .. }
482 | ViewKind::ScrollXY { .. }
483 ) {
484 Some(AlignItems::Stretch)
485 } else {
486 Some(AlignItems::FlexStart)
487 };
488 s.justify_content = Some(JustifyContent::FlexStart);
489
490 if let Some(r) = m.aspect_ratio {
492 s.aspect_ratio = Some(r.max(0.0));
493 }
494
495 if let Some(g) = m.flex_grow {
497 s.flex_grow = g;
498 }
499 if let Some(sh) = m.flex_shrink {
500 s.flex_shrink = sh;
501 }
502 if let Some(b_dp) = m.flex_basis {
503 s.flex_basis = length(px(b_dp.max(0.0)));
504 }
505
506 if let Some(a) = m.align_self {
508 s.align_self = Some(a);
509 }
510
511 if let Some(crate::modifier::PositionType::Absolute) = m.position_type {
513 s.position = Position::Absolute;
514 s.inset = taffy::geometry::Rect {
515 left: m.offset_left.map(|v| length(px(v))).unwrap_or_else(auto),
516 right: m.offset_right.map(|v| length(px(v))).unwrap_or_else(auto),
517 top: m.offset_top.map(|v| length(px(v))).unwrap_or_else(auto),
518 bottom: m.offset_bottom.map(|v| length(px(v))).unwrap_or_else(auto),
519 };
520 }
521
522 if let Some(cfg) = &m.grid {
524 s.display = Display::Grid;
525 s.grid_template_columns = (0..cfg.columns.max(1))
526 .map(|_| GridTemplateComponent::Single(flex(1.0)))
527 .collect();
528 s.gap = Size {
529 width: length(px(cfg.column_gap)),
530 height: length(px(cfg.row_gap)),
531 };
532 }
533
534 if matches!(kind, ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. }) {
536 s.overflow = Point {
537 x: Overflow::Hidden,
538 y: Overflow::Hidden,
539 };
540 }
541
542 if let Some(pv_dp) = m.padding_values {
544 s.padding = taffy::geometry::Rect {
545 left: length(px(pv_dp.left)),
546 right: length(px(pv_dp.right)),
547 top: length(px(pv_dp.top)),
548 bottom: length(px(pv_dp.bottom)),
549 };
550 } else if let Some(p_dp) = m.padding {
551 let v = length(px(p_dp));
552 s.padding = taffy::geometry::Rect {
553 left: v,
554 right: v,
555 top: v,
556 bottom: v,
557 };
558 }
559
560 let mut width_set = false;
562 let mut height_set = false;
563 if let Some(sz_dp) = m.size {
564 if sz_dp.width.is_finite() {
565 s.size.width = length(px(sz_dp.width.max(0.0)));
566 width_set = true;
567 }
568 if sz_dp.height.is_finite() {
569 s.size.height = length(px(sz_dp.height.max(0.0)));
570 height_set = true;
571 }
572 }
573 if let Some(w_dp) = m.width {
574 s.size.width = length(px(w_dp.max(0.0)));
575 width_set = true;
576 }
577 if let Some(h_dp) = m.height {
578 s.size.height = length(px(h_dp.max(0.0)));
579 height_set = true;
580 }
581
582 let is_row = matches!(kind, ViewKind::Row);
584 let is_column = matches!(
585 kind,
586 ViewKind::Column
587 | ViewKind::Surface
588 | ViewKind::ScrollV { .. }
589 | ViewKind::ScrollXY { .. }
590 );
591
592 let want_fill_w = m.fill_max || m.fill_max_w;
593 let want_fill_h = m.fill_max || m.fill_max_h;
594
595 if is_column {
597 if want_fill_h && !height_set {
599 s.flex_grow = s.flex_grow.max(1.0);
600 s.flex_shrink = s.flex_shrink.max(1.0);
601 s.flex_basis = length(0.0);
602 s.min_size.height = length(0.0); }
604 if want_fill_w && !width_set {
605 s.min_size.width = percent(1.0);
606 s.max_size.width = percent(1.0);
607 }
608 } else if is_row {
609 if want_fill_w && !width_set {
611 s.flex_grow = s.flex_grow.max(1.0);
612 s.flex_shrink = s.flex_shrink.max(1.0);
613 s.flex_basis = length(0.0);
614 s.min_size.width = length(0.0);
615 }
616 if want_fill_h && !height_set {
617 s.min_size.height = percent(1.0);
618 s.max_size.height = percent(1.0);
619 }
620 } else {
621 if want_fill_h && !height_set {
623 s.flex_grow = s.flex_grow.max(1.0);
624 s.flex_shrink = s.flex_shrink.max(1.0);
625 s.flex_basis = length(0.0);
626 s.min_size.height = length(0.0);
627 }
628 if want_fill_w && !width_set {
629 s.min_size.width = percent(1.0);
630 s.max_size.width = percent(1.0);
631 }
632 }
633
634 if matches!(kind, ViewKind::Surface) {
635 if (m.fill_max || m.fill_max_w) && s.min_size.width.is_auto() && !width_set {
636 s.min_size.width = percent(1.0);
637 s.max_size.width = percent(1.0);
638 }
639 if (m.fill_max || m.fill_max_h) && s.min_size.height.is_auto() && !height_set {
640 s.min_size.height = percent(1.0);
641 s.max_size.height = percent(1.0);
642 }
643 }
644
645 if let Some(v_dp) = m.min_width {
647 s.min_size.width = length(px(v_dp.max(0.0)));
648 }
649 if let Some(v_dp) = m.min_height {
650 s.min_size.height = length(px(v_dp.max(0.0)));
651 }
652 if let Some(v_dp) = m.max_width {
653 s.max_size.width = length(px(v_dp.max(0.0)));
654 }
655 if let Some(v_dp) = m.max_height {
656 s.max_size.height = length(px(v_dp.max(0.0)));
657 }
658
659 s
660 }
661
662 fn build_node(
663 v: &View,
664 t: &mut TaffyTree<NodeCtx>,
665 nodes_map: &mut HashMap<ViewId, taffy::NodeId>,
666 ) -> taffy::NodeId {
667 let px_helper = |dp_val: f32| dp_to_px(dp_val);
670
671 let mut style = style_from_modifier(&v.modifier, &v.kind, &px_helper);
672
673 if v.modifier.grid_col_span.is_some() || v.modifier.grid_row_span.is_some() {
674 use taffy::prelude::{GridPlacement, Line};
675
676 let col_span = v.modifier.grid_col_span.unwrap_or(1).max(1);
677 let row_span = v.modifier.grid_row_span.unwrap_or(1).max(1);
678
679 style.grid_column = Line {
680 start: GridPlacement::Auto,
681 end: GridPlacement::Span(col_span),
682 };
683 style.grid_row = Line {
684 start: GridPlacement::Auto,
685 end: GridPlacement::Span(row_span),
686 };
687 }
688
689 let children: Vec<_> = v
690 .children
691 .iter()
692 .map(|c| build_node(c, t, nodes_map))
693 .collect();
694
695 let node = match &v.kind {
696 ViewKind::Text {
697 text,
698 font_size: font_dp,
699 soft_wrap,
700 max_lines,
701 overflow,
702 ..
703 } => t
704 .new_leaf_with_context(
705 style,
706 NodeCtx::Text {
707 text: text.clone(),
708 font_dp: *font_dp,
709 soft_wrap: *soft_wrap,
710 max_lines: *max_lines,
711 overflow: *overflow,
712 },
713 )
714 .unwrap(),
715 ViewKind::Button { text, .. } => t
716 .new_leaf_with_context(
717 style,
718 NodeCtx::Button {
719 label: text.clone(),
720 },
721 )
722 .unwrap(),
723 ViewKind::TextField { .. } => {
724 t.new_leaf_with_context(style, NodeCtx::TextField).unwrap()
725 }
726 ViewKind::Image { .. } => t.new_leaf_with_context(style, NodeCtx::Container).unwrap(),
727 ViewKind::Checkbox { label, .. } => t
728 .new_leaf_with_context(
729 style,
730 NodeCtx::Checkbox {
731 label: label.clone(),
732 },
733 )
734 .unwrap(),
735 ViewKind::RadioButton { label, .. } => t
736 .new_leaf_with_context(
737 style,
738 NodeCtx::Radio {
739 label: label.clone(),
740 },
741 )
742 .unwrap(),
743 ViewKind::Switch { label, .. } => t
744 .new_leaf_with_context(
745 style,
746 NodeCtx::Switch {
747 label: label.clone(),
748 },
749 )
750 .unwrap(),
751 ViewKind::Slider { label, .. } => t
752 .new_leaf_with_context(
753 style,
754 NodeCtx::Slider {
755 label: label.clone(),
756 },
757 )
758 .unwrap(),
759 ViewKind::RangeSlider { label, .. } => t
760 .new_leaf_with_context(
761 style,
762 NodeCtx::Range {
763 label: label.clone(),
764 },
765 )
766 .unwrap(),
767 ViewKind::ProgressBar { label, .. } => t
768 .new_leaf_with_context(
769 style,
770 NodeCtx::Progress {
771 label: label.clone(),
772 },
773 )
774 .unwrap(),
775 ViewKind::ScrollV { .. } => {
776 let children: Vec<_> = v
777 .children
778 .iter()
779 .map(|c| build_node(c, t, nodes_map))
780 .collect();
781
782 let n = t.new_with_children(style, &children).unwrap();
783 t.set_node_context(n, Some(NodeCtx::ScrollContainer)).ok();
784 n
785 }
786 _ => {
787 let n = t.new_with_children(style, &children).unwrap();
788 t.set_node_context(n, Some(NodeCtx::Container)).ok();
789 n
790 }
791 };
792
793 nodes_map.insert(v.id, node);
794 node
795 }
796
797 let root_node = build_node(&root, &mut taffy, &mut nodes_map);
798
799 {
800 let mut rs = taffy.style(root_node).unwrap().clone();
801 rs.size.width = length(size_px_u32.0 as f32);
802 rs.size.height = length(size_px_u32.1 as f32);
803 taffy.set_style(root_node, rs).unwrap();
804 }
805
806 let available = taffy::geometry::Size {
807 width: AvailableSpace::Definite(size_px_u32.0 as f32),
808 height: AvailableSpace::Definite(size_px_u32.1 as f32),
809 };
810
811 taffy
813 .compute_layout_with_measure(root_node, available, |known, avail, node, ctx, _style| {
814 match ctx {
815 Some(NodeCtx::Text {
816 text,
817 font_dp,
818 soft_wrap,
819 max_lines,
820 overflow,
821 }) => {
822 let size_px_val = font_px(*font_dp);
824 let line_h_px_val = size_px_val * 1.3;
825
826 let approx_w_px = text.len() as f32 * size_px_val * 0.6; let measured_w_px = known.width.unwrap_or(approx_w_px);
829
830 let wrap_w_px = if *soft_wrap {
832 match avail.width {
833 AvailableSpace::Definite(w) => w,
834 _ => measured_w_px,
835 }
836 } else {
837 measured_w_px
838 };
839
840 let lines_vec: Vec<String> = if *soft_wrap {
842 let (ls, _trunc) =
843 repose_text::wrap_lines(text, size_px_val, wrap_w_px, *max_lines, true);
844 ls
845 } else {
846 match overflow {
847 TextOverflow::Ellipsis => {
848 vec![repose_text::ellipsize_line(text, size_px_val, wrap_w_px)]
849 }
850 _ => vec![text.clone()],
851 }
852 };
853 text_cache.insert(
854 node,
855 TextLayout {
856 lines: lines_vec.clone(),
857 size_px: size_px_val,
858 line_h_px: line_h_px_val,
859 },
860 );
861 let n_lines = lines_vec.len().max(1);
862
863 taffy::geometry::Size {
864 width: measured_w_px,
865 height: line_h_px_val * n_lines as f32,
866 }
867 }
868 Some(NodeCtx::Button { label }) => taffy::geometry::Size {
869 width: (label.len() as f32 * font_px(16.0) * 0.6) + px(24.0),
870 height: px(36.0),
871 },
872 Some(NodeCtx::TextField) => taffy::geometry::Size {
873 width: known.width.unwrap_or(px(220.0)),
874 height: px(36.0),
875 },
876 Some(NodeCtx::Checkbox { label }) => {
877 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
878 let w_px = px(24.0) + px(8.0) + label_w_px; taffy::geometry::Size {
880 width: known.width.unwrap_or(w_px),
881 height: px(24.0),
882 }
883 }
884 Some(NodeCtx::Radio { label }) => {
885 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
886 let w_px = px(24.0) + px(8.0) + label_w_px; taffy::geometry::Size {
888 width: known.width.unwrap_or(w_px),
889 height: px(24.0),
890 }
891 }
892 Some(NodeCtx::Switch { label }) => {
893 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
894 let w_px = (known.width)
895 .unwrap_or(px(46.0) + px(8.0) + label_w_px)
896 .max(px(80.0));
897 taffy::geometry::Size {
898 width: w_px,
899 height: px(28.0),
900 }
901 }
902 Some(NodeCtx::Slider { label }) => {
903 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
904 let w_px =
905 (known.width).unwrap_or(px(200.0).max(px(46.0) + px(8.0) + label_w_px));
906 taffy::geometry::Size {
907 width: w_px,
908 height: px(28.0),
909 }
910 }
911 Some(NodeCtx::Range { label }) => {
912 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
913 let w_px =
914 (known.width).unwrap_or(px(220.0).max(px(46.0) + px(8.0) + label_w_px));
915 taffy::geometry::Size {
916 width: w_px,
917 height: px(28.0),
918 }
919 }
920 Some(NodeCtx::Progress { label }) => {
921 let label_w_px = (label.len() as f32) * font_px(16.0) * 0.6;
922 let w_px =
923 (known.width).unwrap_or(px(200.0).max(px(100.0) + px(8.0) + label_w_px));
924 taffy::geometry::Size {
925 width: w_px,
926 height: px(12.0) + px(8.0),
927 }
928 }
929 Some(NodeCtx::ScrollContainer) | Some(NodeCtx::Container) | None => {
930 taffy::geometry::Size::ZERO
931 }
932 }
933 })
934 .unwrap();
935
936 fn layout_of(node: taffy::NodeId, t: &TaffyTree<impl Clone>) -> repose_core::Rect {
944 let l = t.layout(node).unwrap();
945 repose_core::Rect {
946 x: l.location.x,
947 y: l.location.y,
948 w: l.size.width,
949 h: l.size.height,
950 }
951 }
952
953 fn add_offset(mut r: repose_core::Rect, off: (f32, f32)) -> repose_core::Rect {
954 r.x += off.0;
955 r.y += off.1;
956 r
957 }
958
959 fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> Option<repose_core::Rect> {
961 let x0 = a.x.max(b.x);
962 let y0 = a.y.max(b.y);
963 let x1 = (a.x + a.w).min(b.x + b.w);
964 let y1 = (a.y + a.h).min(b.y + b.h);
965 let w = (x1 - x0).max(0.0);
966 let h = (y1 - y0).max(0.0);
967 if w <= 0.0 || h <= 0.0 {
968 None
969 } else {
970 Some(repose_core::Rect { x: x0, y: y0, w, h })
971 }
972 }
973
974 fn clamp01(x: f32) -> f32 {
975 x.max(0.0).min(1.0)
976 }
977 fn norm(value: f32, min: f32, max: f32) -> f32 {
978 if max > min {
979 (value - min) / (max - min)
980 } else {
981 0.0
982 }
983 }
984 fn denorm(t: f32, min: f32, max: f32) -> f32 {
985 min + t * (max - min)
986 }
987 fn snap_step(v: f32, step: Option<f32>, min: f32, max: f32) -> f32 {
988 match step {
989 Some(s) if s > 0.0 => {
990 let k = ((v - min) / s).round();
991 (min + k * s).clamp(min, max)
992 }
993 _ => v.clamp(min, max),
994 }
995 }
996 fn mul_alpha(c: Color, a: f32) -> Color {
997 let mut out = c;
998 let na = ((c.3 as f32) * a).clamp(0.0, 255.0) as u8;
999 out.3 = na;
1000 out
1001 }
1002 fn push_scrollbar_v(
1004 scene: &mut Scene,
1005 hits: &mut Vec<HitRegion>,
1006 interactions: &Interactions,
1007 view_id: u64,
1008 vp: crate::Rect,
1009 content_h_px: f32,
1010 off_y_px: f32,
1011 z: f32,
1012 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
1013 ) {
1014 if content_h_px <= vp.h + 0.5 {
1015 return;
1016 }
1017 let thickness_px = dp_to_px(6.0);
1018 let margin_px = dp_to_px(2.0);
1019 let min_thumb_px = dp_to_px(24.0);
1020 let th = locals::theme();
1021
1022 let track_x = vp.x + vp.w - margin_px - thickness_px;
1024 let track_y = vp.y + margin_px;
1025 let track_h = (vp.h - 2.0 * margin_px).max(0.0);
1026
1027 let ratio = (vp.h / content_h_px).clamp(0.0, 1.0);
1029 let mut thumb_h = (track_h * ratio).clamp(min_thumb_px, track_h);
1030 let denom = (content_h_px - vp.h).max(1.0);
1031 let tpos = (off_y_px / denom).clamp(0.0, 1.0);
1032 let max_pos = (track_h - thumb_h).max(0.0);
1033 let thumb_y = track_y + tpos * max_pos;
1034
1035 scene.nodes.push(SceneNode::Rect {
1036 rect: crate::Rect {
1037 x: track_x,
1038 y: track_y,
1039 w: thickness_px,
1040 h: track_h,
1041 },
1042 color: th.scrollbar_track,
1043 radius: thickness_px * 0.5,
1044 });
1045 scene.nodes.push(SceneNode::Rect {
1046 rect: crate::Rect {
1047 x: track_x,
1048 y: thumb_y,
1049 w: thickness_px,
1050 h: thumb_h,
1051 },
1052 color: th.scrollbar_thumb,
1053 radius: thickness_px * 0.5,
1054 });
1055 if let Some(setter) = set_scroll_offset {
1056 let thumb_id: u64 = view_id ^ 0x8000_0001;
1057 let map_to_off = Rc::new(move |py_px: f32| -> f32 {
1058 let denom = (content_h_px - vp.h).max(1.0);
1059 let max_pos = (track_h - thumb_h).max(0.0);
1060 let pos = ((py_px - track_y) - thumb_h * 0.5).clamp(0.0, max_pos);
1061 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1062 t * denom
1063 });
1064 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1065 let setter = setter.clone();
1066 let map = map_to_off.clone();
1067 Rc::new(move |pe| setter(map(pe.position.y)))
1068 };
1069 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1070 if interactions.pressed.contains(&thumb_id) {
1071 let setter = setter.clone();
1072 let map = map_to_off.clone();
1073 Some(Rc::new(move |pe| setter(map(pe.position.y))))
1074 } else {
1075 None
1076 };
1077 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1078 hits.push(HitRegion {
1079 id: thumb_id,
1080 rect: crate::Rect {
1081 x: track_x,
1082 y: thumb_y,
1083 w: thickness_px,
1084 h: thumb_h,
1085 },
1086 on_click: None,
1087 on_scroll: None,
1088 focusable: false,
1089 on_pointer_down: Some(on_pd),
1090 on_pointer_move: on_pm,
1091 on_pointer_up: Some(on_pu),
1092 on_pointer_enter: None,
1093 on_pointer_leave: None,
1094 z_index: z + 1000.0,
1095 on_text_change: None,
1096 on_text_submit: None,
1097 });
1098 }
1099 }
1100
1101 fn push_scrollbar_h(
1102 scene: &mut Scene,
1103 hits: &mut Vec<HitRegion>,
1104 interactions: &Interactions,
1105 view_id: u64,
1106 vp: crate::Rect,
1107 content_w_px: f32,
1108 off_x_px: f32,
1109 z: f32,
1110 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
1111 keep_y: f32,
1112 ) {
1113 if content_w_px <= vp.w + 0.5 {
1114 return;
1115 }
1116 let thickness_px = dp_to_px(6.0);
1117 let margin_px = dp_to_px(2.0);
1118 let min_thumb_px = dp_to_px(24.0);
1119 let th = locals::theme();
1120
1121 let track_x = vp.x + margin_px;
1122 let track_y = vp.y + vp.h - margin_px - thickness_px;
1123 let track_w = (vp.w - 2.0 * margin_px).max(0.0);
1124
1125 let ratio = (vp.w / content_w_px).clamp(0.0, 1.0);
1126 let mut thumb_w = (track_w * ratio).clamp(min_thumb_px, track_w);
1127 let denom = (content_w_px - vp.w).max(1.0);
1128 let tpos = (off_x_px / denom).clamp(0.0, 1.0);
1129 let max_pos = (track_w - thumb_w).max(0.0);
1130 let thumb_x = track_x + tpos * max_pos;
1131
1132 scene.nodes.push(SceneNode::Rect {
1133 rect: crate::Rect {
1134 x: track_x,
1135 y: track_y,
1136 w: track_w,
1137 h: thickness_px,
1138 },
1139 color: th.scrollbar_track,
1140 radius: thickness_px * 0.5,
1141 });
1142 scene.nodes.push(SceneNode::Rect {
1143 rect: crate::Rect {
1144 x: thumb_x,
1145 y: track_y,
1146 w: thumb_w,
1147 h: thickness_px,
1148 },
1149 color: th.scrollbar_thumb,
1150 radius: thickness_px * 0.5,
1151 });
1152 if let Some(set_xy) = set_scroll_offset_xy {
1153 let hthumb_id: u64 = view_id ^ 0x8000_0012;
1154 let map_to_off_x = Rc::new(move |px_pos: f32| -> f32 {
1155 let denom = (content_w_px - vp.w).max(1.0);
1156 let max_pos = (track_w - thumb_w).max(0.0);
1157 let pos = ((px_pos - track_x) - thumb_w * 0.5).clamp(0.0, max_pos);
1158 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1159 t * denom
1160 });
1161 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1162 let set_xy = set_xy.clone();
1163 let map = map_to_off_x.clone();
1164 Rc::new(move |pe| set_xy(map(pe.position.x), keep_y))
1165 };
1166 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1167 if interactions.pressed.contains(&hthumb_id) {
1168 let set_xy = set_xy.clone();
1169 let map = map_to_off_x.clone();
1170 Some(Rc::new(move |pe| set_xy(map(pe.position.x), keep_y)))
1171 } else {
1172 None
1173 };
1174 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1175 hits.push(HitRegion {
1176 id: hthumb_id,
1177 rect: crate::Rect {
1178 x: thumb_x,
1179 y: track_y,
1180 w: thumb_w,
1181 h: thickness_px,
1182 },
1183 on_click: None,
1184 on_scroll: None,
1185 focusable: false,
1186 on_pointer_down: Some(on_pd),
1187 on_pointer_move: on_pm,
1188 on_pointer_up: Some(on_pu),
1189 on_pointer_enter: None,
1190 on_pointer_leave: None,
1191 z_index: z + 1000.0,
1192 on_text_change: None,
1193 on_text_submit: None,
1194 });
1195 }
1196 }
1197
1198 let mut scene = Scene {
1199 clear_color: locals::theme().background,
1200 nodes: vec![],
1201 };
1202 let mut hits: Vec<HitRegion> = vec![];
1203 let mut sems: Vec<SemNode> = vec![];
1204
1205 fn walk(
1206 v: &View,
1207 t: &TaffyTree<NodeCtx>,
1208 nodes: &HashMap<ViewId, taffy::NodeId>,
1209 scene: &mut Scene,
1210 hits: &mut Vec<HitRegion>,
1211 sems: &mut Vec<SemNode>,
1212 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1213 interactions: &Interactions,
1214 focused: Option<u64>,
1215 parent_offset_px: (f32, f32),
1216 alpha_accum: f32,
1217 text_cache: &StdHashMap<taffy::NodeId, TextLayout>,
1218 font_px: &dyn Fn(f32) -> f32,
1219 ) {
1220 let local = layout_of(nodes[&v.id], t);
1221 let rect = add_offset(local, parent_offset_px);
1222
1223 let content_rect = {
1225 if let Some(pv_dp) = v.modifier.padding_values {
1226 crate::Rect {
1227 x: rect.x + dp_to_px(pv_dp.left),
1228 y: rect.y + dp_to_px(pv_dp.top),
1229 w: (rect.w - dp_to_px(pv_dp.left) - dp_to_px(pv_dp.right)).max(0.0),
1230 h: (rect.h - dp_to_px(pv_dp.top) - dp_to_px(pv_dp.bottom)).max(0.0),
1231 }
1232 } else if let Some(p_dp) = v.modifier.padding {
1233 let p_px = dp_to_px(p_dp);
1234 crate::Rect {
1235 x: rect.x + p_px,
1236 y: rect.y + p_px,
1237 w: (rect.w - 2.0 * p_px).max(0.0),
1238 h: (rect.h - 2.0 * p_px).max(0.0),
1239 }
1240 } else {
1241 rect
1242 }
1243 };
1244
1245 let pad_dx = content_rect.x - rect.x;
1246 let pad_dy = content_rect.y - rect.y;
1247
1248 let base_px = (parent_offset_px.0 + local.x, parent_offset_px.1 + local.y);
1249
1250 let is_hovered = interactions.hover == Some(v.id);
1251 let is_pressed = interactions.pressed.contains(&v.id);
1252 let is_focused = focused == Some(v.id);
1253
1254 if let Some(bg) = v.modifier.background {
1256 scene.nodes.push(SceneNode::Rect {
1257 rect,
1258 color: mul_alpha(bg, alpha_accum),
1259 radius: v.modifier.clip_rounded.map(dp_to_px).unwrap_or(0.0),
1260 });
1261 }
1262
1263 if let Some(b) = &v.modifier.border {
1265 scene.nodes.push(SceneNode::Border {
1266 rect,
1267 color: mul_alpha(b.color, alpha_accum),
1268 width: dp_to_px(b.width),
1269 radius: dp_to_px(b.radius.max(v.modifier.clip_rounded.unwrap_or(0.0))),
1270 });
1271 }
1272
1273 let this_alpha = v.modifier.alpha.unwrap_or(1.0);
1275 let alpha_accum = (alpha_accum * this_alpha).clamp(0.0, 1.0);
1276
1277 if let Some(tf) = v.modifier.transform {
1278 scene.nodes.push(SceneNode::PushTransform { transform: tf });
1279 }
1280
1281 if let Some(p) = &v.modifier.painter {
1283 (p)(scene, rect);
1284 }
1285
1286 let has_pointer = v.modifier.on_pointer_down.is_some()
1287 || v.modifier.on_pointer_move.is_some()
1288 || v.modifier.on_pointer_up.is_some()
1289 || v.modifier.on_pointer_enter.is_some()
1290 || v.modifier.on_pointer_leave.is_some();
1291
1292 if has_pointer || v.modifier.click {
1293 hits.push(HitRegion {
1294 id: v.id,
1295 rect,
1296 on_click: None, on_scroll: None, focusable: false,
1299 on_pointer_down: v.modifier.on_pointer_down.clone(),
1300 on_pointer_move: v.modifier.on_pointer_move.clone(),
1301 on_pointer_up: v.modifier.on_pointer_up.clone(),
1302 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1303 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1304 z_index: v.modifier.z_index,
1305 on_text_change: None,
1306 on_text_submit: None,
1307 });
1308 }
1309
1310 match &v.kind {
1311 ViewKind::Text {
1312 text,
1313 color,
1314 font_size: font_dp,
1315 soft_wrap,
1316 max_lines,
1317 overflow,
1318 } => {
1319 let nid = nodes[&v.id];
1320 let tl = text_cache.get(&nid);
1321
1322 let (size_px_val, line_h_px_val, mut lines): (f32, f32, Vec<String>) =
1323 if let Some(tl) = tl {
1324 (tl.size_px, tl.line_h_px, tl.lines.clone())
1325 } else {
1326 let sz_px = font_px(*font_dp);
1328 (sz_px, sz_px * 1.3, vec![text.clone()])
1329 };
1330 let mut draw_box = content_rect;
1332 let max_w_px = draw_box.w.max(0.0);
1333 let max_h_px = draw_box.h.max(0.0);
1334
1335 if lines.len() == 1 {
1337 let dy_px = (draw_box.h - size_px_val) * 0.5;
1338 if dy_px.is_finite() {
1339 draw_box.y += dy_px.max(0.0);
1340 draw_box.h = size_px_val;
1341 }
1342 }
1343
1344 let max_visual_lines = if max_h_px > 0.5 {
1346 (max_h_px / line_h_px_val).floor().max(1.0) as usize
1347 } else {
1348 usize::MAX
1349 };
1350
1351 if lines.len() > max_visual_lines {
1352 lines.truncate(max_visual_lines);
1353 if *overflow == TextOverflow::Ellipsis && max_w_px > 0.5 {
1354 if let Some(last) = lines.last_mut() {
1356 *last = repose_text::ellipsize_line(last, size_px_val, max_w_px);
1357 }
1358 }
1359 }
1360
1361 let need_clip = match overflow {
1362 TextOverflow::Visible => false,
1363 TextOverflow::Clip | TextOverflow::Ellipsis => true,
1364 };
1365
1366 if need_clip {
1367 scene.nodes.push(SceneNode::PushClip {
1368 rect: draw_box,
1369 radius: 0.0,
1370 });
1371 }
1372
1373 for (i, ln) in lines.iter().enumerate() {
1374 scene.nodes.push(SceneNode::Text {
1375 rect: crate::Rect {
1376 x: draw_box.x,
1377 y: draw_box.y + i as f32 * line_h_px_val,
1378 w: draw_box.w,
1379 h: line_h_px_val,
1380 },
1381 text: ln.clone(),
1382 color: mul_alpha(*color, alpha_accum),
1383 size: size_px_val,
1384 });
1385 }
1386
1387 if need_clip {
1388 scene.nodes.push(SceneNode::PopClip);
1389 }
1390
1391 sems.push(SemNode {
1392 id: v.id,
1393 role: Role::Text,
1394 label: Some(text.clone()),
1395 rect,
1396 focused: is_focused,
1397 enabled: true,
1398 });
1399 }
1400
1401 ViewKind::Button { text, on_click } => {
1402 if v.modifier.background.is_none() {
1404 let th = locals::theme();
1405 let base = if is_pressed {
1406 th.button_bg_pressed
1407 } else if is_hovered {
1408 th.button_bg_hover
1409 } else {
1410 th.button_bg
1411 };
1412 scene.nodes.push(SceneNode::Rect {
1413 rect,
1414 color: mul_alpha(base, alpha_accum),
1415 radius: v
1416 .modifier
1417 .clip_rounded
1418 .map(dp_to_px)
1419 .unwrap_or(6.0_f32 )
1420 .max(0.0),
1421 });
1422 }
1423 let label_px = font_px(16.0);
1425 let approx_w_px = (text.len() as f32) * label_px * 0.6;
1426 let tx = rect.x + (rect.w - approx_w_px).max(0.0) * 0.5;
1427 let ty = rect.y + (rect.h - label_px).max(0.0) * 0.5;
1428 scene.nodes.push(SceneNode::Text {
1429 rect: repose_core::Rect {
1430 x: tx,
1431 y: ty,
1432 w: approx_w_px,
1433 h: label_px,
1434 },
1435 text: text.clone(),
1436 color: mul_alpha(Color::WHITE, alpha_accum),
1437 size: label_px,
1438 });
1439
1440 if v.modifier.click || on_click.is_some() {
1441 hits.push(HitRegion {
1442 id: v.id,
1443 rect,
1444 on_click: on_click.clone(),
1445 on_scroll: None,
1446 focusable: true,
1447 on_pointer_down: v.modifier.on_pointer_down.clone(),
1448 on_pointer_move: v.modifier.on_pointer_move.clone(),
1449 on_pointer_up: v.modifier.on_pointer_up.clone(),
1450 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1451 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1452 z_index: v.modifier.z_index,
1453 on_text_change: None,
1454 on_text_submit: None,
1455 });
1456 }
1457 sems.push(SemNode {
1458 id: v.id,
1459 role: Role::Button,
1460 label: Some(text.clone()),
1461 rect,
1462 focused: is_focused,
1463 enabled: true,
1464 });
1465 if is_focused {
1467 scene.nodes.push(SceneNode::Border {
1468 rect,
1469 color: mul_alpha(locals::theme().focus, alpha_accum),
1470 width: dp_to_px(2.0),
1471 radius: v
1472 .modifier
1473 .clip_rounded
1474 .map(dp_to_px)
1475 .unwrap_or(dp_to_px(6.0)),
1476 });
1477 }
1478 }
1479 ViewKind::Image { handle, tint, fit } => {
1480 scene.nodes.push(SceneNode::Image {
1481 rect,
1482 handle: *handle,
1483 tint: mul_alpha(*tint, alpha_accum),
1484 fit: *fit,
1485 });
1486 }
1487
1488 ViewKind::TextField {
1489 hint,
1490 on_change,
1491 on_submit,
1492 ..
1493 } => {
1494 hits.push(HitRegion {
1495 id: v.id,
1496 rect,
1497 on_click: None,
1498 on_scroll: None,
1499 focusable: true,
1500 on_pointer_down: None,
1501 on_pointer_move: None,
1502 on_pointer_up: None,
1503 on_pointer_enter: None,
1504 on_pointer_leave: None,
1505 z_index: v.modifier.z_index,
1506 on_text_change: on_change.clone(),
1507 on_text_submit: on_submit.clone(),
1508 });
1509
1510 let pad_x_px = dp_to_px(TF_PADDING_X_DP);
1512 let inner = repose_core::Rect {
1513 x: rect.x + pad_x_px,
1514 y: rect.y + dp_to_px(8.0),
1515 w: rect.w - 2.0 * pad_x_px,
1516 h: rect.h - dp_to_px(16.0),
1517 };
1518 scene.nodes.push(SceneNode::PushClip {
1519 rect: inner,
1520 radius: 0.0,
1521 });
1522 if is_focused {
1524 scene.nodes.push(SceneNode::Border {
1525 rect,
1526 color: mul_alpha(locals::theme().focus, alpha_accum),
1527 width: dp_to_px(2.0),
1528 radius: v
1529 .modifier
1530 .clip_rounded
1531 .map(dp_to_px)
1532 .unwrap_or(dp_to_px(6.0)),
1533 });
1534 }
1535 if let Some(state_rc) = textfield_states.get(&v.id) {
1536 state_rc.borrow_mut().set_inner_width(inner.w);
1537
1538 let state = state_rc.borrow();
1539 let text_val = &state.text;
1540 let font_px_u32 = TF_FONT_DP as u32; let m = measure_text(text_val, font_px_u32);
1542
1543 if state.selection.start != state.selection.end {
1545 let i0 = byte_to_char_index(&m, state.selection.start);
1546 let i1 = byte_to_char_index(&m, state.selection.end);
1547 let sx_px =
1548 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1549 let ex_px =
1550 m.positions.get(i1).copied().unwrap_or(sx_px) - state.scroll_offset;
1551 let sel_x_px = inner.x + sx_px.max(0.0);
1552 let sel_w_px = (ex_px - sx_px).max(0.0);
1553 scene.nodes.push(SceneNode::Rect {
1554 rect: repose_core::Rect {
1555 x: sel_x_px,
1556 y: inner.y,
1557 w: sel_w_px,
1558 h: inner.h,
1559 },
1560 color: mul_alpha(Color::from_hex("#3B7BFF55"), alpha_accum),
1561 radius: 0.0,
1562 });
1563 }
1564
1565 if let Some(range) = &state.composition {
1567 if range.start < range.end && !text_val.is_empty() {
1568 let i0 = byte_to_char_index(&m, range.start);
1569 let i1 = byte_to_char_index(&m, range.end);
1570 let sx_px =
1571 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1572 let ex_px =
1573 m.positions.get(i1).copied().unwrap_or(sx_px) - state.scroll_offset;
1574 let ux = inner.x + sx_px.max(0.0);
1575 let uw = (ex_px - sx_px).max(0.0);
1576 scene.nodes.push(SceneNode::Rect {
1577 rect: repose_core::Rect {
1578 x: ux,
1579 y: inner.y + inner.h - dp_to_px(2.0),
1580 w: uw,
1581 h: dp_to_px(2.0),
1582 },
1583 color: mul_alpha(locals::theme().focus, alpha_accum),
1584 radius: 0.0,
1585 });
1586 }
1587 }
1588
1589 let text_color = if text_val.is_empty() {
1591 mul_alpha(Color::from_hex("#666666"), alpha_accum) } else {
1593 mul_alpha(locals::theme().on_surface, alpha_accum)
1594 };
1595 scene.nodes.push(SceneNode::Text {
1596 rect: repose_core::Rect {
1597 x: inner.x - state.scroll_offset,
1598 y: inner.y,
1599 w: inner.w,
1600 h: inner.h,
1601 },
1602 text: if text_val.is_empty() {
1603 hint.clone()
1604 } else {
1605 text_val.clone()
1606 },
1607 color: text_color,
1608 size: font_px(TF_FONT_DP),
1609 });
1610
1611 if state.selection.start == state.selection.end && state.caret_visible() {
1613 let i = byte_to_char_index(&m, state.selection.end);
1614 let cx_px =
1615 m.positions.get(i).copied().unwrap_or(0.0) - state.scroll_offset;
1616 let caret_x_px = inner.x + cx_px.max(0.0);
1617 scene.nodes.push(SceneNode::Rect {
1618 rect: repose_core::Rect {
1619 x: caret_x_px,
1620 y: inner.y,
1621 w: dp_to_px(1.0),
1622 h: inner.h,
1623 },
1624 color: mul_alpha(Color::WHITE, alpha_accum),
1625 radius: 0.0,
1626 });
1627 }
1628 scene.nodes.push(SceneNode::PopClip);
1630
1631 sems.push(SemNode {
1632 id: v.id,
1633 role: Role::TextField,
1634 label: Some(text_val.clone()),
1635 rect,
1636 focused: is_focused,
1637 enabled: true,
1638 });
1639 } else {
1640 scene.nodes.push(SceneNode::Text {
1642 rect: repose_core::Rect {
1643 x: inner.x,
1644 y: inner.y,
1645 w: inner.w,
1646 h: inner.h,
1647 },
1648 text: hint.clone(),
1649 color: mul_alpha(Color::from_hex("#666666"), alpha_accum),
1650 size: font_px(TF_FONT_DP),
1651 });
1652 sems.push(SemNode {
1653 id: v.id,
1654 role: Role::TextField,
1655 label: Some(hint.clone()),
1656 rect,
1657 focused: is_focused,
1658 enabled: true,
1659 });
1660 }
1661 }
1662 ViewKind::ScrollV {
1663 on_scroll,
1664 set_viewport_height,
1665 set_content_height,
1666 get_scroll_offset,
1667 set_scroll_offset,
1668 } => {
1669 hits.push(HitRegion {
1671 id: v.id,
1672 rect, on_click: None,
1674 on_scroll: on_scroll.clone(),
1675 focusable: false,
1676 on_pointer_down: v.modifier.on_pointer_down.clone(),
1677 on_pointer_move: v.modifier.on_pointer_move.clone(),
1678 on_pointer_up: v.modifier.on_pointer_up.clone(),
1679 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1680 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1681 z_index: v.modifier.z_index,
1682 on_text_change: None,
1683 on_text_submit: None,
1684 });
1685
1686 let vp = content_rect; if let Some(set_vh) = set_viewport_height {
1690 set_vh(vp.h.max(0.0));
1691 }
1692
1693 fn subtree_extents(node: taffy::NodeId, t: &TaffyTree<NodeCtx>) -> (f32, f32) {
1695 let l = t.layout(node).unwrap();
1696 let mut w = l.size.width;
1697 let mut h = l.size.height;
1698 if let Ok(children) = t.children(node) {
1699 for &ch in children.iter() {
1700 let cl = t.layout(ch).unwrap();
1701 let (cw, chh) = subtree_extents(ch, t);
1702 w = w.max(cl.location.x + cw);
1703 h = h.max(cl.location.y + chh);
1704 }
1705 }
1706 (w, h)
1707 }
1708 let mut content_h_px = 0.0f32;
1709 for c in &v.children {
1710 let nid = nodes[&c.id];
1711 let l = t.layout(nid).unwrap();
1712 let (_cw, chh) = subtree_extents(nid, t);
1713 content_h_px = content_h_px.max(l.location.y + chh);
1714 }
1715 if let Some(set_ch) = set_content_height {
1716 set_ch(content_h_px);
1717 }
1718
1719 scene.nodes.push(SceneNode::PushClip {
1721 rect: vp,
1722 radius: 0.0, });
1724
1725 let hit_start = hits.len();
1727 let scroll_offset_px = if let Some(get) = get_scroll_offset {
1728 get()
1729 } else {
1730 0.0
1731 };
1732 let child_offset_px = (base_px.0 + pad_dx, base_px.1 + pad_dy - scroll_offset_px);
1733 for c in &v.children {
1734 walk(
1735 c,
1736 t,
1737 nodes,
1738 scene,
1739 hits,
1740 sems,
1741 textfield_states,
1742 interactions,
1743 focused,
1744 child_offset_px,
1745 alpha_accum,
1746 text_cache,
1747 font_px,
1748 );
1749 }
1750
1751 let mut i = hit_start;
1753 while i < hits.len() {
1754 if let Some(r) = intersect(hits[i].rect, vp) {
1755 hits[i].rect = r;
1756 i += 1;
1757 } else {
1758 hits.remove(i);
1759 }
1760 }
1761
1762 push_scrollbar_v(
1764 scene,
1765 hits,
1766 interactions,
1767 v.id,
1768 vp,
1769 content_h_px,
1770 scroll_offset_px,
1771 v.modifier.z_index,
1772 set_scroll_offset.clone(),
1773 );
1774
1775 scene.nodes.push(SceneNode::PopClip);
1776 return;
1777 }
1778 ViewKind::ScrollXY {
1779 on_scroll,
1780 set_viewport_width,
1781 set_viewport_height,
1782 set_content_width,
1783 set_content_height,
1784 get_scroll_offset_xy,
1785 set_scroll_offset_xy,
1786 } => {
1787 hits.push(HitRegion {
1788 id: v.id,
1789 rect,
1790 on_click: None,
1791 on_scroll: on_scroll.clone(),
1792 focusable: false,
1793 on_pointer_down: v.modifier.on_pointer_down.clone(),
1794 on_pointer_move: v.modifier.on_pointer_move.clone(),
1795 on_pointer_up: v.modifier.on_pointer_up.clone(),
1796 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1797 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1798 z_index: v.modifier.z_index,
1799 on_text_change: None,
1800 on_text_submit: None,
1801 });
1802
1803 let vp = content_rect;
1804
1805 if let Some(set_w) = set_viewport_width {
1806 set_w(vp.w.max(0.0));
1807 }
1808 if let Some(set_h) = set_viewport_height {
1809 set_h(vp.h.max(0.0));
1810 }
1811
1812 fn subtree_extents(node: taffy::NodeId, t: &TaffyTree<NodeCtx>) -> (f32, f32) {
1813 let l = t.layout(node).unwrap();
1814 let mut w = l.size.width;
1815 let mut h = l.size.height;
1816 if let Ok(children) = t.children(node) {
1817 for &ch in children.iter() {
1818 let cl = t.layout(ch).unwrap();
1819 let (cw, chh) = subtree_extents(ch, t);
1820 w = w.max(cl.location.x + cw);
1821 h = h.max(cl.location.y + chh);
1822 }
1823 }
1824 (w, h)
1825 }
1826 let mut content_w_px = 0.0f32;
1827 let mut content_h_px = 0.0f32;
1828 for c in &v.children {
1829 let nid = nodes[&c.id];
1830 let l = t.layout(nid).unwrap();
1831 let (cw, chh) = subtree_extents(nid, t);
1832 content_w_px = content_w_px.max(l.location.x + cw);
1833 content_h_px = content_h_px.max(l.location.y + chh);
1834 }
1835 if let Some(set_cw) = set_content_width {
1836 set_cw(content_w_px);
1837 }
1838 if let Some(set_ch) = set_content_height {
1839 set_ch(content_h_px);
1840 }
1841
1842 scene.nodes.push(SceneNode::PushClip {
1843 rect: vp,
1844 radius: 0.0,
1845 });
1846
1847 let hit_start = hits.len();
1848 let (ox_px, oy_px) = if let Some(get) = get_scroll_offset_xy {
1849 get()
1850 } else {
1851 (0.0, 0.0)
1852 };
1853 let child_offset_px = (base_px.0 + pad_dx - ox_px, base_px.1 + pad_dy - oy_px);
1854 for c in &v.children {
1855 walk(
1856 c,
1857 t,
1858 nodes,
1859 scene,
1860 hits,
1861 sems,
1862 textfield_states,
1863 interactions,
1864 focused,
1865 child_offset_px,
1866 alpha_accum,
1867 text_cache,
1868 font_px,
1869 );
1870 }
1871 let mut i = hit_start;
1873 while i < hits.len() {
1874 if let Some(r) = intersect(hits[i].rect, vp) {
1875 hits[i].rect = r;
1876 i += 1;
1877 } else {
1878 hits.remove(i);
1879 }
1880 }
1881
1882 let set_scroll_y: Option<Rc<dyn Fn(f32)>> =
1883 set_scroll_offset_xy.clone().map(|set_xy| {
1884 let ox = ox_px; Rc::new(move |y| set_xy(ox, y)) as Rc<dyn Fn(f32)>
1886 });
1887
1888 push_scrollbar_v(
1890 scene,
1891 hits,
1892 interactions,
1893 v.id,
1894 vp,
1895 content_h_px,
1896 oy_px,
1897 v.modifier.z_index,
1898 set_scroll_y,
1899 );
1900 push_scrollbar_h(
1901 scene,
1902 hits,
1903 interactions,
1904 v.id,
1905 vp,
1906 content_w_px,
1907 ox_px,
1908 v.modifier.z_index,
1909 set_scroll_offset_xy.clone(),
1910 oy_px,
1911 );
1912
1913 scene.nodes.push(SceneNode::PopClip);
1914 return;
1915 }
1916 ViewKind::Checkbox {
1917 checked,
1918 label,
1919 on_change,
1920 } => {
1921 let theme = locals::theme();
1922 let box_size_px = dp_to_px(18.0);
1924 let bx = rect.x;
1925 let by = rect.y + (rect.h - box_size_px) * 0.5;
1926 scene.nodes.push(SceneNode::Rect {
1928 rect: repose_core::Rect {
1929 x: bx,
1930 y: by,
1931 w: box_size_px,
1932 h: box_size_px,
1933 },
1934 color: if *checked {
1935 mul_alpha(theme.primary, alpha_accum)
1936 } else {
1937 mul_alpha(theme.surface, alpha_accum)
1938 },
1939 radius: dp_to_px(3.0),
1940 });
1941 scene.nodes.push(SceneNode::Border {
1942 rect: repose_core::Rect {
1943 x: bx,
1944 y: by,
1945 w: box_size_px,
1946 h: box_size_px,
1947 },
1948 color: mul_alpha(theme.outline, alpha_accum),
1949 width: dp_to_px(1.0),
1950 radius: dp_to_px(3.0),
1951 });
1952 if *checked {
1954 scene.nodes.push(SceneNode::Text {
1955 rect: repose_core::Rect {
1956 x: bx + dp_to_px(3.0),
1957 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
1958 w: rect.w - (box_size_px + dp_to_px(8.0)),
1959 h: font_px(16.0),
1960 },
1961 text: "✓".to_string(),
1962 color: mul_alpha(theme.on_primary, alpha_accum),
1963 size: font_px(16.0),
1964 });
1965 }
1966 scene.nodes.push(SceneNode::Text {
1968 rect: repose_core::Rect {
1969 x: bx + box_size_px + dp_to_px(8.0),
1970 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
1971 w: rect.w - (box_size_px + dp_to_px(8.0)),
1972 h: font_px(16.0),
1973 },
1974 text: label.clone(),
1975 color: mul_alpha(theme.on_surface, alpha_accum),
1976 size: font_px(16.0),
1977 });
1978
1979 let toggled = !*checked;
1981 let on_click = on_change.as_ref().map(|cb| {
1982 let cb = cb.clone();
1983 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1984 });
1985 hits.push(HitRegion {
1986 id: v.id,
1987 rect,
1988 on_click,
1989 on_scroll: None,
1990 focusable: true,
1991 on_pointer_down: v.modifier.on_pointer_down.clone(),
1992 on_pointer_move: v.modifier.on_pointer_move.clone(),
1993 on_pointer_up: v.modifier.on_pointer_up.clone(),
1994 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1995 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1996 z_index: v.modifier.z_index,
1997 on_text_change: None,
1998 on_text_submit: None,
1999 });
2000 sems.push(SemNode {
2001 id: v.id,
2002 role: Role::Checkbox,
2003 label: Some(label.clone()),
2004 rect,
2005 focused: is_focused,
2006 enabled: true,
2007 });
2008 if is_focused {
2009 scene.nodes.push(SceneNode::Border {
2010 rect,
2011 color: mul_alpha(locals::theme().focus, alpha_accum),
2012 width: dp_to_px(2.0),
2013 radius: v
2014 .modifier
2015 .clip_rounded
2016 .map(dp_to_px)
2017 .unwrap_or(dp_to_px(6.0)),
2018 });
2019 }
2020 }
2021
2022 ViewKind::RadioButton {
2023 selected,
2024 label,
2025 on_select,
2026 } => {
2027 let theme = locals::theme();
2028 let d_px = dp_to_px(18.0);
2029 let cx = rect.x;
2030 let cy = rect.y + (rect.h - d_px) * 0.5;
2031
2032 scene.nodes.push(SceneNode::Border {
2034 rect: repose_core::Rect {
2035 x: cx,
2036 y: cy,
2037 w: d_px,
2038 h: d_px,
2039 },
2040 color: mul_alpha(theme.outline, alpha_accum),
2041 width: dp_to_px(1.5),
2042 radius: d_px * 0.5,
2043 });
2044 if *selected {
2046 scene.nodes.push(SceneNode::Rect {
2047 rect: repose_core::Rect {
2048 x: cx + dp_to_px(4.0),
2049 y: cy + dp_to_px(4.0),
2050 w: d_px - dp_to_px(8.0),
2051 h: d_px - dp_to_px(8.0),
2052 },
2053 color: mul_alpha(theme.primary, alpha_accum),
2054 radius: (d_px - dp_to_px(8.0)) * 0.5,
2055 });
2056 }
2057 scene.nodes.push(SceneNode::Text {
2058 rect: repose_core::Rect {
2059 x: cx + d_px + dp_to_px(8.0),
2060 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2061 w: rect.w - (d_px + dp_to_px(8.0)),
2062 h: font_px(16.0),
2063 },
2064 text: label.clone(),
2065 color: mul_alpha(theme.on_surface, alpha_accum),
2066 size: font_px(16.0),
2067 });
2068
2069 hits.push(HitRegion {
2070 id: v.id,
2071 rect,
2072 on_click: on_select.clone(),
2073 on_scroll: None,
2074 focusable: true,
2075 on_pointer_down: v.modifier.on_pointer_down.clone(),
2076 on_pointer_move: v.modifier.on_pointer_move.clone(),
2077 on_pointer_up: v.modifier.on_pointer_up.clone(),
2078 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2079 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2080 z_index: v.modifier.z_index,
2081 on_text_change: None,
2082 on_text_submit: None,
2083 });
2084 sems.push(SemNode {
2085 id: v.id,
2086 role: Role::RadioButton,
2087 label: Some(label.clone()),
2088 rect,
2089 focused: is_focused,
2090 enabled: true,
2091 });
2092 if is_focused {
2093 scene.nodes.push(SceneNode::Border {
2094 rect,
2095 color: mul_alpha(locals::theme().focus, alpha_accum),
2096 width: dp_to_px(2.0),
2097 radius: v
2098 .modifier
2099 .clip_rounded
2100 .map(dp_to_px)
2101 .unwrap_or(dp_to_px(6.0)),
2102 });
2103 }
2104 }
2105
2106 ViewKind::Switch {
2107 checked,
2108 label,
2109 on_change,
2110 } => {
2111 let theme = locals::theme();
2112 let track_w_px = dp_to_px(46.0);
2114 let track_h_px = dp_to_px(26.0);
2115 let tx = rect.x;
2116 let ty = rect.y + (rect.h - track_h_px) * 0.5;
2117 let knob_px = dp_to_px(22.0);
2118 let on_col = theme.primary;
2119 let off_col = Color::from_hex("#333333");
2120
2121 scene.nodes.push(SceneNode::Rect {
2123 rect: repose_core::Rect {
2124 x: tx,
2125 y: ty,
2126 w: track_w_px,
2127 h: track_h_px,
2128 },
2129 color: if *checked {
2130 mul_alpha(on_col, alpha_accum)
2131 } else {
2132 mul_alpha(off_col, alpha_accum)
2133 },
2134 radius: track_h_px * 0.5,
2135 });
2136 let kx = if *checked {
2138 tx + track_w_px - knob_px - dp_to_px(2.0)
2139 } else {
2140 tx + dp_to_px(2.0)
2141 };
2142 let ky = ty + (track_h_px - knob_px) * 0.5;
2143 scene.nodes.push(SceneNode::Rect {
2144 rect: repose_core::Rect {
2145 x: kx,
2146 y: ky,
2147 w: knob_px,
2148 h: knob_px,
2149 },
2150 color: mul_alpha(Color::from_hex("#EEEEEE"), alpha_accum),
2151 radius: knob_px * 0.5,
2152 });
2153 scene.nodes.push(SceneNode::Border {
2154 rect: repose_core::Rect {
2155 x: kx,
2156 y: ky,
2157 w: knob_px,
2158 h: knob_px,
2159 },
2160 color: mul_alpha(theme.outline, alpha_accum),
2161 width: dp_to_px(1.0),
2162 radius: knob_px * 0.5,
2163 });
2164
2165 scene.nodes.push(SceneNode::Text {
2167 rect: repose_core::Rect {
2168 x: tx + track_w_px + dp_to_px(8.0),
2169 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2170 w: rect.w - (track_w_px + dp_to_px(8.0)),
2171 h: font_px(16.0),
2172 },
2173 text: label.clone(),
2174 color: mul_alpha(theme.on_surface, alpha_accum),
2175 size: font_px(16.0),
2176 });
2177
2178 let toggled = !*checked;
2179 let on_click = on_change.as_ref().map(|cb| {
2180 let cb = cb.clone();
2181 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
2182 });
2183 hits.push(HitRegion {
2184 id: v.id,
2185 rect,
2186 on_click,
2187 on_scroll: None,
2188 focusable: true,
2189 on_pointer_down: v.modifier.on_pointer_down.clone(),
2190 on_pointer_move: v.modifier.on_pointer_move.clone(),
2191 on_pointer_up: v.modifier.on_pointer_up.clone(),
2192 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2193 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2194 z_index: v.modifier.z_index,
2195 on_text_change: None,
2196 on_text_submit: None,
2197 });
2198 sems.push(SemNode {
2199 id: v.id,
2200 role: Role::Switch,
2201 label: Some(label.clone()),
2202 rect,
2203 focused: is_focused,
2204 enabled: true,
2205 });
2206 if is_focused {
2207 scene.nodes.push(SceneNode::Border {
2208 rect,
2209 color: mul_alpha(locals::theme().focus, alpha_accum),
2210 width: dp_to_px(2.0),
2211 radius: v
2212 .modifier
2213 .clip_rounded
2214 .map(dp_to_px)
2215 .unwrap_or(dp_to_px(6.0)),
2216 });
2217 }
2218 }
2219 ViewKind::Slider {
2220 value,
2221 min,
2222 max,
2223 step,
2224 label,
2225 on_change,
2226 } => {
2227 let theme = locals::theme();
2228 let track_h_px = dp_to_px(4.0);
2230 let knob_d_px = dp_to_px(20.0);
2231 let gap_px = dp_to_px(8.0);
2232 let label_x = rect.x + rect.w * 0.6; let track_x = rect.x;
2234 let track_w_px = (label_x - track_x).max(dp_to_px(60.0));
2235 let cy = rect.y + rect.h * 0.5;
2236
2237 scene.nodes.push(SceneNode::Rect {
2239 rect: repose_core::Rect {
2240 x: track_x,
2241 y: cy - track_h_px * 0.5,
2242 w: track_w_px,
2243 h: track_h_px,
2244 },
2245 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2246 radius: track_h_px * 0.5,
2247 });
2248
2249 let t = clamp01(norm(*value, *min, *max));
2251 let kx = track_x + t * track_w_px;
2252 scene.nodes.push(SceneNode::Rect {
2253 rect: repose_core::Rect {
2254 x: kx - knob_d_px * 0.5,
2255 y: cy - knob_d_px * 0.5,
2256 w: knob_d_px,
2257 h: knob_d_px,
2258 },
2259 color: mul_alpha(theme.surface, alpha_accum),
2260 radius: knob_d_px * 0.5,
2261 });
2262 scene.nodes.push(SceneNode::Border {
2263 rect: repose_core::Rect {
2264 x: kx - knob_d_px * 0.5,
2265 y: cy - knob_d_px * 0.5,
2266 w: knob_d_px,
2267 h: knob_d_px,
2268 },
2269 color: mul_alpha(theme.outline, alpha_accum),
2270 width: dp_to_px(1.0),
2271 radius: knob_d_px * 0.5,
2272 });
2273
2274 scene.nodes.push(SceneNode::Text {
2276 rect: repose_core::Rect {
2277 x: label_x + gap_px,
2278 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2279 w: rect.x + rect.w - (label_x + gap_px),
2280 h: font_px(16.0),
2281 },
2282 text: format!("{}: {:.2}", label, *value),
2283 color: mul_alpha(theme.on_surface, alpha_accum),
2284 size: font_px(16.0),
2285 });
2286
2287 let on_change_cb: Option<Rc<dyn Fn(f32)>> = on_change.as_ref().cloned();
2289 let minv = *min;
2290 let maxv = *max;
2291 let stepv = *step;
2292
2293 let current = Rc::new(RefCell::new(*value));
2295
2296 let update_at = {
2298 let on_change_cb = on_change_cb.clone();
2299 let current = current.clone();
2300 Rc::new(move |px_pos: f32| {
2301 let tt = clamp01((px_pos - track_x) / track_w_px);
2302 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2303 *current.borrow_mut() = v;
2304 if let Some(cb) = &on_change_cb {
2305 cb(v);
2306 }
2307 })
2308 };
2309
2310 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2312 let f = update_at.clone();
2313 Rc::new(move |pe| {
2314 f(pe.position.x);
2315 })
2316 };
2317
2318 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2320 let f = update_at.clone();
2321 Rc::new(move |pe| {
2322 f(pe.position.x);
2323 })
2324 };
2325
2326 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
2328
2329 let on_scroll = {
2331 let on_change_cb = on_change_cb.clone();
2332 let current = current.clone();
2333 Rc::new(move |d: Vec2| -> Vec2 {
2334 let base = *current.borrow();
2335 let delta = stepv.unwrap_or((maxv - minv) * 0.01);
2336 let dir = if d.y.is_sign_negative() { 1.0 } else { -1.0 };
2338 let new_v = snap_step(base + dir * delta, stepv, minv, maxv);
2339 *current.borrow_mut() = new_v;
2340 if let Some(cb) = &on_change_cb {
2341 cb(new_v);
2342 }
2343 Vec2 { x: d.x, y: 0.0 } })
2345 };
2346
2347 hits.push(HitRegion {
2349 id: v.id,
2350 rect,
2351 on_click: None,
2352 on_scroll: Some(on_scroll),
2353 focusable: true,
2354 on_pointer_down: Some(on_pd),
2355 on_pointer_move: if is_pressed { Some(on_pm) } else { None },
2356 on_pointer_up: Some(on_pu),
2357 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2358 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2359 z_index: v.modifier.z_index,
2360 on_text_change: None,
2361 on_text_submit: None,
2362 });
2363
2364 sems.push(SemNode {
2365 id: v.id,
2366 role: Role::Slider,
2367 label: Some(label.clone()),
2368 rect,
2369 focused: is_focused,
2370 enabled: true,
2371 });
2372 if is_focused {
2373 scene.nodes.push(SceneNode::Border {
2374 rect,
2375 color: mul_alpha(locals::theme().focus, alpha_accum),
2376 width: dp_to_px(2.0),
2377 radius: v
2378 .modifier
2379 .clip_rounded
2380 .map(dp_to_px)
2381 .unwrap_or(dp_to_px(6.0)),
2382 });
2383 }
2384 }
2385 ViewKind::RangeSlider {
2386 start,
2387 end,
2388 min,
2389 max,
2390 step,
2391 label,
2392 on_change,
2393 } => {
2394 let theme = locals::theme();
2395 let track_h_px = dp_to_px(4.0);
2396 let knob_d_px = dp_to_px(20.0);
2397 let gap_px = dp_to_px(8.0);
2398 let label_x = rect.x + rect.w * 0.6;
2399 let track_x = rect.x;
2400 let track_w_px = (label_x - track_x).max(dp_to_px(80.0));
2401 let cy = rect.y + rect.h * 0.5;
2402
2403 scene.nodes.push(SceneNode::Rect {
2405 rect: repose_core::Rect {
2406 x: track_x,
2407 y: cy - track_h_px * 0.5,
2408 w: track_w_px,
2409 h: track_h_px,
2410 },
2411 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2412 radius: track_h_px * 0.5,
2413 });
2414
2415 let t0 = clamp01(norm(*start, *min, *max));
2417 let t1 = clamp01(norm(*end, *min, *max));
2418 let k0x = track_x + t0 * track_w_px;
2419 let k1x = track_x + t1 * track_w_px;
2420
2421 scene.nodes.push(SceneNode::Rect {
2423 rect: repose_core::Rect {
2424 x: k0x.min(k1x),
2425 y: cy - track_h_px * 0.5,
2426 w: (k1x - k0x).abs(),
2427 h: track_h_px,
2428 },
2429 color: mul_alpha(theme.primary, alpha_accum),
2430 radius: track_h_px * 0.5,
2431 });
2432
2433 for &kx in &[k0x, k1x] {
2435 scene.nodes.push(SceneNode::Rect {
2436 rect: repose_core::Rect {
2437 x: kx - knob_d_px * 0.5,
2438 y: cy - knob_d_px * 0.5,
2439 w: knob_d_px,
2440 h: knob_d_px,
2441 },
2442 color: mul_alpha(theme.surface, alpha_accum),
2443 radius: knob_d_px * 0.5,
2444 });
2445 scene.nodes.push(SceneNode::Border {
2446 rect: repose_core::Rect {
2447 x: kx - knob_d_px * 0.5,
2448 y: cy - knob_d_px * 0.5,
2449 w: knob_d_px,
2450 h: knob_d_px,
2451 },
2452 color: mul_alpha(theme.outline, alpha_accum),
2453 width: dp_to_px(1.0),
2454 radius: knob_d_px * 0.5,
2455 });
2456 }
2457
2458 scene.nodes.push(SceneNode::Text {
2460 rect: repose_core::Rect {
2461 x: label_x + gap_px,
2462 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2463 w: rect.x + rect.w - (label_x + gap_px),
2464 h: font_px(16.0),
2465 },
2466 text: format!("{}: {:.2} – {:.2}", label, *start, *end),
2467 color: mul_alpha(theme.on_surface, alpha_accum),
2468 size: font_px(16.0),
2469 });
2470
2471 let on_change_cb = on_change.as_ref().cloned();
2473 let minv = *min;
2474 let maxv = *max;
2475 let stepv = *step;
2476 let start_val = *start;
2477 let end_val = *end;
2478
2479 let active = Rc::new(RefCell::new(None::<u8>));
2481
2482 let update = {
2484 let active = active.clone();
2485 let on_change_cb = on_change_cb.clone();
2486 Rc::new(move |px_pos: f32| {
2487 if let Some(thumb) = *active.borrow() {
2488 let tt = clamp01((px_pos - track_x) / track_w_px);
2489 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2490 match thumb {
2491 0 => {
2492 let new_start = v.min(end_val).min(maxv).max(minv);
2493 if let Some(cb) = &on_change_cb {
2494 cb(new_start, end_val);
2495 }
2496 }
2497 _ => {
2498 let new_end = v.max(start_val).max(minv).min(maxv);
2499 if let Some(cb) = &on_change_cb {
2500 cb(start_val, new_end);
2501 }
2502 }
2503 }
2504 }
2505 })
2506 };
2507
2508 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2510 let active = active.clone();
2511 let update = update.clone();
2512 let k0x0 = k0x;
2514 let k1x0 = k1x;
2515 Rc::new(move |pe| {
2516 let px_pos = pe.position.x;
2517 let d0 = (px_pos - k0x0).abs();
2518 let d1 = (px_pos - k1x0).abs();
2519 *active.borrow_mut() = Some(if d0 <= d1 { 0 } else { 1 });
2520 update(px_pos);
2521 })
2522 };
2523
2524 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2526 let active = active.clone();
2527 let update = update.clone();
2528 Rc::new(move |pe| {
2529 if active.borrow().is_some() {
2530 update(pe.position.x);
2531 }
2532 })
2533 };
2534
2535 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2537 let active = active.clone();
2538 Rc::new(move |_pe| {
2539 *active.borrow_mut() = None;
2540 })
2541 };
2542
2543 hits.push(HitRegion {
2544 id: v.id,
2545 rect,
2546 on_click: None,
2547 on_scroll: None,
2548 focusable: true,
2549 on_pointer_down: Some(on_pd),
2550 on_pointer_move: Some(on_pm),
2551 on_pointer_up: Some(on_pu),
2552 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2553 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2554 z_index: v.modifier.z_index,
2555 on_text_change: None,
2556 on_text_submit: None,
2557 });
2558 sems.push(SemNode {
2559 id: v.id,
2560 role: Role::Slider,
2561 label: Some(label.clone()),
2562 rect,
2563 focused: is_focused,
2564 enabled: true,
2565 });
2566 if is_focused {
2567 scene.nodes.push(SceneNode::Border {
2568 rect,
2569 color: mul_alpha(locals::theme().focus, alpha_accum),
2570 width: dp_to_px(2.0),
2571 radius: v
2572 .modifier
2573 .clip_rounded
2574 .map(dp_to_px)
2575 .unwrap_or(dp_to_px(6.0)),
2576 });
2577 }
2578 }
2579 ViewKind::ProgressBar {
2580 value,
2581 min,
2582 max,
2583 label,
2584 circular: _,
2585 } => {
2586 let theme = locals::theme();
2587 let track_h_px = dp_to_px(6.0);
2588 let gap_px = dp_to_px(8.0);
2589 let label_w_split_px = rect.w * 0.6;
2590 let track_x = rect.x;
2591 let track_w_px = (label_w_split_px - track_x).max(dp_to_px(60.0));
2592 let cy = rect.y + rect.h * 0.5;
2593
2594 scene.nodes.push(SceneNode::Rect {
2595 rect: repose_core::Rect {
2596 x: track_x,
2597 y: cy - track_h_px * 0.5,
2598 w: track_w_px,
2599 h: track_h_px,
2600 },
2601 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2602 radius: track_h_px * 0.5,
2603 });
2604
2605 let t = clamp01(norm(*value, *min, *max));
2606 scene.nodes.push(SceneNode::Rect {
2607 rect: repose_core::Rect {
2608 x: track_x,
2609 y: cy - track_h_px * 0.5,
2610 w: track_w_px * t,
2611 h: track_h_px,
2612 },
2613 color: mul_alpha(theme.primary, alpha_accum),
2614 radius: track_h_px * 0.5,
2615 });
2616
2617 scene.nodes.push(SceneNode::Text {
2618 rect: repose_core::Rect {
2619 x: rect.x + label_w_split_px + gap_px,
2620 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2621 w: rect.w - (label_w_split_px + gap_px),
2622 h: font_px(16.0),
2623 },
2624 text: format!("{}: {:.0}%", label, t * 100.0),
2625 color: mul_alpha(theme.on_surface, alpha_accum),
2626 size: font_px(16.0),
2627 });
2628
2629 sems.push(SemNode {
2630 id: v.id,
2631 role: Role::ProgressBar,
2632 label: Some(label.clone()),
2633 rect,
2634 focused: is_focused,
2635 enabled: true,
2636 });
2637 }
2638
2639 _ => {}
2640 }
2641
2642 for c in &v.children {
2644 walk(
2645 c,
2646 t,
2647 nodes,
2648 scene,
2649 hits,
2650 sems,
2651 textfield_states,
2652 interactions,
2653 focused,
2654 base_px,
2655 alpha_accum,
2656 text_cache,
2657 font_px,
2658 );
2659 }
2660
2661 if v.modifier.transform.is_some() {
2662 scene.nodes.push(SceneNode::PopTransform);
2663 }
2664 }
2665
2666 let font_px = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
2667
2668 walk(
2670 &root,
2671 &taffy,
2672 &nodes_map,
2673 &mut scene,
2674 &mut hits,
2675 &mut sems,
2676 textfield_states,
2677 interactions,
2678 focused,
2679 (0.0, 0.0),
2680 1.0,
2681 &text_cache,
2682 &font_px,
2683 );
2684
2685 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
2687
2688 (scene, hits, sems)
2689}
2690
2691pub trait TextStyleExt {
2693 fn color(self, c: Color) -> View;
2694 fn size(self, px: f32) -> View;
2695 fn max_lines(self, n: usize) -> View;
2696 fn single_line(self) -> View;
2697 fn overflow_ellipsize(self) -> View;
2698 fn overflow_clip(self) -> View;
2699 fn overflow_visible(self) -> View;
2700}
2701impl TextStyleExt for View {
2702 fn color(self, c: Color) -> View {
2703 TextColor(self, c)
2704 }
2705 fn size(self, dp_font: f32) -> View {
2706 TextSize(self, dp_font)
2707 }
2708 fn max_lines(mut self, n: usize) -> View {
2709 if let ViewKind::Text {
2710 max_lines,
2711 soft_wrap,
2712 ..
2713 } = &mut self.kind
2714 {
2715 *max_lines = Some(n);
2716 *soft_wrap = true;
2717 }
2718 self
2719 }
2720 fn single_line(mut self) -> View {
2721 if let ViewKind::Text {
2722 soft_wrap,
2723 max_lines,
2724 ..
2725 } = &mut self.kind
2726 {
2727 *soft_wrap = false;
2728 *max_lines = Some(1);
2729 }
2730 self
2731 }
2732 fn overflow_ellipsize(mut self) -> View {
2733 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2734 *overflow = TextOverflow::Ellipsis;
2735 }
2736 self
2737 }
2738 fn overflow_clip(mut self) -> View {
2739 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2740 *overflow = TextOverflow::Clip;
2741 }
2742 self
2743 }
2744 fn overflow_visible(mut self) -> View {
2745 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2746 *overflow = TextOverflow::Visible;
2747 }
2748 self
2749 }
2750}