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