1use super::*;
2
3pub fn mount<Msg: Clone>(layout: &mut LayoutTree, v: View<Msg>) -> Mounted<Msg> {
4 let mut nodes = Vec::new();
5 let mut text_measures = std::collections::HashMap::new();
6 let root = mount_recursive(layout, v, &mut nodes, &mut text_measures);
7 Mounted { root, nodes, text_measures }
8}
9
10pub fn mount_recursive<Msg: Clone>(
14 layout: &mut LayoutTree,
15 v: View<Msg>,
16 out: &mut Vec<MountedNode<Msg>>,
17 text_measures: &mut std::collections::HashMap<NodeId, TextMeasure>,
18) -> NodeId {
19 let View {
20 style,
21 fill,
22 hover_fill,
23 radius,
24 corner_radii,
25 shadow,
26 fill_gradient,
27 border,
28 text,
29 image,
30 image_fit,
31 mask_image,
32 mask_placement,
33 mask_extra,
34 painter,
35 gpu_painter,
36 over_painter,
37 on_click,
38 on_click_at,
39 on_right_click,
40 on_right_click_at,
41 on_middle_click,
42 drag,
43 drag_at,
44 drag_velocity,
45 drag_payload,
46 on_drop,
47 drop_hover_fill,
48 clip,
49 clip_inset,
50 clip_ellipse,
51 clip_polygon,
52 clip_path_svg,
53 clip_ref_inset,
54 on_pointer_enter,
55 on_pointer_leave,
56 on_pointer_move_at,
57 on_scroll,
58 on_scale,
59 on_rotate,
60 on_double_tap,
61 on_double_tap_at,
62 on_long_press,
63 on_long_press_at,
64 focusable,
65 text_select_key,
66 alpha,
67 anim,
68 animated_size,
69 semantics,
70 hero,
71 transform,
72 transform_rel,
73 transform_origin,
74 tooltip,
75 cursor,
76 ripple,
77 layout_builder,
78 backdrop_blur,
79 filter,
80 blend,
81 children,
82 } = v;
83 let parent_idx = out.len();
84 out.push(MountedNode {
85 id: NodeId::new(0), fill,
87 hover_fill,
88 radius,
89 corner_radii,
90 shadow,
91 fill_gradient,
92 border,
93 text,
94 image,
95 image_fit,
96 mask_image,
97 mask_placement,
98 mask_extra,
99 painter,
100 gpu_painter,
101 over_painter,
102 on_click,
103 on_click_at,
104 on_right_click,
105 on_right_click_at,
106 on_middle_click,
107 drag,
108 drag_at,
109 drag_velocity,
110 drag_payload,
111 on_drop,
112 drop_hover_fill,
113 clip,
114 clip_inset,
115 clip_ellipse,
116 clip_polygon,
117 clip_path_svg,
118 clip_ref_inset,
119 on_pointer_enter,
120 on_pointer_leave,
121 on_pointer_move_at,
122 on_scroll,
123 on_scale,
124 on_rotate,
125 on_double_tap,
126 on_double_tap_at,
127 on_long_press,
128 on_long_press_at,
129 focusable,
130 text_select_key,
131 alpha,
132 anim,
133 animated_size,
134 semantics,
135 hero,
136 transform,
137 transform_rel,
138 transform_origin,
139 tooltip,
140 cursor,
141 ripple,
142 is_layout_builder: layout_builder.is_some(),
146 backdrop_blur,
147 filter,
148 blend,
149 subtree_end: 0,
150 });
151 let mut child_ids = Vec::with_capacity(children.len());
152 for child in children {
153 child_ids.push(mount_recursive(layout, child, out, text_measures));
154 }
155 let id = if child_ids.is_empty() {
156 layout.leaf(style).expect("layout leaf")
157 } else {
158 layout.node(style, &child_ids).expect("layout node")
159 };
160 out[parent_idx].id = id;
161 out[parent_idx].subtree_end = out.len();
162 if child_ids.is_empty() {
166 if let Some(text) = out[parent_idx].text.as_ref() {
167 if text.runs.is_none() {
168 text_measures.insert(
169 id,
170 TextMeasure {
171 content: text.content.clone(),
172 size_px: text.size_px,
173 alignment: text.alignment,
174 italic: text.italic,
175 font_family: text.font_family.clone(),
176 line_height: text.line_height,
177 weight: text.weight,
178 max_lines: text.max_lines,
179 ellipsis: text.ellipsis,
180 underline: text.underline,
181 strikethrough: text.strikethrough,
182 spans: text.spans.clone(),
183 letter_spacing: text.letter_spacing,
184 word_spacing: text.word_spacing,
185 no_wrap: text.no_wrap,
186 overflow_wrap: text.overflow_wrap,
187 },
188 );
189 }
190 }
191 }
192 id
193}
194
195pub fn measure_text_node(
202 ts: &mut llimphi_text::Typesetter,
203 tm: &TextMeasure,
204 known: llimphi_layout::taffy::Size<Option<f32>>,
205 available: llimphi_layout::taffy::Size<llimphi_layout::taffy::AvailableSpace>,
206) -> llimphi_layout::taffy::Size<f32> {
207 use llimphi_layout::taffy::AvailableSpace;
208 let max_width: Option<f32> = if tm.no_wrap {
211 None
212 } else {
213 known.width.or(match available.width {
214 AvailableSpace::Definite(w) => Some(w),
215 AvailableSpace::MaxContent => None,
216 AvailableSpace::MinContent => Some(0.0),
217 })
218 };
219 if let Some(spans) = tm.spans.as_ref() {
227 if !spans.is_empty() {
228 let layout = ts.layout_spans(
229 &tm.content,
230 tm.size_px,
231 vello::peniko::Color::from_rgba8(0, 0, 0, 255),
232 tm.weight,
233 tm.line_height,
234 tm.italic,
235 tm.font_family.as_deref(),
236 tm.underline,
237 tm.strikethrough,
238 spans,
239 max_width,
240 tm.alignment,
241 );
242 return llimphi_layout::taffy::Size {
243 width: layout.width(),
244 height: layout.height(),
245 };
246 }
247 }
248 let layout = ts.layout_clamped(
252 &tm.content,
253 tm.size_px,
254 max_width,
255 tm.alignment,
256 tm.line_height,
257 tm.italic,
258 tm.font_family.as_deref(),
259 tm.weight,
260 tm.max_lines,
261 tm.ellipsis,
262 tm.underline,
263 tm.strikethrough,
264 tm.letter_spacing,
265 tm.word_spacing,
266 tm.overflow_wrap,
267 );
268 let m = llimphi_text::measurement(&layout);
269 llimphi_layout::taffy::Size { width: m.width, height: m.height }
270}
271
272pub(crate) fn node_rrect(
276 x0: f64,
277 y0: f64,
278 x1: f64,
279 y1: f64,
280 radius: f64,
281 corners: Option<RoundedRectRadii>,
282 inset: f64,
283) -> RoundedRect {
284 let radii = match corners {
285 Some(c) => RoundedRectRadii::new(
286 (c.top_left - inset).max(0.0),
287 (c.top_right - inset).max(0.0),
288 (c.bottom_right - inset).max(0.0),
289 (c.bottom_left - inset).max(0.0),
290 ),
291 None => {
292 let r = (radius - inset).max(0.0);
293 RoundedRectRadii::new(r, r, r, r)
294 }
295 };
296 RoundedRect::new(x0 + inset, y0 + inset, x1 - inset, y1 - inset, radii)
297}
298
299fn resolve_clip_radius(q: &[f32], cxl: f64, cyl: f64, w: f64, h: f64, is_x: bool) -> f64 {
307 let side = q[4] as i32;
308 if side == 0 {
309 let diag = (w * w + h * h).sqrt() / core::f64::consts::SQRT_2;
310 return q[0] as f64 + q[1] as f64 / 100.0 * w + q[2] as f64 / 100.0 * h
311 + q[3] as f64 / 100.0 * diag;
312 }
313 let (dx_near, dx_far) = (cxl.min(w - cxl), cxl.max(w - cxl));
314 let (dy_near, dy_far) = (cyl.min(h - cyl), cyl.max(h - cyl));
315 match side {
316 1 => dx_near.min(dy_near), 2 => dx_far.max(dy_far), 3 => {
319 if is_x {
320 dx_near
321 } else {
322 dy_near
323 }
324 } _ => {
326 if is_x {
327 dx_far
328 } else {
329 dy_far
330 }
331 } }
333}
334
335pub fn paint<Msg>(
336 scene: &mut vello::Scene,
337 mounted: &Mounted<Msg>,
338 computed: &ComputedLayout,
339 typesetter: &mut llimphi_text::Typesetter,
340 hover_idx: Option<usize>,
341 drop_hover_idx: Option<usize>,
342) {
343 paint_range(
344 scene,
345 mounted,
346 computed,
347 typesetter,
348 hover_idx,
349 drop_hover_idx,
350 0,
351 mounted.nodes.len(),
352 Affine::IDENTITY,
353 );
354}
355
356pub fn collect_backdrop_blurs<Msg>(
373 mounted: &Mounted<Msg>,
374 computed: &ComputedLayout,
375) -> Vec<BackdropBlur> {
376 let mut out = Vec::new();
377 let mut idx = 0;
378 while idx < mounted.nodes.len() {
379 let node = &mounted.nodes[idx];
380 if let Some(sigma) = node.backdrop_blur {
381 if let Some(r) = computed.get(node.id) {
382 out.push(BackdropBlur {
383 sigma,
384 rect: (r.x, r.y, r.w, r.h),
385 });
386 idx = node.subtree_end;
387 continue;
388 }
389 }
390 idx += 1;
391 }
392 out
393}
394
395#[derive(Debug, Clone, Copy)]
398pub struct BackdropBlur {
399 pub sigma: f32,
401 pub rect: (f32, f32, f32, f32),
403}
404
405#[derive(Debug, Clone)]
409pub struct FilterPass {
410 pub rect: (f32, f32, f32, f32),
412 pub op: FilterOp,
414}
415
416pub fn collect_filters<Msg>(
425 mounted: &Mounted<Msg>,
426 computed: &ComputedLayout,
427) -> Vec<FilterPass> {
428 let mut out = Vec::new();
429 let mut idx = 0;
430 while idx < mounted.nodes.len() {
431 let node = &mounted.nodes[idx];
432 if !node.filter.is_empty() {
433 if let Some(r) = computed.get(node.id) {
434 let rect = (r.x, r.y, r.w, r.h);
435 for op in &node.filter {
436 if matches!(op, FilterOp::DropShadow(_)) {
439 continue;
440 }
441 out.push(FilterPass { rect, op: op.clone() });
442 }
443 idx = node.subtree_end;
444 continue;
445 }
446 }
447 idx += 1;
448 }
449 out
450}
451
452pub(crate) fn resolve_node_transform(
460 transform: Option<Affine>,
461 transform_rel: Option<(f64, f64)>,
462 transform_origin: Option<crate::TransformPivot>,
463 r: llimphi_layout::Rect,
464) -> Option<Affine> {
465 if transform.is_none() && transform_rel.is_none() {
466 return None;
467 }
468 let mut local = transform.unwrap_or(Affine::IDENTITY);
469 if let Some((fx, fy)) = transform_rel {
470 local = Affine::translate((fx * r.w as f64, fy * r.h as f64)) * local;
471 }
472 let pivot = transform_origin.unwrap_or_default();
476 let ox = r.x as f64 + pivot.px.0 + pivot.frac.0 * r.w as f64;
477 let oy = r.y as f64 + pivot.px.1 + pivot.frac.1 * r.h as f64;
478 Some(Affine::translate((ox, oy)) * local * Affine::translate((-ox, -oy)))
479}
480
481fn paint_mask_close(
506 scene: &mut vello::Scene,
507 img: &Image,
508 extra: &[(Image, MaskCompose)],
509 rect: KurboRect,
510 xf: Affine,
511 placement: Option<MaskPlacement>,
512) {
513 let shrink = |r: KurboRect, inset: Option<[f32; 4]>| -> KurboRect {
517 match inset {
518 None => r,
519 Some([t, ri, b, le]) => KurboRect::new(
520 r.x0 + le as f64,
521 r.y0 + t as f64,
522 (r.x1 - ri as f64).max(r.x0 + le as f64),
523 (r.y1 - b as f64).max(r.y0 + t as f64),
524 ),
525 }
526 };
527 let clip_rect = shrink(rect, placement.and_then(|p| p.clip_inset));
528 let origin_rect = shrink(rect, placement.and_then(|p| p.origin_inset));
529 let mode = placement.map(|p| p.mode).unwrap_or(MaskMode::Luminance);
535 match mode {
536 MaskMode::Luminance => scene.push_luminance_mask_layer(Fill::NonZero, 1.0, xf, &clip_rect),
537 MaskMode::Alpha => scene.push_layer(
538 Fill::NonZero,
539 vello::peniko::BlendMode::new(Mix::Normal, vello::peniko::Compose::DestIn),
540 1.0,
541 xf,
542 &clip_rect,
543 ),
544 }
545 draw_mask_layer(scene, img, origin_rect, xf, placement);
554 for (eimg, op) in extra {
555 match op {
556 MaskCompose::Add => draw_mask_layer(scene, eimg, origin_rect, xf, placement),
557 _ => {
558 let compose = match op {
559 MaskCompose::Subtract => vello::peniko::Compose::SrcOut,
560 MaskCompose::Intersect => vello::peniko::Compose::SrcIn,
561 MaskCompose::Exclude => vello::peniko::Compose::Xor,
562 MaskCompose::Add => unreachable!(),
563 };
564 scene.push_layer(
565 Fill::NonZero,
566 vello::peniko::BlendMode::new(Mix::Normal, compose),
567 1.0,
568 xf,
569 &clip_rect,
570 );
571 draw_mask_layer(scene, eimg, origin_rect, xf, placement);
572 scene.pop_layer();
573 }
574 }
575 }
576 scene.pop_layer();
577}
578
579fn draw_mask_layer(
585 scene: &mut vello::Scene,
586 img: &Image,
587 origin_rect: KurboRect,
588 xf: Affine,
589 placement: Option<MaskPlacement>,
590) {
591 let iw = img.image.width.max(1) as f64;
592 let ih = img.image.height.max(1) as f64;
593 match placement {
594 None => {
597 let fit = Affine::translate((origin_rect.x0, origin_rect.y0))
598 * Affine::scale_non_uniform(origin_rect.width() / iw, origin_rect.height() / ih);
599 scene.draw_image(img, xf * fit);
600 }
601 Some(p) => {
604 let rw = origin_rect.width();
605 let rh = origin_rect.height();
606 let resolve = |l: MaskLen, basis: f64| -> Option<f64> {
608 match l {
609 MaskLen::Px(n) => Some(n as f64),
610 MaskLen::Pct(q) => Some(basis * q as f64 / 100.0),
611 MaskLen::Auto => None,
612 }
613 };
614 let (tw, th) = match p.size {
615 MaskSize::Auto => (iw, ih),
616 MaskSize::Cover => {
617 let s = (rw / iw).max(rh / ih);
618 (iw * s, ih * s)
619 }
620 MaskSize::Contain => {
621 let s = (rw / iw).min(rh / ih);
622 (iw * s, ih * s)
623 }
624 MaskSize::Explicit { x, y } => match (resolve(x, rw), resolve(y, rh)) {
625 (Some(w), Some(h)) => (w, h),
626 (Some(w), None) => (w, w * ih / iw),
627 (None, Some(h)) => (h * iw / ih, h),
628 (None, None) => (iw, ih),
629 },
630 };
631 if tw > 0.5 && th > 0.5 {
632 let pos_off = |l: MaskLen, basis: f64, tile: f64| -> f64 {
634 match l {
635 MaskLen::Px(n) => n as f64,
636 MaskLen::Pct(q) => (basis - tile) * q as f64 / 100.0,
637 MaskLen::Auto => 0.0,
638 }
639 };
640 let ox = pos_off(p.pos_x, rw, tw);
641 let oy = pos_off(p.pos_y, rh, th);
642 let axis = |off: f64, tile: f64, span: f64, rep: bool| -> Vec<f64> {
645 if !rep {
646 return vec![off];
647 }
648 let mut start = off;
649 while start > 0.0 {
650 start -= tile;
651 }
652 let mut v = Vec::new();
653 let mut q = start;
654 while q < span && v.len() < 4096 {
655 v.push(q);
656 q += tile;
657 }
658 v
659 };
660 let xs = axis(ox, tw, rw, p.repeat_x);
661 let ys = axis(oy, th, rh, p.repeat_y);
662 let scale = Affine::scale_non_uniform(tw / iw, th / ih);
663 for &x in &xs {
664 for &y in &ys {
665 let tf =
666 Affine::translate((origin_rect.x0 + x, origin_rect.y0 + y)) * scale;
667 scene.draw_image(img, xf * tf);
668 }
669 }
670 }
671 }
672 }
673}
674
675#[allow(clippy::too_many_arguments)]
676pub fn paint_range<Msg>(
677 scene: &mut vello::Scene,
678 mounted: &Mounted<Msg>,
679 computed: &ComputedLayout,
680 typesetter: &mut llimphi_text::Typesetter,
681 hover_idx: Option<usize>,
682 drop_hover_idx: Option<usize>,
683 start: usize,
684 end: usize,
685 base_xf: Affine,
686) {
687 type MaskClose = (
699 Image,
700 Vec<(Image, MaskCompose)>,
701 KurboRect,
702 Affine,
703 Option<MaskPlacement>,
704 );
705 let mut layer_stack: Vec<(usize, Option<MaskClose>)> = Vec::new();
706 let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
713 let mut cur_xf = base_xf;
714 for idx in start..end {
715 let node = &mounted.nodes[idx];
716 while let Some(&(end, _)) = layer_stack.last() {
719 if idx >= end {
720 let (_, mask) = layer_stack.pop().unwrap();
721 if let Some((img, extra, rect, xf, placement)) = &mask {
722 paint_mask_close(scene, img, extra, *rect, *xf, *placement);
723 }
724 scene.pop_layer();
725 } else {
726 break;
727 }
728 }
729 while let Some(&(end, prev)) = xf_stack.last() {
731 if idx >= end {
732 cur_xf = prev;
733 xf_stack.pop();
734 } else {
735 break;
736 }
737 }
738 let Some(r) = computed.get(node.id) else {
739 continue;
740 };
741 if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
746 xf_stack.push((node.subtree_end, cur_xf));
747 cur_xf *= centered;
748 }
749 if let Some(bm) = node.blend {
757 let rect = KurboRect::new(
758 r.x as f64,
759 r.y as f64,
760 (r.x + r.w) as f64,
761 (r.y + r.h) as f64,
762 );
763 scene.push_layer(Fill::NonZero, bm, 1.0, cur_xf, &rect);
764 layer_stack.push((node.subtree_end, None));
765 }
766 if let Some(a) = node.alpha {
775 let rect = KurboRect::new(
776 r.x as f64,
777 r.y as f64,
778 (r.x + r.w) as f64,
779 (r.y + r.h) as f64,
780 );
781 scene.push_layer(Fill::NonZero, Mix::Normal, a, cur_xf, &rect);
782 layer_stack.push((node.subtree_end, None));
783 }
784 if let Some(mask_img) = node.mask_image.as_ref() {
791 let rect = KurboRect::new(
792 r.x as f64,
793 r.y as f64,
794 (r.x + r.w) as f64,
795 (r.y + r.h) as f64,
796 );
797 scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &rect);
798 layer_stack.push((
799 node.subtree_end,
800 Some((
801 mask_img.clone(),
802 node.mask_extra.clone(),
803 rect,
804 cur_xf,
805 node.mask_placement,
806 )),
807 ));
808 }
809 if let Some(sh) = node.shadow.as_ref() {
813 if sh.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
814 let rect = KurboRect::new(
815 (r.x as f64) + sh.dx - sh.spread,
816 (r.y as f64) + sh.dy - sh.spread,
817 (r.x + r.w) as f64 + sh.dx + sh.spread,
818 (r.y + r.h) as f64 + sh.dy + sh.spread,
819 );
820 let radius = (node.radius + sh.spread).max(0.0);
821 scene.draw_blurred_rounded_rect(cur_xf, rect, sh.color, radius, sh.blur);
822 }
823 }
824 for op in node.filter.iter().rev() {
829 if let FilterOp::DropShadow(sh) = op {
830 if sh.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
831 let rect = KurboRect::new(
832 (r.x as f64) + sh.dx - sh.spread,
833 (r.y as f64) + sh.dy - sh.spread,
834 (r.x + r.w) as f64 + sh.dx + sh.spread,
835 (r.y + r.h) as f64 + sh.dy + sh.spread,
836 );
837 let radius = (node.radius + sh.spread).max(0.0);
838 scene.draw_blurred_rounded_rect(cur_xf, rect, sh.color, radius, sh.blur);
839 }
840 }
841 }
842 let hover_color = if Some(idx) == drop_hover_idx {
846 node.drop_hover_fill.or(node.hover_fill).or(node.fill)
847 } else if Some(idx) == hover_idx {
848 node.hover_fill.or(node.fill)
849 } else {
850 None
851 };
852 let rr = node_rrect(
853 r.x as f64,
854 r.y as f64,
855 (r.x + r.w) as f64,
856 (r.y + r.h) as f64,
857 node.radius,
858 node.corner_radii,
859 0.0,
860 );
861 if let Some(color) = hover_color {
862 scene.fill(Fill::NonZero, cur_xf, color, None, &rr);
864 } else if let Some(grad) = node.fill_gradient.as_ref() {
865 let brush_xf = cur_xf
868 * Affine::translate((r.x as f64, r.y as f64))
869 * Affine::scale_non_uniform(r.w as f64, r.h as f64);
870 scene.fill(Fill::NonZero, cur_xf, grad, Some(brush_xf), &rr);
871 } else if let Some(color) = node.fill {
872 scene.fill(Fill::NonZero, cur_xf, color, None, &rr);
873 }
874 if let Some(b) = node.border.as_ref() {
876 if b.width > 0.0 && b.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
877 let inset = b.width * 0.5;
878 let brr = node_rrect(
879 r.x as f64,
880 r.y as f64,
881 (r.x + r.w) as f64,
882 (r.y + r.h) as f64,
883 node.radius,
884 node.corner_radii,
885 inset,
886 );
887 scene.stroke(&Stroke::new(b.width), cur_xf, b.color, None, &brr);
888 }
889 }
890 if let Some(image) = node.image.as_ref() {
891 if image.image.width > 0 && image.image.height > 0 && r.w > 0.0 && r.h > 0.0 {
896 let sx = r.w as f64 / image.image.width as f64;
897 let sy = r.h as f64 / image.image.height as f64;
898 let fit = node.image_fit.unwrap_or(ImageFit::Contain);
899 let transform = match fit {
900 ImageFit::Contain => {
901 let s = sx.min(sy);
902 let disp_w = image.image.width as f64 * s;
903 let disp_h = image.image.height as f64 * s;
904 let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
905 let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
906 Affine::translate((tx, ty)) * Affine::scale(s)
907 }
908 ImageFit::Cover => {
909 let s = sx.max(sy);
910 let disp_w = image.image.width as f64 * s;
911 let disp_h = image.image.height as f64 * s;
912 let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
913 let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
914 Affine::translate((tx, ty)) * Affine::scale(s)
915 }
916 ImageFit::Fill => {
917 Affine::translate((r.x as f64, r.y as f64))
918 * Affine::scale_non_uniform(sx, sy)
919 }
920 ImageFit::None => {
921 let disp_w = image.image.width as f64;
922 let disp_h = image.image.height as f64;
923 let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
924 let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
925 Affine::translate((tx, ty))
926 }
927 };
928 let clip_rr = node_rrect(
929 r.x as f64,
930 r.y as f64,
931 (r.x + r.w) as f64,
932 (r.y + r.h) as f64,
933 node.radius,
934 node.corner_radii,
935 0.0,
936 );
937 scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &clip_rr);
938 scene.draw_image(image, cur_xf * transform);
939 scene.pop_layer();
940 }
941 }
942 if let Some(painter) = node.painter.as_ref() {
943 (painter)(
944 scene,
945 typesetter,
946 PaintRect {
947 x: r.x,
948 y: r.y,
949 w: r.w,
950 h: r.h,
951 },
952 );
953 }
954 if let Some(text) = node.text.as_ref() {
955 let has_spans = text
956 .spans
957 .as_ref()
958 .map(|s| !s.is_empty())
959 .unwrap_or(false);
960 if has_spans {
961 let spans = text.spans.as_ref().unwrap();
967 let layout = typesetter.layout_spans(
968 &text.content,
969 text.size_px,
970 text.color,
971 text.weight,
972 text.line_height,
973 text.italic,
974 text.font_family.as_deref(),
975 text.underline,
976 text.strikethrough,
977 spans,
978 Some(r.w),
979 text.alignment,
980 );
981 let origin =
982 if matches!(text.alignment, llimphi_text::Alignment::Center) {
983 let lh = layout.height() as f64;
984 (
985 r.x as f64,
986 r.y as f64 + ((r.h as f64 - lh) * 0.5).max(0.0),
987 )
988 } else {
989 (r.x as f64, r.y as f64)
990 };
991 llimphi_text::draw_layout_runs_xf(
992 scene,
993 &layout,
994 cur_xf * Affine::translate(origin),
995 );
996 } else if let Some(runs) = text.runs.as_ref() {
997 let layout = typesetter.layout_runs(
1001 &text.content,
1002 text.size_px,
1003 text.color,
1004 runs,
1005 text.alignment,
1006 text.line_height,
1007 text.weight,
1008 text.underline,
1009 text.strikethrough,
1010 );
1011 llimphi_text::draw_layout_runs_xf(
1016 scene,
1017 &layout,
1018 cur_xf * Affine::translate((r.x as f64, r.y as f64)),
1019 );
1020 } else {
1021 let paint_max_width = if text.no_wrap { None } else { Some(r.w) };
1030 let layout = typesetter.layout_clamped(
1031 &text.content,
1032 text.size_px,
1033 paint_max_width,
1034 text.alignment,
1035 text.line_height,
1036 text.italic,
1037 text.font_family.as_deref(),
1038 text.weight,
1039 text.max_lines,
1040 text.ellipsis,
1041 text.underline,
1042 text.strikethrough,
1043 text.letter_spacing,
1044 text.word_spacing,
1045 text.overflow_wrap,
1046 );
1047 let origin =
1048 if matches!(text.alignment, llimphi_text::Alignment::Center) {
1049 let m = llimphi_text::measurement(&layout);
1050 (
1051 r.x as f64,
1052 r.y as f64 + ((r.h - m.height) as f64 * 0.5).max(0.0),
1053 )
1054 } else {
1055 (r.x as f64, r.y as f64)
1056 };
1057 llimphi_text::draw_layout_xf(
1058 scene,
1059 &layout,
1060 text.color,
1061 cur_xf * Affine::translate(origin),
1062 );
1063 }
1064 }
1065 if node.clip {
1066 let mut pushed = true;
1071 let [rit, rir, rib, ril] = node.clip_ref_inset.unwrap_or([0.0; 4]);
1075 let (bx, by) = ((r.x + ril) as f64, (r.y + rit) as f64);
1076 let (bw, bh) = ((r.w - ril - rir).max(0.0) as f64, (r.h - rit - rib).max(0.0) as f64);
1077 if let Some((evenodd, d)) = &node.clip_path_svg {
1078 match vello::kurbo::BezPath::from_svg(d) {
1082 Ok(mut path) => {
1083 path.apply_affine(Affine::translate((bx, by)));
1084 let fill = if *evenodd { Fill::EvenOdd } else { Fill::NonZero };
1085 scene.push_layer(fill, BlendMode::default(), 1.0, cur_xf, &path);
1086 }
1087 Err(_) => pushed = false,
1088 }
1089 } else if let Some((evenodd, pts)) = &node.clip_polygon {
1090 let mut path = vello::kurbo::BezPath::new();
1094 for (i, p) in pts.iter().enumerate() {
1095 let px = bx + p[0] as f64 + p[1] as f64 / 100.0 * bw;
1096 let py = by + p[2] as f64 + p[3] as f64 / 100.0 * bh;
1097 if i == 0 {
1098 path.move_to((px, py));
1099 } else {
1100 path.line_to((px, py));
1101 }
1102 }
1103 path.close_path();
1104 let fill = if *evenodd { Fill::EvenOdd } else { Fill::NonZero };
1105 scene.push_layer(fill, BlendMode::default(), 1.0, cur_xf, &path);
1106 } else if let Some(s) = node.clip_ellipse {
1107 let cxl = s[0] as f64 + s[1] as f64 / 100.0 * bw;
1112 let cyl = s[2] as f64 + s[3] as f64 / 100.0 * bh;
1113 let cx = bx + cxl;
1114 let cy = by + cyl;
1115 let rx = resolve_clip_radius(&s[4..9], cxl, cyl, bw, bh, true);
1116 let ry = resolve_clip_radius(&s[9..14], cxl, cyl, bw, bh, false);
1117 let ellipse = Ellipse::new((cx, cy), (rx, ry), 0.0);
1118 scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &ellipse);
1119 } else {
1120 let [ct, cr, cb, cl] = node.clip_inset.unwrap_or([0.0; 4]);
1124 let clip_rect = KurboRect::new(
1125 bx + cl as f64,
1126 by + ct as f64,
1127 bx + bw - cr as f64,
1128 by + bh - cb as f64,
1129 );
1130 scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &clip_rect);
1131 }
1132 if pushed {
1133 layer_stack.push((node.subtree_end, None));
1134 }
1135 }
1136 }
1137 while let Some((_, mask)) = layer_stack.pop() {
1140 if let Some((img, extra, rect, xf, placement)) = &mask {
1141 paint_mask_close(scene, img, extra, *rect, *xf, *placement);
1142 }
1143 scene.pop_layer();
1144 }
1145}
1146
1147pub fn has_gpu_painter<Msg>(mounted: &Mounted<Msg>) -> bool {
1159 mounted.nodes.iter().any(|n| n.gpu_painter.is_some())
1160}
1161
1162pub fn paint_gpu<Msg>(
1163 mounted: &Mounted<Msg>,
1164 computed: &ComputedLayout,
1165 device: &wgpu::Device,
1166 queue: &wgpu::Queue,
1167 encoder: &mut wgpu::CommandEncoder,
1168 view: &wgpu::TextureView,
1169 viewport: (u32, u32),
1170) -> bool {
1171 let mut any = false;
1172 for node in &mounted.nodes {
1173 let Some(painter) = node.gpu_painter.as_ref() else {
1174 continue;
1175 };
1176 let Some(r) = computed.get(node.id) else {
1177 continue;
1178 };
1179 (painter)(
1180 device,
1181 queue,
1182 encoder,
1183 view,
1184 PaintRect {
1185 x: r.x,
1186 y: r.y,
1187 w: r.w,
1188 h: r.h,
1189 },
1190 viewport,
1191 );
1192 any = true;
1193 }
1194 any
1195}
1196
1197pub fn has_over_painter<Msg>(mounted: &Mounted<Msg>) -> bool {
1202 mounted.nodes.iter().any(|n| n.over_painter.is_some())
1203}
1204
1205pub fn paint_over<Msg>(
1215 scene: &mut vello::Scene,
1216 mounted: &Mounted<Msg>,
1217 computed: &ComputedLayout,
1218 typesetter: &mut llimphi_text::Typesetter,
1219) -> bool {
1220 let mut any = false;
1221 for node in &mounted.nodes {
1222 let Some(painter) = node.over_painter.as_ref() else {
1223 continue;
1224 };
1225 let Some(r) = computed.get(node.id) else {
1226 continue;
1227 };
1228 (painter)(
1229 scene,
1230 typesetter,
1231 PaintRect {
1232 x: r.x,
1233 y: r.y,
1234 w: r.w,
1235 h: r.h,
1236 },
1237 );
1238 any = true;
1239 }
1240 any
1241}
1242
1243pub fn hit_test_pred<Msg, F>(
1257 mounted: &Mounted<Msg>,
1258 computed: &ComputedLayout,
1259 x: f32,
1260 y: f32,
1261 pred: F,
1262) -> Option<usize>
1263where
1264 F: Fn(&MountedNode<Msg>) -> bool,
1265{
1266 let mut hit: Option<usize> = None;
1267 let mut clip_stack: Vec<usize> = Vec::new();
1268 let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
1273 let mut cur_xf = Affine::IDENTITY;
1274 let mut idx = 0;
1275 while idx < mounted.nodes.len() {
1276 while let Some(&end) = clip_stack.last() {
1277 if idx >= end {
1278 clip_stack.pop();
1279 } else {
1280 break;
1281 }
1282 }
1283 while let Some(&(end, prev)) = xf_stack.last() {
1284 if idx >= end {
1285 cur_xf = prev;
1286 xf_stack.pop();
1287 } else {
1288 break;
1289 }
1290 }
1291 let node = &mounted.nodes[idx];
1292 let Some(r) = computed.get(node.id) else {
1293 idx += 1;
1294 continue;
1295 };
1296 if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
1300 xf_stack.push((node.subtree_end, cur_xf));
1301 cur_xf *= centered;
1302 }
1303 let (lx, ly) = if xf_stack.is_empty() {
1307 (x as f64, y as f64)
1308 } else if cur_xf.determinant().abs() < 1e-9 {
1309 idx = node.subtree_end;
1310 continue;
1311 } else {
1312 let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
1313 (p.x, p.y)
1314 };
1315 let inside = lx >= r.x as f64
1316 && lx < (r.x + r.w) as f64
1317 && ly >= r.y as f64
1318 && ly < (r.y + r.h) as f64;
1319 if node.clip {
1320 if !inside {
1321 idx = node.subtree_end;
1322 continue;
1323 }
1324 clip_stack.push(node.subtree_end);
1325 }
1326 if inside && pred(node) {
1327 hit = Some(idx);
1328 }
1329 idx += 1;
1330 }
1331 hit
1332}
1333
1334pub fn hit_test_click<Msg>(
1336 mounted: &Mounted<Msg>,
1337 computed: &ComputedLayout,
1338 x: f32,
1339 y: f32,
1340) -> Option<usize> {
1341 hit_test_pred(mounted, computed, x, y, |n| {
1342 n.on_click.is_some()
1343 || n.on_click_at.is_some()
1344 || n.drag.is_some()
1345 || n.drag_at.is_some()
1346 || n.drag_velocity.is_some()
1347 })
1348}
1349
1350pub fn hit_test_right_click<Msg>(
1355 mounted: &Mounted<Msg>,
1356 computed: &ComputedLayout,
1357 x: f32,
1358 y: f32,
1359) -> Option<usize> {
1360 hit_test_pred(mounted, computed, x, y, |n| {
1361 n.on_right_click.is_some() || n.on_right_click_at.is_some()
1362 })
1363}
1364
1365pub fn hit_test_middle_click<Msg>(
1368 mounted: &Mounted<Msg>,
1369 computed: &ComputedLayout,
1370 x: f32,
1371 y: f32,
1372) -> Option<usize> {
1373 hit_test_pred(mounted, computed, x, y, |n| n.on_middle_click.is_some())
1374}
1375
1376pub fn hit_test_hover<Msg>(
1378 mounted: &Mounted<Msg>,
1379 computed: &ComputedLayout,
1380 x: f32,
1381 y: f32,
1382) -> Option<usize> {
1383 hit_test_pred(mounted, computed, x, y, |n| n.hover_fill.is_some())
1384}
1385
1386pub fn hit_test_pointer_move<Msg>(
1390 mounted: &Mounted<Msg>,
1391 computed: &ComputedLayout,
1392 x: f32,
1393 y: f32,
1394) -> Option<usize> {
1395 hit_test_pred(mounted, computed, x, y, |n| n.on_pointer_move_at.is_some())
1396}
1397
1398pub fn hit_test_cursor<Msg>(
1405 mounted: &Mounted<Msg>,
1406 computed: &ComputedLayout,
1407 x: f32,
1408 y: f32,
1409) -> Option<Cursor> {
1410 hit_test_pred(mounted, computed, x, y, |n| n.cursor.is_some())
1411 .and_then(|i| mounted.nodes[i].cursor)
1412}
1413
1414pub fn hit_test_drop<Msg>(
1418 mounted: &Mounted<Msg>,
1419 computed: &ComputedLayout,
1420 x: f32,
1421 y: f32,
1422) -> Option<usize> {
1423 hit_test_pred(mounted, computed, x, y, |n| n.on_drop.is_some())
1424}
1425
1426pub fn hit_test_scroll<Msg>(
1431 mounted: &Mounted<Msg>,
1432 computed: &ComputedLayout,
1433 x: f32,
1434 y: f32,
1435) -> Option<usize> {
1436 hit_test_pred(mounted, computed, x, y, |n| n.on_scroll.is_some())
1437}
1438
1439pub fn hit_test_scroll_chain<Msg>(
1449 mounted: &Mounted<Msg>,
1450 computed: &ComputedLayout,
1451 x: f32,
1452 y: f32,
1453) -> Vec<usize> {
1454 let mut chain: Vec<usize> = Vec::new();
1455 let mut clip_stack: Vec<usize> = Vec::new();
1456 let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
1457 let mut cur_xf = Affine::IDENTITY;
1458 let mut idx = 0;
1459 while idx < mounted.nodes.len() {
1460 while let Some(&end) = clip_stack.last() {
1461 if idx >= end {
1462 clip_stack.pop();
1463 } else {
1464 break;
1465 }
1466 }
1467 while let Some(&(end, prev)) = xf_stack.last() {
1468 if idx >= end {
1469 cur_xf = prev;
1470 xf_stack.pop();
1471 } else {
1472 break;
1473 }
1474 }
1475 let node = &mounted.nodes[idx];
1476 let Some(r) = computed.get(node.id) else {
1477 idx += 1;
1478 continue;
1479 };
1480 if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
1481 xf_stack.push((node.subtree_end, cur_xf));
1482 cur_xf *= centered;
1483 }
1484 let (lx, ly) = if xf_stack.is_empty() {
1485 (x as f64, y as f64)
1486 } else if cur_xf.determinant().abs() < 1e-9 {
1487 idx = node.subtree_end;
1488 continue;
1489 } else {
1490 let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
1491 (p.x, p.y)
1492 };
1493 let inside = lx >= r.x as f64
1494 && lx < (r.x + r.w) as f64
1495 && ly >= r.y as f64
1496 && ly < (r.y + r.h) as f64;
1497 if node.clip {
1498 if !inside {
1499 idx = node.subtree_end;
1500 continue;
1501 }
1502 clip_stack.push(node.subtree_end);
1503 }
1504 if inside && node.on_scroll.is_some() {
1505 chain.push(idx);
1506 }
1507 idx += 1;
1508 }
1509 chain.reverse();
1512 chain
1513}
1514
1515pub fn hit_test_scale<Msg>(
1522 mounted: &Mounted<Msg>,
1523 computed: &ComputedLayout,
1524 x: f32,
1525 y: f32,
1526) -> Option<usize> {
1527 hit_test_pred(mounted, computed, x, y, |n| n.on_scale.is_some())
1528}
1529
1530pub fn hit_test_rotate<Msg>(
1535 mounted: &Mounted<Msg>,
1536 computed: &ComputedLayout,
1537 x: f32,
1538 y: f32,
1539) -> Option<usize> {
1540 hit_test_pred(mounted, computed, x, y, |n| n.on_rotate.is_some())
1541}
1542
1543pub fn hit_test_double_tap<Msg>(
1547 mounted: &Mounted<Msg>,
1548 computed: &ComputedLayout,
1549 x: f32,
1550 y: f32,
1551) -> Option<usize> {
1552 hit_test_pred(mounted, computed, x, y, |n| {
1553 n.on_double_tap.is_some() || n.on_double_tap_at.is_some()
1554 })
1555}
1556
1557pub fn hit_test_long_press<Msg>(
1561 mounted: &Mounted<Msg>,
1562 computed: &ComputedLayout,
1563 x: f32,
1564 y: f32,
1565) -> Option<usize> {
1566 hit_test_pred(mounted, computed, x, y, |n| {
1567 n.on_long_press.is_some() || n.on_long_press_at.is_some()
1568 })
1569}
1570
1571pub fn hit_test_ripple<Msg>(
1575 mounted: &Mounted<Msg>,
1576 computed: &ComputedLayout,
1577 x: f32,
1578 y: f32,
1579) -> Option<usize> {
1580 hit_test_pred(mounted, computed, x, y, |n| n.ripple.is_some())
1581}
1582
1583pub fn hit_test_focusable<Msg>(
1586 mounted: &Mounted<Msg>,
1587 computed: &ComputedLayout,
1588 x: f32,
1589 y: f32,
1590) -> Option<u64> {
1591 hit_test_pred(mounted, computed, x, y, |n| n.focusable.is_some())
1592 .and_then(|i| mounted.nodes[i].focusable)
1593}
1594
1595pub fn hit_test_selectable<Msg>(
1601 mounted: &Mounted<Msg>,
1602 computed: &ComputedLayout,
1603 x: f32,
1604 y: f32,
1605) -> Option<usize> {
1606 hit_test_pred(mounted, computed, x, y, |n| n.text_select_key.is_some())
1607}
1608
1609pub fn focus_order<Msg>(mounted: &Mounted<Msg>, computed: &ComputedLayout) -> Vec<u64> {
1613 mounted
1614 .nodes
1615 .iter()
1616 .filter_map(|n| {
1617 n.focusable
1618 .filter(|_| computed.get(n.id).is_some())
1619 })
1620 .collect()
1621}
1622
1623pub fn next_focus(order: &[u64], current: Option<u64>, reverse: bool) -> Option<u64> {
1628 if order.is_empty() {
1629 return None;
1630 }
1631 let n = order.len();
1632 let pos = current.and_then(|c| order.iter().position(|&id| id == c));
1633 let next_idx = match pos {
1634 Some(i) => {
1635 if reverse {
1636 (i + n - 1) % n
1637 } else {
1638 (i + 1) % n
1639 }
1640 }
1641 None => {
1642 if reverse {
1643 n - 1
1644 } else {
1645 0
1646 }
1647 }
1648 };
1649 Some(order[next_idx])
1650}
1651
1652#[cfg(test)]
1653mod tests {
1654 use crate::{hit_test_click, mount, View};
1655 use llimphi_layout::taffy::prelude::*;
1656 use llimphi_layout::{LayoutTree, Style};
1657 use vello::kurbo::Affine;
1658
1659 #[test]
1660 fn transform_origin_fija_el_pivote() {
1661 use super::resolve_node_transform;
1665 use crate::TransformPivot;
1666 use vello::kurbo::Point;
1667 let r = llimphi_layout::Rect { x: 0.0, y: 0.0, w: 100.0, h: 100.0 };
1668 let rot = Affine::rotate(std::f64::consts::FRAC_PI_2);
1669
1670 let tl = TransformPivot { px: (0.0, 0.0), frac: (0.0, 0.0) };
1671 let xf_tl = resolve_node_transform(Some(rot), None, Some(tl), r).unwrap();
1672 let p = xf_tl * Point::new(0.0, 0.0);
1673 assert!(p.x.abs() < 1e-6 && p.y.abs() < 1e-6, "pivote top-left fijo, fue {p:?}");
1674
1675 let xf_c = resolve_node_transform(Some(rot), None, None, r).unwrap();
1677 let c = xf_c * Point::new(50.0, 50.0);
1678 assert!(
1679 (c.x - 50.0).abs() < 1e-6 && (c.y - 50.0).abs() < 1e-6,
1680 "centro fijo con pivote default, fue {c:?}"
1681 );
1682 let c2 = xf_tl * Point::new(50.0, 50.0);
1684 assert!((c2.x - 50.0).abs() > 1.0 || (c2.y - 50.0).abs() > 1.0, "top-left mueve el centro");
1685 }
1686
1687 #[test]
1688 fn resolve_clip_radius_lados_y_porcentajes() {
1689 use super::resolve_clip_radius;
1690 let (w, h, cxl, cyl): (f64, f64, f64, f64) = (200.0, 100.0, 100.0, 50.0);
1692 let diag = (w * w + h * h).sqrt() / core::f64::consts::SQRT_2;
1694 let r = resolve_clip_radius(&[10.0, 0.0, 0.0, 50.0, 0.0], cxl, cyl, w, h, true);
1695 assert!((r - (10.0 + 0.5 * diag)).abs() < 1e-6);
1696 assert_eq!(
1698 resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 1.0], cxl, cyl, w, h, true),
1699 50.0
1700 );
1701 assert_eq!(
1703 resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 2.0], cxl, cyl, w, h, true),
1704 100.0
1705 );
1706 assert_eq!(
1708 resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 3.0], cxl, cyl, w, h, true),
1709 100.0
1710 );
1711 assert_eq!(
1713 resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 3.0], cxl, cyl, w, h, false),
1714 50.0
1715 );
1716 assert_eq!(
1718 resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 1.0], 30.0, 20.0, w, h, true),
1719 20.0
1720 );
1721 }
1722
1723 fn fixture(
1726 transform: Option<Affine>,
1727 ) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
1728 let mut child = View::<()>::new(Style {
1729 size: Size {
1730 width: length(100.0),
1731 height: length(100.0),
1732 },
1733 ..Default::default()
1734 })
1735 .on_click(());
1736 if let Some(xf) = transform {
1737 child = child.transform(xf);
1738 }
1739 let root = View::<()>::new(Style {
1740 align_items: Some(AlignItems::FlexStart),
1741 justify_content: Some(JustifyContent::FlexStart),
1742 ..Default::default()
1743 })
1744 .children(vec![child]);
1745 let mut layout = LayoutTree::new();
1746 let mounted = mount(&mut layout, root);
1747 let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
1748 (mounted, computed)
1749 }
1750
1751 #[test]
1752 fn sin_transform_el_hit_cae_en_el_rect() {
1753 let (m, c) = fixture(None);
1754 assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), Some(1)); assert_eq!(hit_test_click(&m, &c, 250.0, 50.0), None); }
1757
1758 #[test]
1759 fn traslacion_mueve_el_area_clickeable() {
1760 let (m, c) = fixture(Some(Affine::translate((200.0, 0.0))));
1762 assert_eq!(hit_test_click(&m, &c, 250.0, 50.0), Some(1)); assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), None); }
1765
1766 #[test]
1767 fn rotacion_180_grados_alrededor_del_centro() {
1768 let (m, c) = fixture(Some(Affine::rotate(std::f64::consts::PI)));
1771 assert_eq!(hit_test_click(&m, &c, 10.0, 10.0), Some(1));
1772 assert_eq!(hit_test_click(&m, &c, 90.0, 90.0), Some(1));
1773 assert_eq!(hit_test_click(&m, &c, 150.0, 150.0), None);
1774 }
1775
1776 #[test]
1777 fn escala_cero_es_inalcanzable() {
1778 let (m, c) = fixture(Some(Affine::scale(0.0)));
1779 assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), None);
1780 }
1781
1782 fn fixture_rel(
1785 rel: (f64, f64),
1786 ) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
1787 let child = View::<()>::new(Style {
1788 size: Size { width: length(100.0), height: length(100.0) },
1789 ..Default::default()
1790 })
1791 .on_click(())
1792 .transform_rel(rel);
1793 let root = View::<()>::new(Style {
1794 align_items: Some(AlignItems::FlexStart),
1795 justify_content: Some(JustifyContent::FlexStart),
1796 ..Default::default()
1797 })
1798 .children(vec![child]);
1799 let mut layout = LayoutTree::new();
1800 let mounted = mount(&mut layout, root);
1801 let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
1802 (mounted, computed)
1803 }
1804
1805 fn dominium_like(gpu: bool) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
1811 let mut canvas = View::<()>::new(Style {
1812 size: Size { width: percent(1.0), height: percent(1.0) },
1813 ..Default::default()
1814 });
1815 canvas = if gpu {
1816 canvas
1817 .gpu_paint_with(|_d, _q, _e, _t, _r, _vp| {})
1818 .paint_over(|_s, _ts, _r| {})
1819 } else {
1820 canvas.paint_with(|_s, _ts, _r| {})
1821 };
1822 let wrapper = View::<()>::new(Style {
1823 size: Size { width: percent(1.0), height: percent(1.0) },
1824 ..Default::default()
1825 })
1826 .clip(true)
1827 .on_click_at(|_lx, _ly, _rw, _rh| Some(()))
1828 .draggable_at(|_phase, _dx, _dy, _x0, _y0| Some(()))
1829 .children(vec![canvas]);
1830 let mut layout = LayoutTree::new();
1831 let mounted = mount(&mut layout, wrapper);
1832 let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
1833 (mounted, computed)
1834 }
1835
1836 #[test]
1837 fn canvas_gpu_only_es_clickeable_igual_que_legacy() {
1838 let (m_leg, c_leg) = dominium_like(false);
1843 assert_eq!(hit_test_click(&m_leg, &c_leg, 200.0, 200.0), Some(0), "LEGACY (paint_with)");
1844 let (m_gpu, c_gpu) = dominium_like(true);
1845 assert_eq!(hit_test_click(&m_gpu, &c_gpu, 200.0, 200.0), Some(0), "GPU (gpu_paint_with+paint_over)");
1846 }
1847
1848 #[test]
1849 fn nodo_gpu_paint_with_solo_es_hittable_por_si_mismo() {
1850 let canvas = View::<()>::new(Style {
1854 size: Size { width: length(100.0), height: length(100.0) },
1855 ..Default::default()
1856 })
1857 .gpu_paint_with(|_d, _q, _e, _t, _r, _vp| {})
1858 .on_click(());
1859 let root = View::<()>::new(Style {
1860 align_items: Some(AlignItems::FlexStart),
1861 justify_content: Some(JustifyContent::FlexStart),
1862 ..Default::default()
1863 })
1864 .children(vec![canvas]);
1865 let mut layout = LayoutTree::new();
1866 let m = mount(&mut layout, root);
1867 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
1868 assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), Some(1), "gpu-only con on_click debe ser hittable");
1869 }
1870
1871 #[test]
1872 fn transform_rel_resuelve_contra_el_tamano_del_nodo() {
1873 let (m, c) = fixture_rel((-0.5, -0.5));
1878 assert_eq!(hit_test_click(&m, &c, 25.0, 25.0), Some(1)); assert_eq!(hit_test_click(&m, &c, 49.0, 49.0), Some(1)); assert_eq!(hit_test_click(&m, &c, 75.0, 75.0), None);
1884 let (m0, c0) = fixture_rel((0.0, 0.0)); assert_eq!(hit_test_click(&m0, &c0, 75.0, 75.0), Some(1));
1887 }
1888
1889 #[test]
1890 fn hit_test_cursor_directo_y_por_herencia() {
1891 use crate::{hit_test_cursor, Cursor};
1892 let hijo_sin = View::<()>::new(Style {
1895 size: Size { width: length(100.0), height: length(100.0) },
1896 ..Default::default()
1897 });
1898 let hijo_con = View::<()>::new(Style {
1899 size: Size { width: length(50.0), height: length(50.0) },
1900 ..Default::default()
1901 })
1902 .cursor(Cursor::Pointer);
1903 let root = View::<()>::new(Style {
1904 size: Size { width: length(200.0), height: length(200.0) },
1905 flex_direction: FlexDirection::Column,
1906 align_items: Some(AlignItems::FlexStart),
1907 justify_content: Some(JustifyContent::FlexStart),
1908 ..Default::default()
1909 })
1910 .cursor(Cursor::Text)
1911 .children(vec![hijo_sin, hijo_con]);
1912 let mut layout = LayoutTree::new();
1913 let m = mount(&mut layout, root);
1914 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
1915 assert_eq!(hit_test_cursor(&m, &c, 50.0, 50.0), Some(Cursor::Text));
1917 assert_eq!(hit_test_cursor(&m, &c, 25.0, 120.0), Some(Cursor::Pointer));
1919 assert_eq!(hit_test_cursor(&m, &c, 150.0, 50.0), Some(Cursor::Text));
1921 assert_eq!(hit_test_cursor(&m, &c, 350.0, 350.0), None);
1923 }
1924
1925 #[test]
1926 fn tab_traversal_envuelve_en_los_extremos() {
1927 use crate::next_focus;
1928 let order = [10u64, 20, 30];
1929 assert_eq!(next_focus(&order, Some(10), false), Some(20));
1931 assert_eq!(next_focus(&order, Some(30), false), Some(10)); assert_eq!(next_focus(&order, Some(20), true), Some(10));
1934 assert_eq!(next_focus(&order, Some(10), true), Some(30)); assert_eq!(next_focus(&order, None, false), Some(10));
1937 assert_eq!(next_focus(&order, None, true), Some(30));
1938 assert_eq!(next_focus(&order, Some(99), false), Some(10));
1940 assert_eq!(next_focus(&[], Some(10), false), None);
1942 }
1943
1944 #[test]
1945 fn hit_test_scale_directo_y_por_herencia() {
1946 use crate::{hit_test_scale, GesturePhase};
1947 let widget = View::<()>::new(Style {
1952 size: Size { width: length(50.0), height: length(50.0) },
1953 ..Default::default()
1954 });
1955 let canvas = View::<()>::new(Style {
1956 size: Size { width: length(200.0), height: length(200.0) },
1957 align_items: Some(AlignItems::FlexStart),
1958 justify_content: Some(JustifyContent::FlexStart),
1959 ..Default::default()
1960 })
1961 .on_scale(|_phase: GesturePhase, _f, _fx, _fy| None)
1962 .children(vec![widget]);
1963 let mut layout = LayoutTree::new();
1964 let m = mount(&mut layout, canvas);
1965 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
1966 assert_eq!(hit_test_scale(&m, &c, 25.0, 25.0), Some(0));
1968 assert_eq!(hit_test_scale(&m, &c, 150.0, 25.0), Some(0));
1970 assert_eq!(hit_test_scale(&m, &c, 350.0, 350.0), None);
1972 }
1973
1974 #[test]
1975 fn hit_test_rotate_directo_y_por_herencia() {
1976 use crate::{hit_test_rotate, GesturePhase};
1977 let widget = View::<()>::new(Style {
1980 size: Size { width: length(50.0), height: length(50.0) },
1981 ..Default::default()
1982 });
1983 let canvas = View::<()>::new(Style {
1984 size: Size { width: length(200.0), height: length(200.0) },
1985 align_items: Some(AlignItems::FlexStart),
1986 justify_content: Some(JustifyContent::FlexStart),
1987 ..Default::default()
1988 })
1989 .on_rotate(|_phase: GesturePhase, _d, _fx, _fy| None)
1990 .children(vec![widget]);
1991 let mut layout = LayoutTree::new();
1992 let m = mount(&mut layout, canvas);
1993 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
1994 assert_eq!(hit_test_rotate(&m, &c, 25.0, 25.0), Some(0));
1995 assert_eq!(hit_test_rotate(&m, &c, 150.0, 25.0), Some(0));
1996 assert_eq!(hit_test_rotate(&m, &c, 350.0, 350.0), None);
1997 }
1998
1999 #[test]
2000 fn hit_test_selectable_solo_sobre_texto_seleccionable() {
2001 use crate::hit_test_selectable;
2002 let label = View::<()>::new(Style {
2005 size: Size { width: length(100.0), height: length(30.0) },
2006 ..Default::default()
2007 })
2008 .text("hola", 14.0, vello::peniko::Color::from_rgba8(255, 255, 255, 255))
2009 .selectable(7);
2010 let panel = View::<()>::new(Style {
2011 size: Size { width: length(200.0), height: length(200.0) },
2012 align_items: Some(AlignItems::FlexStart),
2013 justify_content: Some(JustifyContent::FlexStart),
2014 ..Default::default()
2015 })
2016 .children(vec![label]);
2017 let mut layout = LayoutTree::new();
2018 let m = mount(&mut layout, panel);
2019 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
2020 assert_eq!(hit_test_selectable(&m, &c, 50.0, 15.0), Some(1));
2022 assert_eq!(hit_test_selectable(&m, &c, 150.0, 150.0), None);
2024 }
2025
2026 #[test]
2027 fn hit_test_scroll_chain_devuelve_front_to_back() {
2028 use crate::hit_test_scroll_chain;
2029 let hijo = View::<()>::new(Style {
2033 size: Size { width: length(100.0), height: length(100.0) },
2034 ..Default::default()
2035 })
2036 .on_scroll(|_dx, _dy| None::<()>);
2037 let padre = View::<()>::new(Style {
2038 size: Size { width: length(200.0), height: length(200.0) },
2039 align_items: Some(AlignItems::FlexStart),
2040 justify_content: Some(JustifyContent::FlexStart),
2041 ..Default::default()
2042 })
2043 .on_scroll(|_dx, _dy| None::<()>)
2044 .children(vec![hijo]);
2045 let mut layout = LayoutTree::new();
2046 let m = mount(&mut layout, padre);
2047 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
2048 let ch = hit_test_scroll_chain(&m, &c, 50.0, 50.0);
2050 assert_eq!(ch, vec![1, 0]);
2051 let ch = hit_test_scroll_chain(&m, &c, 150.0, 50.0);
2053 assert_eq!(ch, vec![0]);
2054 let ch = hit_test_scroll_chain(&m, &c, 350.0, 350.0);
2056 assert!(ch.is_empty());
2057 }
2058
2059 #[test]
2060 fn hit_test_double_tap_y_long_press() {
2061 use crate::{hit_test_double_tap, hit_test_long_press};
2062 let arriba = View::<()>::new(Style {
2065 size: Size { width: length(100.0), height: length(100.0) },
2066 ..Default::default()
2067 })
2068 .on_double_tap(());
2069 let abajo = View::<()>::new(Style {
2070 size: Size { width: length(100.0), height: length(100.0) },
2071 ..Default::default()
2072 })
2073 .on_long_press(());
2074 let root = View::<()>::new(Style {
2075 flex_direction: FlexDirection::Column,
2076 align_items: Some(AlignItems::FlexStart),
2077 justify_content: Some(JustifyContent::FlexStart),
2078 ..Default::default()
2079 })
2080 .children(vec![arriba, abajo]);
2081 let mut layout = LayoutTree::new();
2082 let m = mount(&mut layout, root);
2083 let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
2084 assert_eq!(hit_test_double_tap(&m, &c, 50.0, 50.0), Some(1));
2086 assert_eq!(hit_test_long_press(&m, &c, 50.0, 50.0), None);
2087 assert_eq!(hit_test_long_press(&m, &c, 50.0, 150.0), Some(2));
2089 assert_eq!(hit_test_double_tap(&m, &c, 50.0, 150.0), None);
2090 assert_eq!(hit_test_double_tap(&m, &c, 300.0, 300.0), None);
2092 assert_eq!(hit_test_long_press(&m, &c, 300.0, 300.0), None);
2093 }
2094}