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: true,
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 if matches!(kind, ViewKind::Text { .. }) && s.min_size.width.is_auto() {
666 s.min_size.width = length(0.0);
667 }
668
669 s
670 }
671
672 fn build_node(
673 v: &View,
674 t: &mut TaffyTree<NodeCtx>,
675 nodes_map: &mut HashMap<ViewId, taffy::NodeId>,
676 ) -> taffy::NodeId {
677 let px_helper = |dp_val: f32| dp_to_px(dp_val);
680
681 let mut style = style_from_modifier(&v.modifier, &v.kind, &px_helper);
682
683 if v.modifier.grid_col_span.is_some() || v.modifier.grid_row_span.is_some() {
684 use taffy::prelude::{GridPlacement, Line};
685
686 let col_span = v.modifier.grid_col_span.unwrap_or(1).max(1);
687 let row_span = v.modifier.grid_row_span.unwrap_or(1).max(1);
688
689 style.grid_column = Line {
690 start: GridPlacement::Auto,
691 end: GridPlacement::Span(col_span),
692 };
693 style.grid_row = Line {
694 start: GridPlacement::Auto,
695 end: GridPlacement::Span(row_span),
696 };
697 }
698
699 let children: Vec<_> = v
700 .children
701 .iter()
702 .map(|c| build_node(c, t, nodes_map))
703 .collect();
704
705 let node = match &v.kind {
706 ViewKind::Text {
707 text,
708 font_size: font_dp,
709 soft_wrap,
710 max_lines,
711 overflow,
712 ..
713 } => t
714 .new_leaf_with_context(
715 style,
716 NodeCtx::Text {
717 text: text.clone(),
718 font_dp: *font_dp,
719 soft_wrap: *soft_wrap,
720 max_lines: *max_lines,
721 overflow: *overflow,
722 },
723 )
724 .unwrap(),
725 ViewKind::Button { .. } => {
726 let children: Vec<_> = v
727 .children
728 .iter()
729 .map(|c| build_node(c, t, nodes_map))
730 .collect();
731 let n = t.new_with_children(style, &children).unwrap();
732 t.set_node_context(n, Some(NodeCtx::Container)).ok();
733 n
734 }
735 ViewKind::TextField { .. } => {
736 t.new_leaf_with_context(style, NodeCtx::TextField).unwrap()
737 }
738 ViewKind::Image { .. } => t.new_leaf_with_context(style, NodeCtx::Container).unwrap(),
739 ViewKind::Checkbox { .. } => t
740 .new_leaf_with_context(style, NodeCtx::Checkbox {})
741 .unwrap(),
742 ViewKind::RadioButton { .. } => {
743 t.new_leaf_with_context(style, NodeCtx::Radio {}).unwrap()
744 }
745 ViewKind::Switch { .. } => t.new_leaf_with_context(style, NodeCtx::Switch {}).unwrap(),
746 ViewKind::Slider { .. } => t.new_leaf_with_context(style, NodeCtx::Slider).unwrap(),
747 ViewKind::RangeSlider { .. } => t.new_leaf_with_context(style, NodeCtx::Range).unwrap(),
748 ViewKind::ProgressBar { .. } => {
749 t.new_leaf_with_context(style, NodeCtx::Progress).unwrap()
750 }
751 ViewKind::ScrollV { .. } => {
752 let children: Vec<_> = v
753 .children
754 .iter()
755 .map(|c| build_node(c, t, nodes_map))
756 .collect();
757
758 let n = t.new_with_children(style, &children).unwrap();
759 t.set_node_context(n, Some(NodeCtx::ScrollContainer)).ok();
760 n
761 }
762 _ => {
763 let n = t.new_with_children(style, &children).unwrap();
764 t.set_node_context(n, Some(NodeCtx::Container)).ok();
765 n
766 }
767 };
768
769 nodes_map.insert(v.id, node);
770 node
771 }
772
773 let root_node = build_node(&root, &mut taffy, &mut nodes_map);
774
775 {
776 let mut rs = taffy.style(root_node).unwrap().clone();
777 rs.size.width = length(size_px_u32.0 as f32);
778 rs.size.height = length(size_px_u32.1 as f32);
779 taffy.set_style(root_node, rs).unwrap();
780 }
781
782 let available = taffy::geometry::Size {
783 width: AvailableSpace::Definite(size_px_u32.0 as f32),
784 height: AvailableSpace::Definite(size_px_u32.1 as f32),
785 };
786
787 taffy
789 .compute_layout_with_measure(root_node, available, |known, avail, node, ctx, _style| {
790 match ctx {
791 Some(NodeCtx::Text {
792 text,
793 font_dp,
794 soft_wrap,
795 max_lines,
796 overflow,
797 }) => {
798 let size_px_val = font_px(*font_dp);
799 let line_h_px_val = size_px_val * 1.3;
800
801 let approx_w_px = text.len() as f32 * size_px_val * 0.6;
803
804 let target_w_px = match avail.width {
806 AvailableSpace::Definite(w) => w,
807 _ => known.width.unwrap_or(approx_w_px),
808 };
809
810 let wrap_w_px = if *soft_wrap || matches!(overflow, TextOverflow::Ellipsis) {
812 target_w_px
813 } else {
814 known.width.unwrap_or(approx_w_px)
815 };
816
817 let mut lines_vec: Vec<String>;
819 let mut truncated = false;
820
821 if *soft_wrap {
822 let (ls, trunc) = repose_text::wrap_lines(
823 &text,
824 size_px_val,
825 wrap_w_px,
826 *max_lines,
827 true,
828 );
829 lines_vec = ls;
830 truncated = trunc;
831 if matches!(overflow, TextOverflow::Ellipsis)
832 && truncated
833 && !lines_vec.is_empty()
834 {
835 let last = lines_vec.len() - 1;
836 lines_vec[last] = repose_text::ellipsize_line(
837 &lines_vec[last],
838 size_px_val,
839 wrap_w_px,
840 );
841 }
842 } else if matches!(overflow, TextOverflow::Ellipsis) {
843 if approx_w_px > wrap_w_px + 0.5 {
844 lines_vec =
845 vec![repose_text::ellipsize_line(text, size_px_val, wrap_w_px)];
846 } else {
847 lines_vec = vec![text.clone()];
848 }
849 } else {
850 lines_vec = vec![text.clone()];
851 }
852
853 text_cache.insert(
855 node,
856 TextLayout {
857 lines: lines_vec.clone(),
858 size_px: size_px_val,
859 line_h_px: line_h_px_val,
860 },
861 );
862
863 let line_count = lines_vec.len().max(1);
865 taffy::geometry::Size {
866 width: wrap_w_px,
867 height: line_h_px_val * line_count as f32,
868 }
869 }
870 Some(NodeCtx::Button { label }) => taffy::geometry::Size {
871 width: (label.len() as f32 * font_px(16.0) * 0.6) + px(24.0),
872 height: px(36.0),
873 },
874 Some(NodeCtx::TextField) => taffy::geometry::Size {
875 width: known.width.unwrap_or(px(220.0)),
876 height: px(36.0),
877 },
878 Some(NodeCtx::Checkbox { .. }) => taffy::geometry::Size {
879 width: known.width.unwrap_or(px(24.0)),
880 height: px(24.0),
881 },
882 Some(NodeCtx::Radio { .. }) => taffy::geometry::Size {
883 width: known.width.unwrap_or(px(18.0)),
884 height: px(18.0),
885 },
886 Some(NodeCtx::Switch { .. }) => taffy::geometry::Size {
887 width: known.width.unwrap_or(px(46.0)),
888 height: px(28.0),
889 },
890 Some(NodeCtx::Slider { .. }) => taffy::geometry::Size {
891 width: known.width.unwrap_or(px(200.0)),
892 height: px(28.0),
893 },
894 Some(NodeCtx::Range { .. }) => taffy::geometry::Size {
895 width: known.width.unwrap_or(px(220.0)),
896 height: px(28.0),
897 },
898 Some(NodeCtx::Progress { .. }) => taffy::geometry::Size {
899 width: known.width.unwrap_or(px(200.0)),
900 height: px(12.0),
901 },
902 Some(NodeCtx::ScrollContainer) | Some(NodeCtx::Container) | None => {
903 taffy::geometry::Size::ZERO
904 }
905 }
906 })
907 .unwrap();
908
909 fn layout_of(node: taffy::NodeId, t: &TaffyTree<impl Clone>) -> repose_core::Rect {
917 let l = t.layout(node).unwrap();
918 repose_core::Rect {
919 x: l.location.x,
920 y: l.location.y,
921 w: l.size.width,
922 h: l.size.height,
923 }
924 }
925
926 fn add_offset(mut r: repose_core::Rect, off: (f32, f32)) -> repose_core::Rect {
927 r.x += off.0;
928 r.y += off.1;
929 r
930 }
931
932 fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> Option<repose_core::Rect> {
934 let x0 = a.x.max(b.x);
935 let y0 = a.y.max(b.y);
936 let x1 = (a.x + a.w).min(b.x + b.w);
937 let y1 = (a.y + a.h).min(b.y + b.h);
938 let w = (x1 - x0).max(0.0);
939 let h = (y1 - y0).max(0.0);
940 if w <= 0.0 || h <= 0.0 {
941 None
942 } else {
943 Some(repose_core::Rect { x: x0, y: y0, w, h })
944 }
945 }
946
947 fn clamp01(x: f32) -> f32 {
948 x.max(0.0).min(1.0)
949 }
950 fn norm(value: f32, min: f32, max: f32) -> f32 {
951 if max > min {
952 (value - min) / (max - min)
953 } else {
954 0.0
955 }
956 }
957 fn denorm(t: f32, min: f32, max: f32) -> f32 {
958 min + t * (max - min)
959 }
960 fn snap_step(v: f32, step: Option<f32>, min: f32, max: f32) -> f32 {
961 match step {
962 Some(s) if s > 0.0 => {
963 let k = ((v - min) / s).round();
964 (min + k * s).clamp(min, max)
965 }
966 _ => v.clamp(min, max),
967 }
968 }
969 fn mul_alpha(c: Color, a: f32) -> Color {
970 let mut out = c;
971 let na = ((c.3 as f32) * a).clamp(0.0, 255.0) as u8;
972 out.3 = na;
973 out
974 }
975 fn push_scrollbar_v(
977 scene: &mut Scene,
978 hits: &mut Vec<HitRegion>,
979 interactions: &Interactions,
980 view_id: u64,
981 vp: crate::Rect,
982 content_h_px: f32,
983 off_y_px: f32,
984 z: f32,
985 set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
986 ) {
987 if content_h_px <= vp.h + 0.5 {
988 return;
989 }
990 let thickness_px = dp_to_px(6.0);
991 let margin_px = dp_to_px(2.0);
992 let min_thumb_px = dp_to_px(24.0);
993 let th = locals::theme();
994
995 let track_x = vp.x + vp.w - margin_px - thickness_px;
997 let track_y = vp.y + margin_px;
998 let track_h = (vp.h - 2.0 * margin_px).max(0.0);
999
1000 let ratio = (vp.h / content_h_px).clamp(0.0, 1.0);
1002 let thumb_h = (track_h * ratio).clamp(min_thumb_px, track_h);
1003 let denom = (content_h_px - vp.h).max(1.0);
1004 let tpos = (off_y_px / denom).clamp(0.0, 1.0);
1005 let max_pos = (track_h - thumb_h).max(0.0);
1006 let thumb_y = track_y + tpos * max_pos;
1007
1008 scene.nodes.push(SceneNode::Rect {
1009 rect: crate::Rect {
1010 x: track_x,
1011 y: track_y,
1012 w: thickness_px,
1013 h: track_h,
1014 },
1015 color: th.scrollbar_track,
1016 radius: thickness_px * 0.5,
1017 });
1018 scene.nodes.push(SceneNode::Rect {
1019 rect: crate::Rect {
1020 x: track_x,
1021 y: thumb_y,
1022 w: thickness_px,
1023 h: thumb_h,
1024 },
1025 color: th.scrollbar_thumb,
1026 radius: thickness_px * 0.5,
1027 });
1028 if let Some(setter) = set_scroll_offset {
1029 let thumb_id: u64 = view_id ^ 0x8000_0001;
1030 let map_to_off = Rc::new(move |py_px: f32| -> f32 {
1031 let denom = (content_h_px - vp.h).max(1.0);
1032 let max_pos = (track_h - thumb_h).max(0.0);
1033 let pos = ((py_px - track_y) - thumb_h * 0.5).clamp(0.0, max_pos);
1034 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1035 t * denom
1036 });
1037 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1038 let setter = setter.clone();
1039 let map = map_to_off.clone();
1040 Rc::new(move |pe| setter(map(pe.position.y)))
1041 };
1042 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1043 if interactions.pressed.contains(&thumb_id) {
1044 let setter = setter.clone();
1045 let map = map_to_off.clone();
1046 Some(Rc::new(move |pe| setter(map(pe.position.y))))
1047 } else {
1048 None
1049 };
1050 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1051 hits.push(HitRegion {
1052 id: thumb_id,
1053 rect: crate::Rect {
1054 x: track_x,
1055 y: thumb_y,
1056 w: thickness_px,
1057 h: thumb_h,
1058 },
1059 on_click: None,
1060 on_scroll: None,
1061 focusable: false,
1062 on_pointer_down: Some(on_pd),
1063 on_pointer_move: on_pm,
1064 on_pointer_up: Some(on_pu),
1065 on_pointer_enter: None,
1066 on_pointer_leave: None,
1067 z_index: z + 1000.0,
1068 on_text_change: None,
1069 on_text_submit: None,
1070 tf_state_key: None,
1071 });
1072 }
1073 }
1074
1075 fn push_scrollbar_h(
1076 scene: &mut Scene,
1077 hits: &mut Vec<HitRegion>,
1078 interactions: &Interactions,
1079 view_id: u64,
1080 vp: crate::Rect,
1081 content_w_px: f32,
1082 off_x_px: f32,
1083 z: f32,
1084 set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
1085 keep_y: f32,
1086 ) {
1087 if content_w_px <= vp.w + 0.5 {
1088 return;
1089 }
1090 let thickness_px = dp_to_px(6.0);
1091 let margin_px = dp_to_px(2.0);
1092 let min_thumb_px = dp_to_px(24.0);
1093 let th = locals::theme();
1094
1095 let track_x = vp.x + margin_px;
1096 let track_y = vp.y + vp.h - margin_px - thickness_px;
1097 let track_w = (vp.w - 2.0 * margin_px).max(0.0);
1098
1099 let ratio = (vp.w / content_w_px).clamp(0.0, 1.0);
1100 let thumb_w = (track_w * ratio).clamp(min_thumb_px, track_w);
1101 let denom = (content_w_px - vp.w).max(1.0);
1102 let tpos = (off_x_px / denom).clamp(0.0, 1.0);
1103 let max_pos = (track_w - thumb_w).max(0.0);
1104 let thumb_x = track_x + tpos * max_pos;
1105
1106 scene.nodes.push(SceneNode::Rect {
1107 rect: crate::Rect {
1108 x: track_x,
1109 y: track_y,
1110 w: track_w,
1111 h: thickness_px,
1112 },
1113 color: th.scrollbar_track,
1114 radius: thickness_px * 0.5,
1115 });
1116 scene.nodes.push(SceneNode::Rect {
1117 rect: crate::Rect {
1118 x: thumb_x,
1119 y: track_y,
1120 w: thumb_w,
1121 h: thickness_px,
1122 },
1123 color: th.scrollbar_thumb,
1124 radius: thickness_px * 0.5,
1125 });
1126 if let Some(set_xy) = set_scroll_offset_xy {
1127 let hthumb_id: u64 = view_id ^ 0x8000_0012;
1128 let map_to_off_x = Rc::new(move |px_pos: f32| -> f32 {
1129 let denom = (content_w_px - vp.w).max(1.0);
1130 let max_pos = (track_w - thumb_w).max(0.0);
1131 let pos = ((px_pos - track_x) - thumb_w * 0.5).clamp(0.0, max_pos);
1132 let t = if max_pos > 0.0 { pos / max_pos } else { 0.0 };
1133 t * denom
1134 });
1135 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
1136 let set_xy = set_xy.clone();
1137 let map = map_to_off_x.clone();
1138 Rc::new(move |pe| set_xy(map(pe.position.x), keep_y))
1139 };
1140 let on_pm: Option<Rc<dyn Fn(repose_core::input::PointerEvent)>> =
1141 if interactions.pressed.contains(&hthumb_id) {
1142 let set_xy = set_xy.clone();
1143 let map = map_to_off_x.clone();
1144 Some(Rc::new(move |pe| set_xy(map(pe.position.x), keep_y)))
1145 } else {
1146 None
1147 };
1148 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
1149 hits.push(HitRegion {
1150 id: hthumb_id,
1151 rect: crate::Rect {
1152 x: thumb_x,
1153 y: track_y,
1154 w: thumb_w,
1155 h: thickness_px,
1156 },
1157 on_click: None,
1158 on_scroll: None,
1159 focusable: false,
1160 on_pointer_down: Some(on_pd),
1161 on_pointer_move: on_pm,
1162 on_pointer_up: Some(on_pu),
1163 on_pointer_enter: None,
1164 on_pointer_leave: None,
1165 z_index: z + 1000.0,
1166 on_text_change: None,
1167 on_text_submit: None,
1168 tf_state_key: None,
1169 });
1170 }
1171 }
1172
1173 let mut scene = Scene {
1174 clear_color: locals::theme().background,
1175 nodes: vec![],
1176 };
1177 let mut hits: Vec<HitRegion> = vec![];
1178 let mut sems: Vec<SemNode> = vec![];
1179
1180 fn walk(
1181 v: &View,
1182 t: &TaffyTree<NodeCtx>,
1183 nodes: &HashMap<ViewId, taffy::NodeId>,
1184 scene: &mut Scene,
1185 hits: &mut Vec<HitRegion>,
1186 sems: &mut Vec<SemNode>,
1187 textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1188 interactions: &Interactions,
1189 focused: Option<u64>,
1190 parent_offset_px: (f32, f32),
1191 alpha_accum: f32,
1192 text_cache: &StdHashMap<taffy::NodeId, TextLayout>,
1193 font_px: &dyn Fn(f32) -> f32,
1194 ) {
1195 let local = layout_of(nodes[&v.id], t);
1196 let rect = add_offset(local, parent_offset_px);
1197
1198 let content_rect = {
1200 if let Some(pv_dp) = v.modifier.padding_values {
1201 crate::Rect {
1202 x: rect.x + dp_to_px(pv_dp.left),
1203 y: rect.y + dp_to_px(pv_dp.top),
1204 w: (rect.w - dp_to_px(pv_dp.left) - dp_to_px(pv_dp.right)).max(0.0),
1205 h: (rect.h - dp_to_px(pv_dp.top) - dp_to_px(pv_dp.bottom)).max(0.0),
1206 }
1207 } else if let Some(p_dp) = v.modifier.padding {
1208 let p_px = dp_to_px(p_dp);
1209 crate::Rect {
1210 x: rect.x + p_px,
1211 y: rect.y + p_px,
1212 w: (rect.w - 2.0 * p_px).max(0.0),
1213 h: (rect.h - 2.0 * p_px).max(0.0),
1214 }
1215 } else {
1216 rect
1217 }
1218 };
1219
1220 let pad_dx = content_rect.x - rect.x;
1221 let pad_dy = content_rect.y - rect.y;
1222
1223 let base_px = (parent_offset_px.0 + local.x, parent_offset_px.1 + local.y);
1224
1225 let is_hovered = interactions.hover == Some(v.id);
1226 let is_pressed = interactions.pressed.contains(&v.id);
1227 let is_focused = focused == Some(v.id);
1228
1229 if let Some(bg) = v.modifier.background {
1231 scene.nodes.push(SceneNode::Rect {
1232 rect,
1233 color: mul_alpha(bg, alpha_accum),
1234 radius: v.modifier.clip_rounded.map(dp_to_px).unwrap_or(0.0),
1235 });
1236 }
1237
1238 if let Some(b) = &v.modifier.border {
1240 scene.nodes.push(SceneNode::Border {
1241 rect,
1242 color: mul_alpha(b.color, alpha_accum),
1243 width: dp_to_px(b.width),
1244 radius: dp_to_px(b.radius.max(v.modifier.clip_rounded.unwrap_or(0.0))),
1245 });
1246 }
1247
1248 let this_alpha = v.modifier.alpha.unwrap_or(1.0);
1250 let alpha_accum = (alpha_accum * this_alpha).clamp(0.0, 1.0);
1251
1252 if let Some(tf) = v.modifier.transform {
1253 scene.nodes.push(SceneNode::PushTransform { transform: tf });
1254 }
1255
1256 if let Some(p) = &v.modifier.painter {
1258 (p)(scene, rect);
1259 }
1260
1261 let has_pointer = v.modifier.on_pointer_down.is_some()
1262 || v.modifier.on_pointer_move.is_some()
1263 || v.modifier.on_pointer_up.is_some()
1264 || v.modifier.on_pointer_enter.is_some()
1265 || v.modifier.on_pointer_leave.is_some();
1266
1267 if has_pointer || v.modifier.click {
1268 hits.push(HitRegion {
1269 id: v.id,
1270 rect,
1271 on_click: None, on_scroll: None, focusable: false,
1274 on_pointer_down: v.modifier.on_pointer_down.clone(),
1275 on_pointer_move: v.modifier.on_pointer_move.clone(),
1276 on_pointer_up: v.modifier.on_pointer_up.clone(),
1277 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1278 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1279 z_index: v.modifier.z_index,
1280 on_text_change: None,
1281 on_text_submit: None,
1282 tf_state_key: None,
1283 });
1284 }
1285
1286 match &v.kind {
1287 ViewKind::Text {
1288 text,
1289 color,
1290 font_size: font_dp,
1291 soft_wrap,
1292 max_lines,
1293 overflow,
1294 } => {
1295 let nid = nodes[&v.id];
1296 let tl = text_cache.get(&nid);
1297
1298 let (size_px_val, line_h_px_val, mut lines): (f32, f32, Vec<String>) =
1299 if let Some(tl) = tl {
1300 (tl.size_px, tl.line_h_px, tl.lines.clone())
1301 } else {
1302 let sz_px = font_px(*font_dp);
1304 (sz_px, sz_px * 1.3, vec![text.clone()])
1305 };
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 && !*soft_wrap {
1314 let dy_px = (draw_box.h - line_h_px_val) * 0.5;
1315 if dy_px.is_finite() && dy_px > 0.0 {
1316 draw_box.y += dy_px;
1317 draw_box.h = line_h_px_val;
1318 }
1319 }
1320
1321 let total_text_height = lines.len() as f32 * line_h_px_val;
1323
1324 let need_v_clip =
1325 total_text_height > max_h_px + 0.5 && *overflow != TextOverflow::Visible;
1326
1327 let max_visual_lines = if max_h_px > 0.5 && need_v_clip {
1328 (max_h_px / line_h_px_val).floor().max(1.0) as usize
1329 } else {
1330 lines.len()
1331 };
1332
1333 if lines.len() > max_visual_lines {
1334 lines.truncate(max_visual_lines);
1335 if *overflow == TextOverflow::Ellipsis && max_w_px > 0.5 && !lines.is_empty() {
1336 if let Some(last) = lines.last_mut() {
1338 *last = repose_text::ellipsize_line(last, size_px_val, max_w_px);
1339 }
1340 }
1341 }
1342
1343 let approx_w_px = (text.len() as f32) * size_px_val * 0.6;
1344 let need_h_clip = match overflow {
1345 TextOverflow::Visible => false,
1346 TextOverflow::Ellipsis => false, TextOverflow::Clip => approx_w_px > max_w_px + 0.5 || need_v_clip,
1348 };
1349
1350 let need_clip = need_h_clip || need_v_clip;
1351
1352 if need_clip {
1353 scene.nodes.push(SceneNode::PushClip {
1354 rect: content_rect,
1355 radius: 0.0,
1356 });
1357 }
1358
1359 if !*soft_wrap && matches!(overflow, TextOverflow::Ellipsis) {
1361 if approx_w_px > max_w_px + 0.5 {
1362 lines = vec![repose_text::ellipsize_line(text, size_px_val, max_w_px)];
1363 }
1364 }
1365
1366 for (i, ln) in lines.iter().enumerate() {
1367 scene.nodes.push(SceneNode::Text {
1368 rect: crate::Rect {
1369 x: content_rect.x,
1370 y: content_rect.y + i as f32 * line_h_px_val,
1371 w: content_rect.w,
1372 h: line_h_px_val,
1373 },
1374 text: ln.clone(),
1375 color: mul_alpha(*color, alpha_accum),
1376 size: size_px_val,
1377 });
1378 }
1379
1380 if need_clip {
1381 scene.nodes.push(SceneNode::PopClip);
1382 }
1383
1384 sems.push(SemNode {
1385 id: v.id,
1386 role: Role::Text,
1387 label: Some(text.clone()),
1388 rect,
1389 focused: is_focused,
1390 enabled: true,
1391 });
1392 }
1393
1394 ViewKind::Button { on_click } => {
1395 if v.modifier.background.is_none() {
1397 let th = locals::theme();
1398 let base = if is_pressed {
1399 th.button_bg_pressed
1400 } else if is_hovered {
1401 th.button_bg_hover
1402 } else {
1403 th.button_bg
1404 };
1405 scene.nodes.push(SceneNode::Rect {
1406 rect,
1407 color: mul_alpha(base, alpha_accum),
1408 radius: v.modifier.clip_rounded.map(dp_to_px).unwrap_or(6.0),
1409 });
1410 }
1411
1412 if v.modifier.click || on_click.is_some() {
1413 hits.push(HitRegion {
1414 id: v.id,
1415 rect,
1416 on_click: on_click.clone(),
1417 on_scroll: None,
1418 focusable: true,
1419 on_pointer_down: v.modifier.on_pointer_down.clone(),
1420 on_pointer_move: v.modifier.on_pointer_move.clone(),
1421 on_pointer_up: v.modifier.on_pointer_up.clone(),
1422 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1423 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1424 z_index: v.modifier.z_index,
1425 on_text_change: None,
1426 on_text_submit: None,
1427 tf_state_key: None,
1428 });
1429 }
1430
1431 sems.push(SemNode {
1432 id: v.id,
1433 role: Role::Button,
1434 label: None,
1435 rect,
1436 focused: is_focused,
1437 enabled: true,
1438 });
1439
1440 if is_focused {
1441 scene.nodes.push(SceneNode::Border {
1442 rect,
1443 color: mul_alpha(locals::theme().focus, alpha_accum),
1444 width: dp_to_px(2.0),
1445 radius: v
1446 .modifier
1447 .clip_rounded
1448 .map(dp_to_px)
1449 .unwrap_or(dp_to_px(6.0)),
1450 });
1451 }
1452 }
1453 ViewKind::Image { handle, tint, fit } => {
1454 scene.nodes.push(SceneNode::Image {
1455 rect,
1456 handle: *handle,
1457 tint: mul_alpha(*tint, alpha_accum),
1458 fit: *fit,
1459 });
1460 }
1461
1462 ViewKind::TextField {
1463 state_key,
1464 hint,
1465 on_change,
1466 on_submit,
1467 ..
1468 } => {
1469 let tf_key = if *state_key != 0 { *state_key } else { v.id };
1471
1472 hits.push(HitRegion {
1473 id: v.id,
1474 rect,
1475 on_click: None,
1476 on_scroll: None,
1477 focusable: true,
1478 on_pointer_down: None,
1479 on_pointer_move: None,
1480 on_pointer_up: None,
1481 on_pointer_enter: None,
1482 on_pointer_leave: None,
1483 z_index: v.modifier.z_index,
1484 on_text_change: on_change.clone(),
1485 on_text_submit: on_submit.clone(),
1486 tf_state_key: Some(tf_key),
1487 });
1488
1489 let pad_x_px = dp_to_px(TF_PADDING_X_DP);
1491 let inner = repose_core::Rect {
1492 x: rect.x + pad_x_px,
1493 y: rect.y + dp_to_px(8.0),
1494 w: rect.w - 2.0 * pad_x_px,
1495 h: rect.h - dp_to_px(16.0),
1496 };
1497 scene.nodes.push(SceneNode::PushClip {
1498 rect: inner,
1499 radius: 0.0,
1500 });
1501 if is_focused {
1503 scene.nodes.push(SceneNode::Border {
1504 rect,
1505 color: mul_alpha(locals::theme().focus, alpha_accum),
1506 width: dp_to_px(2.0),
1507 radius: v
1508 .modifier
1509 .clip_rounded
1510 .map(dp_to_px)
1511 .unwrap_or(dp_to_px(6.0)),
1512 });
1513 }
1514
1515 if let Some(state_rc) = textfield_states
1516 .get(&tf_key)
1517 .or_else(|| textfield_states.get(&v.id))
1518 {
1520 state_rc.borrow_mut().set_inner_width(inner.w);
1521
1522 let state = state_rc.borrow();
1523 let text_val = &state.text;
1524 let font_px_u32 = TF_FONT_DP as u32;
1525 let m = measure_text(text_val, font_px_u32);
1526
1527 if state.selection.start != state.selection.end {
1529 let i0 = byte_to_char_index(&m, state.selection.start);
1530 let i1 = byte_to_char_index(&m, state.selection.end);
1531 let sx_px =
1532 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1533 let ex_px =
1534 m.positions.get(i1).copied().unwrap_or(sx_px) - state.scroll_offset;
1535 let sel_x_px = inner.x + sx_px.max(0.0);
1536 let sel_w_px = (ex_px - sx_px).max(0.0);
1537 scene.nodes.push(SceneNode::Rect {
1538 rect: repose_core::Rect {
1539 x: sel_x_px,
1540 y: inner.y,
1541 w: sel_w_px,
1542 h: inner.h,
1543 },
1544 color: mul_alpha(Color::from_hex("#3B7BFF55"), alpha_accum),
1545 radius: 0.0,
1546 });
1547 }
1548
1549 if let Some(range) = &state.composition {
1551 if range.start < range.end && !text_val.is_empty() {
1552 let i0 = byte_to_char_index(&m, range.start);
1553 let i1 = byte_to_char_index(&m, range.end);
1554 let sx_px =
1555 m.positions.get(i0).copied().unwrap_or(0.0) - state.scroll_offset;
1556 let ex_px =
1557 m.positions.get(i1).copied().unwrap_or(sx_px) - state.scroll_offset;
1558 let ux = inner.x + sx_px.max(0.0);
1559 let uw = (ex_px - sx_px).max(0.0);
1560 scene.nodes.push(SceneNode::Rect {
1561 rect: repose_core::Rect {
1562 x: ux,
1563 y: inner.y + inner.h - dp_to_px(2.0),
1564 w: uw,
1565 h: dp_to_px(2.0),
1566 },
1567 color: mul_alpha(locals::theme().focus, alpha_accum),
1568 radius: 0.0,
1569 });
1570 }
1571 }
1572
1573 let text_color = if text_val.is_empty() {
1575 mul_alpha(Color::from_hex("#666666"), alpha_accum)
1576 } else {
1577 mul_alpha(locals::theme().on_surface, alpha_accum)
1578 };
1579 scene.nodes.push(SceneNode::Text {
1580 rect: repose_core::Rect {
1581 x: inner.x - state.scroll_offset,
1582 y: inner.y,
1583 w: inner.w,
1584 h: inner.h,
1585 },
1586 text: if text_val.is_empty() {
1587 hint.clone()
1588 } else {
1589 text_val.clone()
1590 },
1591 color: text_color,
1592 size: font_px(TF_FONT_DP),
1593 });
1594
1595 if state.selection.start == state.selection.end && state.caret_visible() {
1597 let i = byte_to_char_index(&m, state.selection.end);
1598 let cx_px =
1599 m.positions.get(i).copied().unwrap_or(0.0) - state.scroll_offset;
1600 let caret_x_px = inner.x + cx_px.max(0.0);
1601 scene.nodes.push(SceneNode::Rect {
1602 rect: repose_core::Rect {
1603 x: caret_x_px,
1604 y: inner.y,
1605 w: dp_to_px(1.0),
1606 h: inner.h,
1607 },
1608 color: mul_alpha(Color::WHITE, alpha_accum),
1609 radius: 0.0,
1610 });
1611 }
1612 scene.nodes.push(SceneNode::PopClip);
1614
1615 sems.push(SemNode {
1616 id: v.id,
1617 role: Role::TextField,
1618 label: Some(text_val.clone()),
1619 rect,
1620 focused: is_focused,
1621 enabled: true,
1622 });
1623 } else {
1624 scene.nodes.push(SceneNode::Text {
1626 rect: repose_core::Rect {
1627 x: inner.x,
1628 y: inner.y,
1629 w: inner.w,
1630 h: inner.h,
1631 },
1632 text: hint.clone(),
1633 color: mul_alpha(Color::from_hex("#666666"), alpha_accum),
1634 size: font_px(TF_FONT_DP),
1635 });
1636 scene.nodes.push(SceneNode::PopClip);
1637
1638 sems.push(SemNode {
1639 id: v.id,
1640 role: Role::TextField,
1641 label: Some(hint.clone()),
1642 rect,
1643 focused: is_focused,
1644 enabled: true,
1645 });
1646 }
1647 }
1648 ViewKind::ScrollV {
1649 on_scroll,
1650 set_viewport_height,
1651 set_content_height,
1652 get_scroll_offset,
1653 set_scroll_offset,
1654 } => {
1655 hits.push(HitRegion {
1657 id: v.id,
1658 rect, on_click: None,
1660 on_scroll: on_scroll.clone(),
1661 focusable: false,
1662 on_pointer_down: v.modifier.on_pointer_down.clone(),
1663 on_pointer_move: v.modifier.on_pointer_move.clone(),
1664 on_pointer_up: v.modifier.on_pointer_up.clone(),
1665 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1666 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1667 z_index: v.modifier.z_index,
1668 on_text_change: None,
1669 on_text_submit: None,
1670 tf_state_key: None,
1671 });
1672
1673 let vp = content_rect; if let Some(set_vh) = set_viewport_height {
1677 set_vh(vp.h.max(0.0));
1678 }
1679
1680 fn subtree_extents(node: taffy::NodeId, t: &TaffyTree<NodeCtx>) -> (f32, f32) {
1682 let l = t.layout(node).unwrap();
1683 let mut w = l.size.width;
1684 let mut h = l.size.height;
1685 if let Ok(children) = t.children(node) {
1686 for &ch in children.iter() {
1687 let cl = t.layout(ch).unwrap();
1688 let (cw, chh) = subtree_extents(ch, t);
1689 w = w.max(cl.location.x + cw);
1690 h = h.max(cl.location.y + chh);
1691 }
1692 }
1693 (w, h)
1694 }
1695 let mut content_h_px = 0.0f32;
1696 for c in &v.children {
1697 let nid = nodes[&c.id];
1698 let l = t.layout(nid).unwrap();
1699 let (_cw, chh) = subtree_extents(nid, t);
1700 content_h_px = content_h_px.max(l.location.y + chh);
1701 }
1702 if let Some(set_ch) = set_content_height {
1703 set_ch(content_h_px);
1704 }
1705
1706 scene.nodes.push(SceneNode::PushClip {
1708 rect: vp,
1709 radius: 0.0, });
1711
1712 let hit_start = hits.len();
1714 let scroll_offset_px = if let Some(get) = get_scroll_offset {
1715 get()
1716 } else {
1717 0.0
1718 };
1719 let child_offset_px = (base_px.0 + pad_dx, base_px.1 + pad_dy - scroll_offset_px);
1720 for c in &v.children {
1721 walk(
1722 c,
1723 t,
1724 nodes,
1725 scene,
1726 hits,
1727 sems,
1728 textfield_states,
1729 interactions,
1730 focused,
1731 child_offset_px,
1732 alpha_accum,
1733 text_cache,
1734 font_px,
1735 );
1736 }
1737
1738 let mut i = hit_start;
1740 while i < hits.len() {
1741 if let Some(r) = intersect(hits[i].rect, vp) {
1742 hits[i].rect = r;
1743 i += 1;
1744 } else {
1745 hits.remove(i);
1746 }
1747 }
1748
1749 push_scrollbar_v(
1751 scene,
1752 hits,
1753 interactions,
1754 v.id,
1755 vp,
1756 content_h_px,
1757 scroll_offset_px,
1758 v.modifier.z_index,
1759 set_scroll_offset.clone(),
1760 );
1761
1762 scene.nodes.push(SceneNode::PopClip);
1763 return;
1764 }
1765 ViewKind::ScrollXY {
1766 on_scroll,
1767 set_viewport_width,
1768 set_viewport_height,
1769 set_content_width,
1770 set_content_height,
1771 get_scroll_offset_xy,
1772 set_scroll_offset_xy,
1773 } => {
1774 hits.push(HitRegion {
1775 id: v.id,
1776 rect,
1777 on_click: None,
1778 on_scroll: on_scroll.clone(),
1779 focusable: false,
1780 on_pointer_down: v.modifier.on_pointer_down.clone(),
1781 on_pointer_move: v.modifier.on_pointer_move.clone(),
1782 on_pointer_up: v.modifier.on_pointer_up.clone(),
1783 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1784 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1785 z_index: v.modifier.z_index,
1786 on_text_change: None,
1787 on_text_submit: None,
1788 tf_state_key: None,
1789 });
1790
1791 let vp = content_rect;
1792
1793 if let Some(set_w) = set_viewport_width {
1794 set_w(vp.w.max(0.0));
1795 }
1796 if let Some(set_h) = set_viewport_height {
1797 set_h(vp.h.max(0.0));
1798 }
1799
1800 fn subtree_extents(node: taffy::NodeId, t: &TaffyTree<NodeCtx>) -> (f32, f32) {
1801 let l = t.layout(node).unwrap();
1802 let mut w = l.size.width;
1803 let mut h = l.size.height;
1804 if let Ok(children) = t.children(node) {
1805 for &ch in children.iter() {
1806 let cl = t.layout(ch).unwrap();
1807 let (cw, chh) = subtree_extents(ch, t);
1808 w = w.max(cl.location.x + cw);
1809 h = h.max(cl.location.y + chh);
1810 }
1811 }
1812 (w, h)
1813 }
1814 let mut content_w_px = 0.0f32;
1815 let mut content_h_px = 0.0f32;
1816 for c in &v.children {
1817 let nid = nodes[&c.id];
1818 let l = t.layout(nid).unwrap();
1819 let (cw, chh) = subtree_extents(nid, t);
1820 content_w_px = content_w_px.max(l.location.x + cw);
1821 content_h_px = content_h_px.max(l.location.y + chh);
1822 }
1823 if let Some(set_cw) = set_content_width {
1824 set_cw(content_w_px);
1825 }
1826 if let Some(set_ch) = set_content_height {
1827 set_ch(content_h_px);
1828 }
1829
1830 scene.nodes.push(SceneNode::PushClip {
1831 rect: vp,
1832 radius: 0.0,
1833 });
1834
1835 let hit_start = hits.len();
1836 let (ox_px, oy_px) = if let Some(get) = get_scroll_offset_xy {
1837 get()
1838 } else {
1839 (0.0, 0.0)
1840 };
1841 let child_offset_px = (base_px.0 + pad_dx - ox_px, base_px.1 + pad_dy - oy_px);
1842 for c in &v.children {
1843 walk(
1844 c,
1845 t,
1846 nodes,
1847 scene,
1848 hits,
1849 sems,
1850 textfield_states,
1851 interactions,
1852 focused,
1853 child_offset_px,
1854 alpha_accum,
1855 text_cache,
1856 font_px,
1857 );
1858 }
1859 let mut i = hit_start;
1861 while i < hits.len() {
1862 if let Some(r) = intersect(hits[i].rect, vp) {
1863 hits[i].rect = r;
1864 i += 1;
1865 } else {
1866 hits.remove(i);
1867 }
1868 }
1869
1870 let set_scroll_y: Option<Rc<dyn Fn(f32)>> =
1871 set_scroll_offset_xy.clone().map(|set_xy| {
1872 let ox = ox_px; Rc::new(move |y| set_xy(ox, y)) as Rc<dyn Fn(f32)>
1874 });
1875
1876 push_scrollbar_v(
1878 scene,
1879 hits,
1880 interactions,
1881 v.id,
1882 vp,
1883 content_h_px,
1884 oy_px,
1885 v.modifier.z_index,
1886 set_scroll_y,
1887 );
1888 push_scrollbar_h(
1889 scene,
1890 hits,
1891 interactions,
1892 v.id,
1893 vp,
1894 content_w_px,
1895 ox_px,
1896 v.modifier.z_index,
1897 set_scroll_offset_xy.clone(),
1898 oy_px,
1899 );
1900
1901 scene.nodes.push(SceneNode::PopClip);
1902 return;
1903 }
1904 ViewKind::Checkbox { checked, on_change } => {
1905 let theme = locals::theme();
1906 let box_size_px = dp_to_px(18.0);
1908 let bx = rect.x;
1909 let by = rect.y + (rect.h - box_size_px) * 0.5;
1910 scene.nodes.push(SceneNode::Rect {
1912 rect: repose_core::Rect {
1913 x: bx,
1914 y: by,
1915 w: box_size_px,
1916 h: box_size_px,
1917 },
1918 color: if *checked {
1919 mul_alpha(theme.primary, alpha_accum)
1920 } else {
1921 mul_alpha(theme.surface, alpha_accum)
1922 },
1923 radius: dp_to_px(3.0),
1924 });
1925 scene.nodes.push(SceneNode::Border {
1926 rect: repose_core::Rect {
1927 x: bx,
1928 y: by,
1929 w: box_size_px,
1930 h: box_size_px,
1931 },
1932 color: mul_alpha(theme.outline, alpha_accum),
1933 width: dp_to_px(1.0),
1934 radius: dp_to_px(3.0),
1935 });
1936 if *checked {
1938 scene.nodes.push(SceneNode::Text {
1939 rect: repose_core::Rect {
1940 x: bx + dp_to_px(3.0),
1941 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
1942 w: rect.w - (box_size_px + dp_to_px(8.0)),
1943 h: font_px(16.0),
1944 },
1945 text: "✓".to_string(),
1946 color: mul_alpha(theme.on_primary, alpha_accum),
1947 size: font_px(16.0),
1948 });
1949 }
1950 let toggled = !*checked;
1952 let on_click = on_change.as_ref().map(|cb| {
1953 let cb = cb.clone();
1954 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
1955 });
1956 hits.push(HitRegion {
1957 id: v.id,
1958 rect,
1959 on_click,
1960 on_scroll: None,
1961 focusable: true,
1962 on_pointer_down: v.modifier.on_pointer_down.clone(),
1963 on_pointer_move: v.modifier.on_pointer_move.clone(),
1964 on_pointer_up: v.modifier.on_pointer_up.clone(),
1965 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
1966 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
1967 z_index: v.modifier.z_index,
1968 on_text_change: None,
1969 on_text_submit: None,
1970 tf_state_key: None,
1971 });
1972 sems.push(SemNode {
1973 id: v.id,
1974 role: Role::Checkbox,
1975 label: None,
1976 rect,
1977 focused: is_focused,
1978 enabled: true,
1979 });
1980 if is_focused {
1981 scene.nodes.push(SceneNode::Border {
1982 rect,
1983 color: mul_alpha(locals::theme().focus, alpha_accum),
1984 width: dp_to_px(2.0),
1985 radius: v
1986 .modifier
1987 .clip_rounded
1988 .map(dp_to_px)
1989 .unwrap_or(dp_to_px(6.0)),
1990 });
1991 }
1992 }
1993
1994 ViewKind::RadioButton {
1995 selected,
1996
1997 on_select,
1998 } => {
1999 let theme = locals::theme();
2000 let d_px = dp_to_px(18.0);
2001 let cx = rect.x;
2002 let cy = rect.y + (rect.h - d_px) * 0.5;
2003
2004 scene.nodes.push(SceneNode::Border {
2006 rect: repose_core::Rect {
2007 x: cx,
2008 y: cy,
2009 w: d_px,
2010 h: d_px,
2011 },
2012 color: mul_alpha(theme.outline, alpha_accum),
2013 width: dp_to_px(1.5),
2014 radius: d_px * 0.5,
2015 });
2016 if *selected {
2018 scene.nodes.push(SceneNode::Rect {
2019 rect: repose_core::Rect {
2020 x: cx + dp_to_px(4.0),
2021 y: cy + dp_to_px(4.0),
2022 w: d_px - dp_to_px(8.0),
2023 h: d_px - dp_to_px(8.0),
2024 },
2025 color: mul_alpha(theme.primary, alpha_accum),
2026 radius: (d_px - dp_to_px(8.0)) * 0.5,
2027 });
2028 }
2029
2030 hits.push(HitRegion {
2031 id: v.id,
2032 rect,
2033 on_click: on_select.clone(),
2034 on_scroll: None,
2035 focusable: true,
2036 on_pointer_down: v.modifier.on_pointer_down.clone(),
2037 on_pointer_move: v.modifier.on_pointer_move.clone(),
2038 on_pointer_up: v.modifier.on_pointer_up.clone(),
2039 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2040 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2041 z_index: v.modifier.z_index,
2042 on_text_change: None,
2043 on_text_submit: None,
2044 tf_state_key: None,
2045 });
2046 sems.push(SemNode {
2047 id: v.id,
2048 role: Role::RadioButton,
2049 label: None,
2050 rect,
2051 focused: is_focused,
2052 enabled: true,
2053 });
2054 if is_focused {
2055 scene.nodes.push(SceneNode::Border {
2056 rect,
2057 color: mul_alpha(locals::theme().focus, alpha_accum),
2058 width: dp_to_px(2.0),
2059 radius: v
2060 .modifier
2061 .clip_rounded
2062 .map(dp_to_px)
2063 .unwrap_or(dp_to_px(6.0)),
2064 });
2065 }
2066 }
2067
2068 ViewKind::Switch { checked, on_change } => {
2069 let theme = locals::theme();
2070 let track_w_px = dp_to_px(46.0);
2072 let track_h_px = dp_to_px(26.0);
2073 let tx = rect.x;
2074 let ty = rect.y + (rect.h - track_h_px) * 0.5;
2075 let knob_px = dp_to_px(22.0);
2076 let on_col = theme.primary;
2077 let off_col = Color::from_hex("#333333");
2078
2079 scene.nodes.push(SceneNode::Rect {
2081 rect: repose_core::Rect {
2082 x: tx,
2083 y: ty,
2084 w: track_w_px,
2085 h: track_h_px,
2086 },
2087 color: if *checked {
2088 mul_alpha(on_col, alpha_accum)
2089 } else {
2090 mul_alpha(off_col, alpha_accum)
2091 },
2092 radius: track_h_px * 0.5,
2093 });
2094 let kx = if *checked {
2096 tx + track_w_px - knob_px - dp_to_px(2.0)
2097 } else {
2098 tx + dp_to_px(2.0)
2099 };
2100 let ky = ty + (track_h_px - knob_px) * 0.5;
2101 scene.nodes.push(SceneNode::Rect {
2102 rect: repose_core::Rect {
2103 x: kx,
2104 y: ky,
2105 w: knob_px,
2106 h: knob_px,
2107 },
2108 color: mul_alpha(Color::from_hex("#EEEEEE"), alpha_accum),
2109 radius: knob_px * 0.5,
2110 });
2111 scene.nodes.push(SceneNode::Border {
2112 rect: repose_core::Rect {
2113 x: kx,
2114 y: ky,
2115 w: knob_px,
2116 h: knob_px,
2117 },
2118 color: mul_alpha(theme.outline, alpha_accum),
2119 width: dp_to_px(1.0),
2120 radius: knob_px * 0.5,
2121 });
2122
2123 let toggled = !*checked;
2124 let on_click = on_change.as_ref().map(|cb| {
2125 let cb = cb.clone();
2126 Rc::new(move || cb(toggled)) as Rc<dyn Fn()>
2127 });
2128 hits.push(HitRegion {
2129 id: v.id,
2130 rect,
2131 on_click,
2132 on_scroll: None,
2133 focusable: true,
2134 on_pointer_down: v.modifier.on_pointer_down.clone(),
2135 on_pointer_move: v.modifier.on_pointer_move.clone(),
2136 on_pointer_up: v.modifier.on_pointer_up.clone(),
2137 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2138 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2139 z_index: v.modifier.z_index,
2140 on_text_change: None,
2141 on_text_submit: None,
2142 tf_state_key: None,
2143 });
2144 sems.push(SemNode {
2145 id: v.id,
2146 role: Role::Switch,
2147 label: None,
2148 rect,
2149 focused: is_focused,
2150 enabled: true,
2151 });
2152 if is_focused {
2153 scene.nodes.push(SceneNode::Border {
2154 rect,
2155 color: mul_alpha(locals::theme().focus, alpha_accum),
2156 width: dp_to_px(2.0),
2157 radius: v
2158 .modifier
2159 .clip_rounded
2160 .map(dp_to_px)
2161 .unwrap_or(dp_to_px(6.0)),
2162 });
2163 }
2164 }
2165 ViewKind::Slider {
2166 value,
2167 min,
2168 max,
2169 step,
2170 on_change,
2171 } => {
2172 let theme = locals::theme();
2173 let track_h_px = dp_to_px(4.0);
2175 let knob_d_px = dp_to_px(20.0);
2176 let gap_px = dp_to_px(8.0);
2177 let label_x = rect.x + rect.w * 0.6; let track_x = rect.x;
2179 let track_w_px = (label_x - track_x).max(dp_to_px(60.0));
2180 let cy = rect.y + rect.h * 0.5;
2181
2182 scene.nodes.push(SceneNode::Rect {
2184 rect: repose_core::Rect {
2185 x: track_x,
2186 y: cy - track_h_px * 0.5,
2187 w: track_w_px,
2188 h: track_h_px,
2189 },
2190 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2191 radius: track_h_px * 0.5,
2192 });
2193
2194 let t = clamp01(norm(*value, *min, *max));
2196 let kx = track_x + t * track_w_px;
2197 scene.nodes.push(SceneNode::Rect {
2198 rect: repose_core::Rect {
2199 x: kx - knob_d_px * 0.5,
2200 y: cy - knob_d_px * 0.5,
2201 w: knob_d_px,
2202 h: knob_d_px,
2203 },
2204 color: mul_alpha(theme.surface, alpha_accum),
2205 radius: knob_d_px * 0.5,
2206 });
2207 scene.nodes.push(SceneNode::Border {
2208 rect: repose_core::Rect {
2209 x: kx - knob_d_px * 0.5,
2210 y: cy - knob_d_px * 0.5,
2211 w: knob_d_px,
2212 h: knob_d_px,
2213 },
2214 color: mul_alpha(theme.outline, alpha_accum),
2215 width: dp_to_px(1.0),
2216 radius: knob_d_px * 0.5,
2217 });
2218
2219 let on_change_cb: Option<Rc<dyn Fn(f32)>> = on_change.as_ref().cloned();
2221 let minv = *min;
2222 let maxv = *max;
2223 let stepv = *step;
2224
2225 let current = Rc::new(RefCell::new(*value));
2227
2228 let update_at = {
2230 let on_change_cb = on_change_cb.clone();
2231 let current = current.clone();
2232 Rc::new(move |px_pos: f32| {
2233 let tt = clamp01((px_pos - track_x) / track_w_px);
2234 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2235 *current.borrow_mut() = v;
2236 if let Some(cb) = &on_change_cb {
2237 cb(v);
2238 }
2239 })
2240 };
2241
2242 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2244 let f = update_at.clone();
2245 Rc::new(move |pe| {
2246 f(pe.position.x);
2247 })
2248 };
2249
2250 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2252 let f = update_at.clone();
2253 Rc::new(move |pe| {
2254 f(pe.position.x);
2255 })
2256 };
2257
2258 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = Rc::new(move |_pe| {});
2260
2261 let on_scroll = {
2263 let on_change_cb = on_change_cb.clone();
2264 let current = current.clone();
2265 Rc::new(move |d: Vec2| -> Vec2 {
2266 let base = *current.borrow();
2267 let delta = stepv.unwrap_or((maxv - minv) * 0.01);
2268 let dir = if d.y.is_sign_negative() { 1.0 } else { -1.0 };
2270 let new_v = snap_step(base + dir * delta, stepv, minv, maxv);
2271 *current.borrow_mut() = new_v;
2272 if let Some(cb) = &on_change_cb {
2273 cb(new_v);
2274 }
2275 Vec2 { x: d.x, y: 0.0 } })
2277 };
2278
2279 hits.push(HitRegion {
2281 id: v.id,
2282 rect,
2283 on_click: None,
2284 on_scroll: Some(on_scroll),
2285 focusable: true,
2286 on_pointer_down: Some(on_pd),
2287 on_pointer_move: if is_pressed { Some(on_pm) } else { None },
2288 on_pointer_up: Some(on_pu),
2289 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2290 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2291 z_index: v.modifier.z_index,
2292 on_text_change: None,
2293 on_text_submit: None,
2294 tf_state_key: None,
2295 });
2296
2297 sems.push(SemNode {
2298 id: v.id,
2299 role: Role::Slider,
2300 label: None,
2301 rect,
2302 focused: is_focused,
2303 enabled: true,
2304 });
2305 if is_focused {
2306 scene.nodes.push(SceneNode::Border {
2307 rect,
2308 color: mul_alpha(locals::theme().focus, alpha_accum),
2309 width: dp_to_px(2.0),
2310 radius: v
2311 .modifier
2312 .clip_rounded
2313 .map(dp_to_px)
2314 .unwrap_or(dp_to_px(6.0)),
2315 });
2316 }
2317 }
2318 ViewKind::RangeSlider {
2319 start,
2320 end,
2321 min,
2322 max,
2323 step,
2324 on_change,
2325 } => {
2326 let theme = locals::theme();
2327 let track_h_px = dp_to_px(4.0);
2328 let knob_d_px = dp_to_px(20.0);
2329 let gap_px = dp_to_px(8.0);
2330 let label_x = rect.x + rect.w * 0.6;
2331 let track_x = rect.x;
2332 let track_w_px = (label_x - track_x).max(dp_to_px(80.0));
2333 let cy = rect.y + rect.h * 0.5;
2334
2335 scene.nodes.push(SceneNode::Rect {
2337 rect: repose_core::Rect {
2338 x: track_x,
2339 y: cy - track_h_px * 0.5,
2340 w: track_w_px,
2341 h: track_h_px,
2342 },
2343 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2344 radius: track_h_px * 0.5,
2345 });
2346
2347 let t0 = clamp01(norm(*start, *min, *max));
2349 let t1 = clamp01(norm(*end, *min, *max));
2350 let k0x = track_x + t0 * track_w_px;
2351 let k1x = track_x + t1 * track_w_px;
2352
2353 scene.nodes.push(SceneNode::Rect {
2355 rect: repose_core::Rect {
2356 x: k0x.min(k1x),
2357 y: cy - track_h_px * 0.5,
2358 w: (k1x - k0x).abs(),
2359 h: track_h_px,
2360 },
2361 color: mul_alpha(theme.primary, alpha_accum),
2362 radius: track_h_px * 0.5,
2363 });
2364
2365 for &kx in &[k0x, k1x] {
2367 scene.nodes.push(SceneNode::Rect {
2368 rect: repose_core::Rect {
2369 x: kx - knob_d_px * 0.5,
2370 y: cy - knob_d_px * 0.5,
2371 w: knob_d_px,
2372 h: knob_d_px,
2373 },
2374 color: mul_alpha(theme.surface, alpha_accum),
2375 radius: knob_d_px * 0.5,
2376 });
2377 scene.nodes.push(SceneNode::Border {
2378 rect: repose_core::Rect {
2379 x: kx - knob_d_px * 0.5,
2380 y: cy - knob_d_px * 0.5,
2381 w: knob_d_px,
2382 h: knob_d_px,
2383 },
2384 color: mul_alpha(theme.outline, alpha_accum),
2385 width: dp_to_px(1.0),
2386 radius: knob_d_px * 0.5,
2387 });
2388 }
2389
2390 let on_change_cb = on_change.as_ref().cloned();
2392 let minv = *min;
2393 let maxv = *max;
2394 let stepv = *step;
2395 let start_val = *start;
2396 let end_val = *end;
2397
2398 let active = Rc::new(RefCell::new(None::<u8>));
2400
2401 let update = {
2403 let active = active.clone();
2404 let on_change_cb = on_change_cb.clone();
2405 Rc::new(move |px_pos: f32| {
2406 if let Some(thumb) = *active.borrow() {
2407 let tt = clamp01((px_pos - track_x) / track_w_px);
2408 let v = snap_step(denorm(tt, minv, maxv), stepv, minv, maxv);
2409 match thumb {
2410 0 => {
2411 let new_start = v.min(end_val).min(maxv).max(minv);
2412 if let Some(cb) = &on_change_cb {
2413 cb(new_start, end_val);
2414 }
2415 }
2416 _ => {
2417 let new_end = v.max(start_val).max(minv).min(maxv);
2418 if let Some(cb) = &on_change_cb {
2419 cb(start_val, new_end);
2420 }
2421 }
2422 }
2423 }
2424 })
2425 };
2426
2427 let on_pd: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2429 let active = active.clone();
2430 let update = update.clone();
2431 let k0x0 = k0x;
2433 let k1x0 = k1x;
2434 Rc::new(move |pe| {
2435 let px_pos = pe.position.x;
2436 let d0 = (px_pos - k0x0).abs();
2437 let d1 = (px_pos - k1x0).abs();
2438 *active.borrow_mut() = Some(if d0 <= d1 { 0 } else { 1 });
2439 update(px_pos);
2440 })
2441 };
2442
2443 let on_pm: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2445 let active = active.clone();
2446 let update = update.clone();
2447 Rc::new(move |pe| {
2448 if active.borrow().is_some() {
2449 update(pe.position.x);
2450 }
2451 })
2452 };
2453
2454 let on_pu: Rc<dyn Fn(repose_core::input::PointerEvent)> = {
2456 let active = active.clone();
2457 Rc::new(move |_pe| {
2458 *active.borrow_mut() = None;
2459 })
2460 };
2461
2462 hits.push(HitRegion {
2463 id: v.id,
2464 rect,
2465 on_click: None,
2466 on_scroll: None,
2467 focusable: true,
2468 on_pointer_down: Some(on_pd),
2469 on_pointer_move: Some(on_pm),
2470 on_pointer_up: Some(on_pu),
2471 on_pointer_enter: v.modifier.on_pointer_enter.clone(),
2472 on_pointer_leave: v.modifier.on_pointer_leave.clone(),
2473 z_index: v.modifier.z_index,
2474 on_text_change: None,
2475 on_text_submit: None,
2476 tf_state_key: None,
2477 });
2478 sems.push(SemNode {
2479 id: v.id,
2480 role: Role::Slider,
2481 label: None,
2482 rect,
2483 focused: is_focused,
2484 enabled: true,
2485 });
2486 if is_focused {
2487 scene.nodes.push(SceneNode::Border {
2488 rect,
2489 color: mul_alpha(locals::theme().focus, alpha_accum),
2490 width: dp_to_px(2.0),
2491 radius: v
2492 .modifier
2493 .clip_rounded
2494 .map(dp_to_px)
2495 .unwrap_or(dp_to_px(6.0)),
2496 });
2497 }
2498 }
2499 ViewKind::ProgressBar {
2500 value,
2501 min,
2502 max,
2503 circular: _,
2504 } => {
2505 let theme = locals::theme();
2506 let track_h_px = dp_to_px(6.0);
2507 let gap_px = dp_to_px(8.0);
2508 let label_w_split_px = rect.w * 0.6;
2509 let track_x = rect.x;
2510 let track_w_px = (label_w_split_px - track_x).max(dp_to_px(60.0));
2511 let cy = rect.y + rect.h * 0.5;
2512
2513 scene.nodes.push(SceneNode::Rect {
2514 rect: repose_core::Rect {
2515 x: track_x,
2516 y: cy - track_h_px * 0.5,
2517 w: track_w_px,
2518 h: track_h_px,
2519 },
2520 color: mul_alpha(Color::from_hex("#333333"), alpha_accum),
2521 radius: track_h_px * 0.5,
2522 });
2523
2524 let t = clamp01(norm(*value, *min, *max));
2525 scene.nodes.push(SceneNode::Rect {
2526 rect: repose_core::Rect {
2527 x: track_x,
2528 y: cy - track_h_px * 0.5,
2529 w: track_w_px * t,
2530 h: track_h_px,
2531 },
2532 color: mul_alpha(theme.primary, alpha_accum),
2533 radius: track_h_px * 0.5,
2534 });
2535
2536 scene.nodes.push(SceneNode::Text {
2537 rect: repose_core::Rect {
2538 x: rect.x + label_w_split_px + gap_px,
2539 y: rect.y + rect.h * 0.5 - font_px(16.0) * 0.6,
2540 w: rect.w - (label_w_split_px + gap_px),
2541 h: font_px(16.0),
2542 },
2543 text: format!("{:.0}%", t * 100.0),
2544 color: mul_alpha(theme.on_surface, alpha_accum),
2545 size: font_px(16.0),
2546 });
2547
2548 sems.push(SemNode {
2549 id: v.id,
2550 role: Role::ProgressBar,
2551 label: None,
2552 rect,
2553 focused: is_focused,
2554 enabled: true,
2555 });
2556 }
2557
2558 _ => {}
2559 }
2560
2561 for c in &v.children {
2563 walk(
2564 c,
2565 t,
2566 nodes,
2567 scene,
2568 hits,
2569 sems,
2570 textfield_states,
2571 interactions,
2572 focused,
2573 base_px,
2574 alpha_accum,
2575 text_cache,
2576 font_px,
2577 );
2578 }
2579
2580 if v.modifier.transform.is_some() {
2581 scene.nodes.push(SceneNode::PopTransform);
2582 }
2583 }
2584
2585 let font_px = |dp_font: f32| dp_to_px(dp_font) * locals::text_scale().0;
2586
2587 walk(
2589 &root,
2590 &taffy,
2591 &nodes_map,
2592 &mut scene,
2593 &mut hits,
2594 &mut sems,
2595 textfield_states,
2596 interactions,
2597 focused,
2598 (0.0, 0.0),
2599 1.0,
2600 &text_cache,
2601 &font_px,
2602 );
2603
2604 hits.sort_by(|a, b| a.z_index.partial_cmp(&b.z_index).unwrap_or(Ordering::Equal));
2606
2607 (scene, hits, sems)
2608}
2609
2610pub trait TextStyle {
2612 fn color(self, c: Color) -> View;
2613 fn size(self, px: f32) -> View;
2614 fn max_lines(self, n: usize) -> View;
2615 fn single_line(self) -> View;
2616 fn overflow_ellipsize(self) -> View;
2617 fn overflow_clip(self) -> View;
2618 fn overflow_visible(self) -> View;
2619}
2620impl TextStyle for View {
2621 fn color(mut self, c: Color) -> View {
2622 if let ViewKind::Text {
2623 color: text_color, ..
2624 } = &mut self.kind
2625 {
2626 *text_color = c;
2627 }
2628 self
2629 }
2630 fn size(mut self, dp_font: f32) -> View {
2631 if let ViewKind::Text {
2632 font_size: text_size_dp,
2633 ..
2634 } = &mut self.kind
2635 {
2636 *text_size_dp = dp_font;
2637 }
2638 self
2639 }
2640 fn max_lines(mut self, n: usize) -> View {
2641 if let ViewKind::Text {
2642 max_lines,
2643 soft_wrap,
2644 ..
2645 } = &mut self.kind
2646 {
2647 *max_lines = Some(n);
2648 *soft_wrap = true;
2649 }
2650 self
2651 }
2652 fn single_line(mut self) -> View {
2653 if let ViewKind::Text {
2654 soft_wrap,
2655 max_lines,
2656 ..
2657 } = &mut self.kind
2658 {
2659 *soft_wrap = false;
2660 *max_lines = Some(1);
2661 }
2662 self
2663 }
2664 fn overflow_ellipsize(mut self) -> View {
2665 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2666 *overflow = TextOverflow::Ellipsis;
2667 }
2668 self
2669 }
2670 fn overflow_clip(mut self) -> View {
2671 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2672 *overflow = TextOverflow::Clip;
2673 }
2674 self
2675 }
2676 fn overflow_visible(mut self) -> View {
2677 if let ViewKind::Text { overflow, .. } = &mut self.kind {
2678 *overflow = TextOverflow::Visible;
2679 }
2680 self
2681 }
2682}