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