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