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