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