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