use super::*;
pub fn mount<Msg: Clone>(layout: &mut LayoutTree, v: View<Msg>) -> Mounted<Msg> {
let mut nodes = Vec::new();
let mut text_measures = std::collections::HashMap::new();
let root = mount_recursive(layout, v, &mut nodes, &mut text_measures);
Mounted { root, nodes, text_measures }
}
pub fn mount_recursive<Msg: Clone>(
layout: &mut LayoutTree,
v: View<Msg>,
out: &mut Vec<MountedNode<Msg>>,
text_measures: &mut std::collections::HashMap<NodeId, TextMeasure>,
) -> NodeId {
let View {
style,
fill,
hover_fill,
radius,
corner_radii,
shadow,
fill_gradient,
border,
text,
image,
image_fit,
mask_image,
mask_placement,
mask_extra,
painter,
gpu_painter,
over_painter,
on_click,
on_click_at,
on_right_click,
on_right_click_at,
on_middle_click,
drag,
drag_at,
drag_velocity,
drag_payload,
on_drop,
drop_hover_fill,
clip,
clip_inset,
clip_ellipse,
clip_polygon,
clip_path_svg,
clip_ref_inset,
on_pointer_enter,
on_pointer_leave,
on_pointer_move_at,
on_scroll,
on_scale,
on_rotate,
on_double_tap,
on_double_tap_at,
on_long_press,
on_long_press_at,
focusable,
text_select_key,
alpha,
anim,
animated_size,
semantics,
hero,
transform,
transform_rel,
transform_origin,
tooltip,
cursor,
ripple,
layout_builder,
backdrop_blur,
filter,
blend,
children,
} = v;
let parent_idx = out.len();
out.push(MountedNode {
id: NodeId::new(0), fill,
hover_fill,
radius,
corner_radii,
shadow,
fill_gradient,
border,
text,
image,
image_fit,
mask_image,
mask_placement,
mask_extra,
painter,
gpu_painter,
over_painter,
on_click,
on_click_at,
on_right_click,
on_right_click_at,
on_middle_click,
drag,
drag_at,
drag_velocity,
drag_payload,
on_drop,
drop_hover_fill,
clip,
clip_inset,
clip_ellipse,
clip_polygon,
clip_path_svg,
clip_ref_inset,
on_pointer_enter,
on_pointer_leave,
on_pointer_move_at,
on_scroll,
on_scale,
on_rotate,
on_double_tap,
on_double_tap_at,
on_long_press,
on_long_press_at,
focusable,
text_select_key,
alpha,
anim,
animated_size,
semantics,
hero,
transform,
transform_rel,
transform_origin,
tooltip,
cursor,
ripple,
is_layout_builder: layout_builder.is_some(),
backdrop_blur,
filter,
blend,
subtree_end: 0,
});
let mut child_ids = Vec::with_capacity(children.len());
for child in children {
child_ids.push(mount_recursive(layout, child, out, text_measures));
}
let id = if child_ids.is_empty() {
layout.leaf(style).expect("layout leaf")
} else {
layout.node(style, &child_ids).expect("layout node")
};
out[parent_idx].id = id;
out[parent_idx].subtree_end = out.len();
if child_ids.is_empty() {
if let Some(text) = out[parent_idx].text.as_ref() {
if text.runs.is_none() {
text_measures.insert(
id,
TextMeasure {
content: text.content.clone(),
size_px: text.size_px,
alignment: text.alignment,
italic: text.italic,
font_family: text.font_family.clone(),
line_height: text.line_height,
weight: text.weight,
max_lines: text.max_lines,
ellipsis: text.ellipsis,
underline: text.underline,
strikethrough: text.strikethrough,
spans: text.spans.clone(),
letter_spacing: text.letter_spacing,
word_spacing: text.word_spacing,
no_wrap: text.no_wrap,
overflow_wrap: text.overflow_wrap,
},
);
}
}
}
id
}
pub fn measure_text_node(
ts: &mut llimphi_text::Typesetter,
tm: &TextMeasure,
known: llimphi_layout::taffy::Size<Option<f32>>,
available: llimphi_layout::taffy::Size<llimphi_layout::taffy::AvailableSpace>,
) -> llimphi_layout::taffy::Size<f32> {
use llimphi_layout::taffy::AvailableSpace;
let max_width: Option<f32> = if tm.no_wrap {
None
} else {
known.width.or(match available.width {
AvailableSpace::Definite(w) => Some(w),
AvailableSpace::MaxContent => None,
AvailableSpace::MinContent => Some(0.0),
})
};
if let Some(spans) = tm.spans.as_ref() {
if !spans.is_empty() {
let layout = ts.layout_spans(
&tm.content,
tm.size_px,
vello::peniko::Color::from_rgba8(0, 0, 0, 255),
tm.weight,
tm.line_height,
tm.italic,
tm.font_family.as_deref(),
tm.underline,
tm.strikethrough,
spans,
max_width,
tm.alignment,
);
return llimphi_layout::taffy::Size {
width: layout.width(),
height: layout.height(),
};
}
}
let layout = ts.layout_clamped(
&tm.content,
tm.size_px,
max_width,
tm.alignment,
tm.line_height,
tm.italic,
tm.font_family.as_deref(),
tm.weight,
tm.max_lines,
tm.ellipsis,
tm.underline,
tm.strikethrough,
tm.letter_spacing,
tm.word_spacing,
tm.overflow_wrap,
);
let m = llimphi_text::measurement(&layout);
llimphi_layout::taffy::Size { width: m.width, height: m.height }
}
pub(crate) fn node_rrect(
x0: f64,
y0: f64,
x1: f64,
y1: f64,
radius: f64,
corners: Option<RoundedRectRadii>,
inset: f64,
) -> RoundedRect {
let radii = match corners {
Some(c) => RoundedRectRadii::new(
(c.top_left - inset).max(0.0),
(c.top_right - inset).max(0.0),
(c.bottom_right - inset).max(0.0),
(c.bottom_left - inset).max(0.0),
),
None => {
let r = (radius - inset).max(0.0);
RoundedRectRadii::new(r, r, r, r)
}
};
RoundedRect::new(x0 + inset, y0 + inset, x1 - inset, y1 - inset, radii)
}
fn resolve_clip_radius(q: &[f32], cxl: f64, cyl: f64, w: f64, h: f64, is_x: bool) -> f64 {
let side = q[4] as i32;
if side == 0 {
let diag = (w * w + h * h).sqrt() / core::f64::consts::SQRT_2;
return q[0] as f64 + q[1] as f64 / 100.0 * w + q[2] as f64 / 100.0 * h
+ q[3] as f64 / 100.0 * diag;
}
let (dx_near, dx_far) = (cxl.min(w - cxl), cxl.max(w - cxl));
let (dy_near, dy_far) = (cyl.min(h - cyl), cyl.max(h - cyl));
match side {
1 => dx_near.min(dy_near), 2 => dx_far.max(dy_far), 3 => {
if is_x {
dx_near
} else {
dy_near
}
} _ => {
if is_x {
dx_far
} else {
dy_far
}
} }
}
pub fn paint<Msg>(
scene: &mut vello::Scene,
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
typesetter: &mut llimphi_text::Typesetter,
hover_idx: Option<usize>,
drop_hover_idx: Option<usize>,
) {
paint_range(
scene,
mounted,
computed,
typesetter,
hover_idx,
drop_hover_idx,
0,
mounted.nodes.len(),
Affine::IDENTITY,
);
}
pub fn collect_backdrop_blurs<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
) -> Vec<BackdropBlur> {
let mut out = Vec::new();
let mut idx = 0;
while idx < mounted.nodes.len() {
let node = &mounted.nodes[idx];
if let Some(sigma) = node.backdrop_blur {
if let Some(r) = computed.get(node.id) {
out.push(BackdropBlur {
sigma,
rect: (r.x, r.y, r.w, r.h),
});
idx = node.subtree_end;
continue;
}
}
idx += 1;
}
out
}
#[derive(Debug, Clone, Copy)]
pub struct BackdropBlur {
pub sigma: f32,
pub rect: (f32, f32, f32, f32),
}
#[derive(Debug, Clone)]
pub struct FilterPass {
pub rect: (f32, f32, f32, f32),
pub op: FilterOp,
}
pub fn collect_filters<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
) -> Vec<FilterPass> {
let mut out = Vec::new();
let mut idx = 0;
while idx < mounted.nodes.len() {
let node = &mounted.nodes[idx];
if !node.filter.is_empty() {
if let Some(r) = computed.get(node.id) {
let rect = (r.x, r.y, r.w, r.h);
for op in &node.filter {
if matches!(op, FilterOp::DropShadow(_)) {
continue;
}
out.push(FilterPass { rect, op: op.clone() });
}
idx = node.subtree_end;
continue;
}
}
idx += 1;
}
out
}
pub(crate) fn resolve_node_transform(
transform: Option<Affine>,
transform_rel: Option<(f64, f64)>,
transform_origin: Option<crate::TransformPivot>,
r: llimphi_layout::Rect,
) -> Option<Affine> {
if transform.is_none() && transform_rel.is_none() {
return None;
}
let mut local = transform.unwrap_or(Affine::IDENTITY);
if let Some((fx, fy)) = transform_rel {
local = Affine::translate((fx * r.w as f64, fy * r.h as f64)) * local;
}
let pivot = transform_origin.unwrap_or_default();
let ox = r.x as f64 + pivot.px.0 + pivot.frac.0 * r.w as f64;
let oy = r.y as f64 + pivot.px.1 + pivot.frac.1 * r.h as f64;
Some(Affine::translate((ox, oy)) * local * Affine::translate((-ox, -oy)))
}
fn paint_mask_close(
scene: &mut vello::Scene,
img: &Image,
extra: &[(Image, MaskCompose)],
rect: KurboRect,
xf: Affine,
placement: Option<MaskPlacement>,
) {
let shrink = |r: KurboRect, inset: Option<[f32; 4]>| -> KurboRect {
match inset {
None => r,
Some([t, ri, b, le]) => KurboRect::new(
r.x0 + le as f64,
r.y0 + t as f64,
(r.x1 - ri as f64).max(r.x0 + le as f64),
(r.y1 - b as f64).max(r.y0 + t as f64),
),
}
};
let clip_rect = shrink(rect, placement.and_then(|p| p.clip_inset));
let origin_rect = shrink(rect, placement.and_then(|p| p.origin_inset));
let mode = placement.map(|p| p.mode).unwrap_or(MaskMode::Luminance);
match mode {
MaskMode::Luminance => scene.push_luminance_mask_layer(Fill::NonZero, 1.0, xf, &clip_rect),
MaskMode::Alpha => scene.push_layer(
Fill::NonZero,
vello::peniko::BlendMode::new(Mix::Normal, vello::peniko::Compose::DestIn),
1.0,
xf,
&clip_rect,
),
}
draw_mask_layer(scene, img, origin_rect, xf, placement);
for (eimg, op) in extra {
match op {
MaskCompose::Add => draw_mask_layer(scene, eimg, origin_rect, xf, placement),
_ => {
let compose = match op {
MaskCompose::Subtract => vello::peniko::Compose::SrcOut,
MaskCompose::Intersect => vello::peniko::Compose::SrcIn,
MaskCompose::Exclude => vello::peniko::Compose::Xor,
MaskCompose::Add => unreachable!(),
};
scene.push_layer(
Fill::NonZero,
vello::peniko::BlendMode::new(Mix::Normal, compose),
1.0,
xf,
&clip_rect,
);
draw_mask_layer(scene, eimg, origin_rect, xf, placement);
scene.pop_layer();
}
}
}
scene.pop_layer();
}
fn draw_mask_layer(
scene: &mut vello::Scene,
img: &Image,
origin_rect: KurboRect,
xf: Affine,
placement: Option<MaskPlacement>,
) {
let iw = img.image.width.max(1) as f64;
let ih = img.image.height.max(1) as f64;
match placement {
None => {
let fit = Affine::translate((origin_rect.x0, origin_rect.y0))
* Affine::scale_non_uniform(origin_rect.width() / iw, origin_rect.height() / ih);
scene.draw_image(img, xf * fit);
}
Some(p) => {
let rw = origin_rect.width();
let rh = origin_rect.height();
let resolve = |l: MaskLen, basis: f64| -> Option<f64> {
match l {
MaskLen::Px(n) => Some(n as f64),
MaskLen::Pct(q) => Some(basis * q as f64 / 100.0),
MaskLen::Auto => None,
}
};
let (tw, th) = match p.size {
MaskSize::Auto => (iw, ih),
MaskSize::Cover => {
let s = (rw / iw).max(rh / ih);
(iw * s, ih * s)
}
MaskSize::Contain => {
let s = (rw / iw).min(rh / ih);
(iw * s, ih * s)
}
MaskSize::Explicit { x, y } => match (resolve(x, rw), resolve(y, rh)) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (w, w * ih / iw),
(None, Some(h)) => (h * iw / ih, h),
(None, None) => (iw, ih),
},
};
if tw > 0.5 && th > 0.5 {
let pos_off = |l: MaskLen, basis: f64, tile: f64| -> f64 {
match l {
MaskLen::Px(n) => n as f64,
MaskLen::Pct(q) => (basis - tile) * q as f64 / 100.0,
MaskLen::Auto => 0.0,
}
};
let ox = pos_off(p.pos_x, rw, tw);
let oy = pos_off(p.pos_y, rh, th);
let axis = |off: f64, tile: f64, span: f64, rep: bool| -> Vec<f64> {
if !rep {
return vec![off];
}
let mut start = off;
while start > 0.0 {
start -= tile;
}
let mut v = Vec::new();
let mut q = start;
while q < span && v.len() < 4096 {
v.push(q);
q += tile;
}
v
};
let xs = axis(ox, tw, rw, p.repeat_x);
let ys = axis(oy, th, rh, p.repeat_y);
let scale = Affine::scale_non_uniform(tw / iw, th / ih);
for &x in &xs {
for &y in &ys {
let tf =
Affine::translate((origin_rect.x0 + x, origin_rect.y0 + y)) * scale;
scene.draw_image(img, xf * tf);
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn paint_range<Msg>(
scene: &mut vello::Scene,
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
typesetter: &mut llimphi_text::Typesetter,
hover_idx: Option<usize>,
drop_hover_idx: Option<usize>,
start: usize,
end: usize,
base_xf: Affine,
) {
type MaskClose = (
Image,
Vec<(Image, MaskCompose)>,
KurboRect,
Affine,
Option<MaskPlacement>,
);
let mut layer_stack: Vec<(usize, Option<MaskClose>)> = Vec::new();
let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
let mut cur_xf = base_xf;
for idx in start..end {
let node = &mounted.nodes[idx];
while let Some(&(end, _)) = layer_stack.last() {
if idx >= end {
let (_, mask) = layer_stack.pop().unwrap();
if let Some((img, extra, rect, xf, placement)) = &mask {
paint_mask_close(scene, img, extra, *rect, *xf, *placement);
}
scene.pop_layer();
} else {
break;
}
}
while let Some(&(end, prev)) = xf_stack.last() {
if idx >= end {
cur_xf = prev;
xf_stack.pop();
} else {
break;
}
}
let Some(r) = computed.get(node.id) else {
continue;
};
if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
xf_stack.push((node.subtree_end, cur_xf));
cur_xf *= centered;
}
if let Some(bm) = node.blend {
let rect = KurboRect::new(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
);
scene.push_layer(Fill::NonZero, bm, 1.0, cur_xf, &rect);
layer_stack.push((node.subtree_end, None));
}
if let Some(a) = node.alpha {
let rect = KurboRect::new(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
);
scene.push_layer(Fill::NonZero, Mix::Normal, a, cur_xf, &rect);
layer_stack.push((node.subtree_end, None));
}
if let Some(mask_img) = node.mask_image.as_ref() {
let rect = KurboRect::new(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
);
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &rect);
layer_stack.push((
node.subtree_end,
Some((
mask_img.clone(),
node.mask_extra.clone(),
rect,
cur_xf,
node.mask_placement,
)),
));
}
if let Some(sh) = node.shadow.as_ref() {
if sh.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
let rect = KurboRect::new(
(r.x as f64) + sh.dx - sh.spread,
(r.y as f64) + sh.dy - sh.spread,
(r.x + r.w) as f64 + sh.dx + sh.spread,
(r.y + r.h) as f64 + sh.dy + sh.spread,
);
let radius = (node.radius + sh.spread).max(0.0);
scene.draw_blurred_rounded_rect(cur_xf, rect, sh.color, radius, sh.blur);
}
}
for op in node.filter.iter().rev() {
if let FilterOp::DropShadow(sh) = op {
if sh.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
let rect = KurboRect::new(
(r.x as f64) + sh.dx - sh.spread,
(r.y as f64) + sh.dy - sh.spread,
(r.x + r.w) as f64 + sh.dx + sh.spread,
(r.y + r.h) as f64 + sh.dy + sh.spread,
);
let radius = (node.radius + sh.spread).max(0.0);
scene.draw_blurred_rounded_rect(cur_xf, rect, sh.color, radius, sh.blur);
}
}
}
let hover_color = if Some(idx) == drop_hover_idx {
node.drop_hover_fill.or(node.hover_fill).or(node.fill)
} else if Some(idx) == hover_idx {
node.hover_fill.or(node.fill)
} else {
None
};
let rr = node_rrect(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
node.radius,
node.corner_radii,
0.0,
);
if let Some(color) = hover_color {
scene.fill(Fill::NonZero, cur_xf, color, None, &rr);
} else if let Some(grad) = node.fill_gradient.as_ref() {
let brush_xf = cur_xf
* Affine::translate((r.x as f64, r.y as f64))
* Affine::scale_non_uniform(r.w as f64, r.h as f64);
scene.fill(Fill::NonZero, cur_xf, grad, Some(brush_xf), &rr);
} else if let Some(color) = node.fill {
scene.fill(Fill::NonZero, cur_xf, color, None, &rr);
}
if let Some(b) = node.border.as_ref() {
if b.width > 0.0 && b.color.components[3] > 0.0 && r.w > 0.0 && r.h > 0.0 {
let inset = b.width * 0.5;
let brr = node_rrect(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
node.radius,
node.corner_radii,
inset,
);
scene.stroke(&Stroke::new(b.width), cur_xf, b.color, None, &brr);
}
}
if let Some(image) = node.image.as_ref() {
if image.image.width > 0 && image.image.height > 0 && r.w > 0.0 && r.h > 0.0 {
let sx = r.w as f64 / image.image.width as f64;
let sy = r.h as f64 / image.image.height as f64;
let fit = node.image_fit.unwrap_or(ImageFit::Contain);
let transform = match fit {
ImageFit::Contain => {
let s = sx.min(sy);
let disp_w = image.image.width as f64 * s;
let disp_h = image.image.height as f64 * s;
let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
Affine::translate((tx, ty)) * Affine::scale(s)
}
ImageFit::Cover => {
let s = sx.max(sy);
let disp_w = image.image.width as f64 * s;
let disp_h = image.image.height as f64 * s;
let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
Affine::translate((tx, ty)) * Affine::scale(s)
}
ImageFit::Fill => {
Affine::translate((r.x as f64, r.y as f64))
* Affine::scale_non_uniform(sx, sy)
}
ImageFit::None => {
let disp_w = image.image.width as f64;
let disp_h = image.image.height as f64;
let tx = r.x as f64 + (r.w as f64 - disp_w) * 0.5;
let ty = r.y as f64 + (r.h as f64 - disp_h) * 0.5;
Affine::translate((tx, ty))
}
};
let clip_rr = node_rrect(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
node.radius,
node.corner_radii,
0.0,
);
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &clip_rr);
scene.draw_image(image, cur_xf * transform);
scene.pop_layer();
}
}
if let Some(painter) = node.painter.as_ref() {
(painter)(
scene,
typesetter,
PaintRect {
x: r.x,
y: r.y,
w: r.w,
h: r.h,
},
);
}
if let Some(text) = node.text.as_ref() {
let has_spans = text
.spans
.as_ref()
.map(|s| !s.is_empty())
.unwrap_or(false);
if has_spans {
let spans = text.spans.as_ref().unwrap();
let layout = typesetter.layout_spans(
&text.content,
text.size_px,
text.color,
text.weight,
text.line_height,
text.italic,
text.font_family.as_deref(),
text.underline,
text.strikethrough,
spans,
Some(r.w),
text.alignment,
);
let origin =
if matches!(text.alignment, llimphi_text::Alignment::Center) {
let lh = layout.height() as f64;
(
r.x as f64,
r.y as f64 + ((r.h as f64 - lh) * 0.5).max(0.0),
)
} else {
(r.x as f64, r.y as f64)
};
llimphi_text::draw_layout_runs_xf(
scene,
&layout,
cur_xf * Affine::translate(origin),
);
} else if let Some(runs) = text.runs.as_ref() {
let layout = typesetter.layout_runs(
&text.content,
text.size_px,
text.color,
runs,
text.alignment,
text.line_height,
text.weight,
text.underline,
text.strikethrough,
);
llimphi_text::draw_layout_runs_xf(
scene,
&layout,
cur_xf * Affine::translate((r.x as f64, r.y as f64)),
);
} else {
let paint_max_width = if text.no_wrap { None } else { Some(r.w) };
let layout = typesetter.layout_clamped(
&text.content,
text.size_px,
paint_max_width,
text.alignment,
text.line_height,
text.italic,
text.font_family.as_deref(),
text.weight,
text.max_lines,
text.ellipsis,
text.underline,
text.strikethrough,
text.letter_spacing,
text.word_spacing,
text.overflow_wrap,
);
let origin =
if matches!(text.alignment, llimphi_text::Alignment::Center) {
let m = llimphi_text::measurement(&layout);
(
r.x as f64,
r.y as f64 + ((r.h - m.height) as f64 * 0.5).max(0.0),
)
} else {
(r.x as f64, r.y as f64)
};
llimphi_text::draw_layout_xf(
scene,
&layout,
text.color,
cur_xf * Affine::translate(origin),
);
}
}
if node.clip {
let mut pushed = true;
let [rit, rir, rib, ril] = node.clip_ref_inset.unwrap_or([0.0; 4]);
let (bx, by) = ((r.x + ril) as f64, (r.y + rit) as f64);
let (bw, bh) = ((r.w - ril - rir).max(0.0) as f64, (r.h - rit - rib).max(0.0) as f64);
if let Some((evenodd, d)) = &node.clip_path_svg {
match vello::kurbo::BezPath::from_svg(d) {
Ok(mut path) => {
path.apply_affine(Affine::translate((bx, by)));
let fill = if *evenodd { Fill::EvenOdd } else { Fill::NonZero };
scene.push_layer(fill, BlendMode::default(), 1.0, cur_xf, &path);
}
Err(_) => pushed = false,
}
} else if let Some((evenodd, pts)) = &node.clip_polygon {
let mut path = vello::kurbo::BezPath::new();
for (i, p) in pts.iter().enumerate() {
let px = bx + p[0] as f64 + p[1] as f64 / 100.0 * bw;
let py = by + p[2] as f64 + p[3] as f64 / 100.0 * bh;
if i == 0 {
path.move_to((px, py));
} else {
path.line_to((px, py));
}
}
path.close_path();
let fill = if *evenodd { Fill::EvenOdd } else { Fill::NonZero };
scene.push_layer(fill, BlendMode::default(), 1.0, cur_xf, &path);
} else if let Some(s) = node.clip_ellipse {
let cxl = s[0] as f64 + s[1] as f64 / 100.0 * bw;
let cyl = s[2] as f64 + s[3] as f64 / 100.0 * bh;
let cx = bx + cxl;
let cy = by + cyl;
let rx = resolve_clip_radius(&s[4..9], cxl, cyl, bw, bh, true);
let ry = resolve_clip_radius(&s[9..14], cxl, cyl, bw, bh, false);
let ellipse = Ellipse::new((cx, cy), (rx, ry), 0.0);
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &ellipse);
} else {
let [ct, cr, cb, cl] = node.clip_inset.unwrap_or([0.0; 4]);
let clip_rect = KurboRect::new(
bx + cl as f64,
by + ct as f64,
bx + bw - cr as f64,
by + bh - cb as f64,
);
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, cur_xf, &clip_rect);
}
if pushed {
layer_stack.push((node.subtree_end, None));
}
}
}
while let Some((_, mask)) = layer_stack.pop() {
if let Some((img, extra, rect, xf, placement)) = &mask {
paint_mask_close(scene, img, extra, *rect, *xf, *placement);
}
scene.pop_layer();
}
}
pub fn has_gpu_painter<Msg>(mounted: &Mounted<Msg>) -> bool {
mounted.nodes.iter().any(|n| n.gpu_painter.is_some())
}
pub fn paint_gpu<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
viewport: (u32, u32),
) -> bool {
let mut any = false;
for node in &mounted.nodes {
let Some(painter) = node.gpu_painter.as_ref() else {
continue;
};
let Some(r) = computed.get(node.id) else {
continue;
};
(painter)(
device,
queue,
encoder,
view,
PaintRect {
x: r.x,
y: r.y,
w: r.w,
h: r.h,
},
viewport,
);
any = true;
}
any
}
pub fn has_over_painter<Msg>(mounted: &Mounted<Msg>) -> bool {
mounted.nodes.iter().any(|n| n.over_painter.is_some())
}
pub fn paint_over<Msg>(
scene: &mut vello::Scene,
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
typesetter: &mut llimphi_text::Typesetter,
) -> bool {
let mut any = false;
for node in &mounted.nodes {
let Some(painter) = node.over_painter.as_ref() else {
continue;
};
let Some(r) = computed.get(node.id) else {
continue;
};
(painter)(
scene,
typesetter,
PaintRect {
x: r.x,
y: r.y,
w: r.w,
h: r.h,
},
);
any = true;
}
any
}
pub fn hit_test_pred<Msg, F>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
pred: F,
) -> Option<usize>
where
F: Fn(&MountedNode<Msg>) -> bool,
{
let mut hit: Option<usize> = None;
let mut clip_stack: Vec<usize> = Vec::new();
let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
let mut cur_xf = Affine::IDENTITY;
let mut idx = 0;
while idx < mounted.nodes.len() {
while let Some(&end) = clip_stack.last() {
if idx >= end {
clip_stack.pop();
} else {
break;
}
}
while let Some(&(end, prev)) = xf_stack.last() {
if idx >= end {
cur_xf = prev;
xf_stack.pop();
} else {
break;
}
}
let node = &mounted.nodes[idx];
let Some(r) = computed.get(node.id) else {
idx += 1;
continue;
};
if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
xf_stack.push((node.subtree_end, cur_xf));
cur_xf *= centered;
}
let (lx, ly) = if xf_stack.is_empty() {
(x as f64, y as f64)
} else if cur_xf.determinant().abs() < 1e-9 {
idx = node.subtree_end;
continue;
} else {
let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
(p.x, p.y)
};
let inside = lx >= r.x as f64
&& lx < (r.x + r.w) as f64
&& ly >= r.y as f64
&& ly < (r.y + r.h) as f64;
if node.clip {
if !inside {
idx = node.subtree_end;
continue;
}
clip_stack.push(node.subtree_end);
}
if inside && pred(node) {
hit = Some(idx);
}
idx += 1;
}
hit
}
pub fn hit_test_click<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| {
n.on_click.is_some()
|| n.on_click_at.is_some()
|| n.drag.is_some()
|| n.drag_at.is_some()
|| n.drag_velocity.is_some()
})
}
pub fn hit_test_right_click<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| {
n.on_right_click.is_some() || n.on_right_click_at.is_some()
})
}
pub fn hit_test_middle_click<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_middle_click.is_some())
}
pub fn hit_test_hover<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.hover_fill.is_some())
}
pub fn hit_test_pointer_move<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_pointer_move_at.is_some())
}
pub fn hit_test_cursor<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<Cursor> {
hit_test_pred(mounted, computed, x, y, |n| n.cursor.is_some())
.and_then(|i| mounted.nodes[i].cursor)
}
pub fn hit_test_drop<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_drop.is_some())
}
pub fn hit_test_scroll<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_scroll.is_some())
}
pub fn hit_test_scroll_chain<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Vec<usize> {
let mut chain: Vec<usize> = Vec::new();
let mut clip_stack: Vec<usize> = Vec::new();
let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
let mut cur_xf = Affine::IDENTITY;
let mut idx = 0;
while idx < mounted.nodes.len() {
while let Some(&end) = clip_stack.last() {
if idx >= end {
clip_stack.pop();
} else {
break;
}
}
while let Some(&(end, prev)) = xf_stack.last() {
if idx >= end {
cur_xf = prev;
xf_stack.pop();
} else {
break;
}
}
let node = &mounted.nodes[idx];
let Some(r) = computed.get(node.id) else {
idx += 1;
continue;
};
if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
xf_stack.push((node.subtree_end, cur_xf));
cur_xf *= centered;
}
let (lx, ly) = if xf_stack.is_empty() {
(x as f64, y as f64)
} else if cur_xf.determinant().abs() < 1e-9 {
idx = node.subtree_end;
continue;
} else {
let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
(p.x, p.y)
};
let inside = lx >= r.x as f64
&& lx < (r.x + r.w) as f64
&& ly >= r.y as f64
&& ly < (r.y + r.h) as f64;
if node.clip {
if !inside {
idx = node.subtree_end;
continue;
}
clip_stack.push(node.subtree_end);
}
if inside && node.on_scroll.is_some() {
chain.push(idx);
}
idx += 1;
}
chain.reverse();
chain
}
pub fn hit_test_scale<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_scale.is_some())
}
pub fn hit_test_rotate<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.on_rotate.is_some())
}
pub fn hit_test_double_tap<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| {
n.on_double_tap.is_some() || n.on_double_tap_at.is_some()
})
}
pub fn hit_test_long_press<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| {
n.on_long_press.is_some() || n.on_long_press_at.is_some()
})
}
pub fn hit_test_ripple<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.ripple.is_some())
}
pub fn hit_test_focusable<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<u64> {
hit_test_pred(mounted, computed, x, y, |n| n.focusable.is_some())
.and_then(|i| mounted.nodes[i].focusable)
}
pub fn hit_test_selectable<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
x: f32,
y: f32,
) -> Option<usize> {
hit_test_pred(mounted, computed, x, y, |n| n.text_select_key.is_some())
}
pub fn focus_order<Msg>(mounted: &Mounted<Msg>, computed: &ComputedLayout) -> Vec<u64> {
mounted
.nodes
.iter()
.filter_map(|n| {
n.focusable
.filter(|_| computed.get(n.id).is_some())
})
.collect()
}
pub fn next_focus(order: &[u64], current: Option<u64>, reverse: bool) -> Option<u64> {
if order.is_empty() {
return None;
}
let n = order.len();
let pos = current.and_then(|c| order.iter().position(|&id| id == c));
let next_idx = match pos {
Some(i) => {
if reverse {
(i + n - 1) % n
} else {
(i + 1) % n
}
}
None => {
if reverse {
n - 1
} else {
0
}
}
};
Some(order[next_idx])
}
#[cfg(test)]
mod tests {
use crate::{hit_test_click, mount, View};
use llimphi_layout::taffy::prelude::*;
use llimphi_layout::{LayoutTree, Style};
use vello::kurbo::Affine;
#[test]
fn transform_origin_fija_el_pivote() {
use super::resolve_node_transform;
use crate::TransformPivot;
use vello::kurbo::Point;
let r = llimphi_layout::Rect { x: 0.0, y: 0.0, w: 100.0, h: 100.0 };
let rot = Affine::rotate(std::f64::consts::FRAC_PI_2);
let tl = TransformPivot { px: (0.0, 0.0), frac: (0.0, 0.0) };
let xf_tl = resolve_node_transform(Some(rot), None, Some(tl), r).unwrap();
let p = xf_tl * Point::new(0.0, 0.0);
assert!(p.x.abs() < 1e-6 && p.y.abs() < 1e-6, "pivote top-left fijo, fue {p:?}");
let xf_c = resolve_node_transform(Some(rot), None, None, r).unwrap();
let c = xf_c * Point::new(50.0, 50.0);
assert!(
(c.x - 50.0).abs() < 1e-6 && (c.y - 50.0).abs() < 1e-6,
"centro fijo con pivote default, fue {c:?}"
);
let c2 = xf_tl * Point::new(50.0, 50.0);
assert!((c2.x - 50.0).abs() > 1.0 || (c2.y - 50.0).abs() > 1.0, "top-left mueve el centro");
}
#[test]
fn resolve_clip_radius_lados_y_porcentajes() {
use super::resolve_clip_radius;
let (w, h, cxl, cyl): (f64, f64, f64, f64) = (200.0, 100.0, 100.0, 50.0);
let diag = (w * w + h * h).sqrt() / core::f64::consts::SQRT_2;
let r = resolve_clip_radius(&[10.0, 0.0, 0.0, 50.0, 0.0], cxl, cyl, w, h, true);
assert!((r - (10.0 + 0.5 * diag)).abs() < 1e-6);
assert_eq!(
resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 1.0], cxl, cyl, w, h, true),
50.0
);
assert_eq!(
resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 2.0], cxl, cyl, w, h, true),
100.0
);
assert_eq!(
resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 3.0], cxl, cyl, w, h, true),
100.0
);
assert_eq!(
resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 3.0], cxl, cyl, w, h, false),
50.0
);
assert_eq!(
resolve_clip_radius(&[0.0, 0.0, 0.0, 0.0, 1.0], 30.0, 20.0, w, h, true),
20.0
);
}
fn fixture(
transform: Option<Affine>,
) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
let mut child = View::<()>::new(Style {
size: Size {
width: length(100.0),
height: length(100.0),
},
..Default::default()
})
.on_click(());
if let Some(xf) = transform {
child = child.transform(xf);
}
let root = View::<()>::new(Style {
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.children(vec![child]);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, root);
let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
(mounted, computed)
}
#[test]
fn sin_transform_el_hit_cae_en_el_rect() {
let (m, c) = fixture(None);
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); }
#[test]
fn traslacion_mueve_el_area_clickeable() {
let (m, c) = fixture(Some(Affine::translate((200.0, 0.0))));
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); }
#[test]
fn rotacion_180_grados_alrededor_del_centro() {
let (m, c) = fixture(Some(Affine::rotate(std::f64::consts::PI)));
assert_eq!(hit_test_click(&m, &c, 10.0, 10.0), Some(1));
assert_eq!(hit_test_click(&m, &c, 90.0, 90.0), Some(1));
assert_eq!(hit_test_click(&m, &c, 150.0, 150.0), None);
}
#[test]
fn escala_cero_es_inalcanzable() {
let (m, c) = fixture(Some(Affine::scale(0.0)));
assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), None);
}
fn fixture_rel(
rel: (f64, f64),
) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
let child = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.on_click(())
.transform_rel(rel);
let root = View::<()>::new(Style {
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.children(vec![child]);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, root);
let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
(mounted, computed)
}
fn dominium_like(gpu: bool) -> (crate::Mounted<()>, llimphi_layout::ComputedLayout) {
let mut canvas = View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
..Default::default()
});
canvas = if gpu {
canvas
.gpu_paint_with(|_d, _q, _e, _t, _r, _vp| {})
.paint_over(|_s, _ts, _r| {})
} else {
canvas.paint_with(|_s, _ts, _r| {})
};
let wrapper = View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
..Default::default()
})
.clip(true)
.on_click_at(|_lx, _ly, _rw, _rh| Some(()))
.draggable_at(|_phase, _dx, _dy, _x0, _y0| Some(()))
.children(vec![canvas]);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, wrapper);
let computed = layout.compute(mounted.root, (400.0, 400.0)).expect("layout");
(mounted, computed)
}
#[test]
fn canvas_gpu_only_es_clickeable_igual_que_legacy() {
let (m_leg, c_leg) = dominium_like(false);
assert_eq!(hit_test_click(&m_leg, &c_leg, 200.0, 200.0), Some(0), "LEGACY (paint_with)");
let (m_gpu, c_gpu) = dominium_like(true);
assert_eq!(hit_test_click(&m_gpu, &c_gpu, 200.0, 200.0), Some(0), "GPU (gpu_paint_with+paint_over)");
}
#[test]
fn nodo_gpu_paint_with_solo_es_hittable_por_si_mismo() {
let canvas = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.gpu_paint_with(|_d, _q, _e, _t, _r, _vp| {})
.on_click(());
let root = View::<()>::new(Style {
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.children(vec![canvas]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, root);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_click(&m, &c, 50.0, 50.0), Some(1), "gpu-only con on_click debe ser hittable");
}
#[test]
fn transform_rel_resuelve_contra_el_tamano_del_nodo() {
let (m, c) = fixture_rel((-0.5, -0.5));
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);
let (m0, c0) = fixture_rel((0.0, 0.0)); assert_eq!(hit_test_click(&m0, &c0, 75.0, 75.0), Some(1));
}
#[test]
fn hit_test_cursor_directo_y_por_herencia() {
use crate::{hit_test_cursor, Cursor};
let hijo_sin = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
});
let hijo_con = View::<()>::new(Style {
size: Size { width: length(50.0), height: length(50.0) },
..Default::default()
})
.cursor(Cursor::Pointer);
let root = View::<()>::new(Style {
size: Size { width: length(200.0), height: length(200.0) },
flex_direction: FlexDirection::Column,
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.cursor(Cursor::Text)
.children(vec![hijo_sin, hijo_con]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, root);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_cursor(&m, &c, 50.0, 50.0), Some(Cursor::Text));
assert_eq!(hit_test_cursor(&m, &c, 25.0, 120.0), Some(Cursor::Pointer));
assert_eq!(hit_test_cursor(&m, &c, 150.0, 50.0), Some(Cursor::Text));
assert_eq!(hit_test_cursor(&m, &c, 350.0, 350.0), None);
}
#[test]
fn tab_traversal_envuelve_en_los_extremos() {
use crate::next_focus;
let order = [10u64, 20, 30];
assert_eq!(next_focus(&order, Some(10), false), Some(20));
assert_eq!(next_focus(&order, Some(30), false), Some(10)); assert_eq!(next_focus(&order, Some(20), true), Some(10));
assert_eq!(next_focus(&order, Some(10), true), Some(30)); assert_eq!(next_focus(&order, None, false), Some(10));
assert_eq!(next_focus(&order, None, true), Some(30));
assert_eq!(next_focus(&order, Some(99), false), Some(10));
assert_eq!(next_focus(&[], Some(10), false), None);
}
#[test]
fn hit_test_scale_directo_y_por_herencia() {
use crate::{hit_test_scale, GesturePhase};
let widget = View::<()>::new(Style {
size: Size { width: length(50.0), height: length(50.0) },
..Default::default()
});
let canvas = View::<()>::new(Style {
size: Size { width: length(200.0), height: length(200.0) },
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.on_scale(|_phase: GesturePhase, _f, _fx, _fy| None)
.children(vec![widget]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, canvas);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_scale(&m, &c, 25.0, 25.0), Some(0));
assert_eq!(hit_test_scale(&m, &c, 150.0, 25.0), Some(0));
assert_eq!(hit_test_scale(&m, &c, 350.0, 350.0), None);
}
#[test]
fn hit_test_rotate_directo_y_por_herencia() {
use crate::{hit_test_rotate, GesturePhase};
let widget = View::<()>::new(Style {
size: Size { width: length(50.0), height: length(50.0) },
..Default::default()
});
let canvas = View::<()>::new(Style {
size: Size { width: length(200.0), height: length(200.0) },
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.on_rotate(|_phase: GesturePhase, _d, _fx, _fy| None)
.children(vec![widget]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, canvas);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_rotate(&m, &c, 25.0, 25.0), Some(0));
assert_eq!(hit_test_rotate(&m, &c, 150.0, 25.0), Some(0));
assert_eq!(hit_test_rotate(&m, &c, 350.0, 350.0), None);
}
#[test]
fn hit_test_selectable_solo_sobre_texto_seleccionable() {
use crate::hit_test_selectable;
let label = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(30.0) },
..Default::default()
})
.text("hola", 14.0, vello::peniko::Color::from_rgba8(255, 255, 255, 255))
.selectable(7);
let panel = View::<()>::new(Style {
size: Size { width: length(200.0), height: length(200.0) },
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.children(vec![label]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, panel);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_selectable(&m, &c, 50.0, 15.0), Some(1));
assert_eq!(hit_test_selectable(&m, &c, 150.0, 150.0), None);
}
#[test]
fn hit_test_scroll_chain_devuelve_front_to_back() {
use crate::hit_test_scroll_chain;
let hijo = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.on_scroll(|_dx, _dy| None::<()>);
let padre = View::<()>::new(Style {
size: Size { width: length(200.0), height: length(200.0) },
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.on_scroll(|_dx, _dy| None::<()>)
.children(vec![hijo]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, padre);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
let ch = hit_test_scroll_chain(&m, &c, 50.0, 50.0);
assert_eq!(ch, vec![1, 0]);
let ch = hit_test_scroll_chain(&m, &c, 150.0, 50.0);
assert_eq!(ch, vec![0]);
let ch = hit_test_scroll_chain(&m, &c, 350.0, 350.0);
assert!(ch.is_empty());
}
#[test]
fn hit_test_double_tap_y_long_press() {
use crate::{hit_test_double_tap, hit_test_long_press};
let arriba = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.on_double_tap(());
let abajo = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.on_long_press(());
let root = View::<()>::new(Style {
flex_direction: FlexDirection::Column,
align_items: Some(AlignItems::FlexStart),
justify_content: Some(JustifyContent::FlexStart),
..Default::default()
})
.children(vec![arriba, abajo]);
let mut layout = LayoutTree::new();
let m = mount(&mut layout, root);
let c = layout.compute(m.root, (400.0, 400.0)).expect("layout");
assert_eq!(hit_test_double_tap(&m, &c, 50.0, 50.0), Some(1));
assert_eq!(hit_test_long_press(&m, &c, 50.0, 50.0), None);
assert_eq!(hit_test_long_press(&m, &c, 50.0, 150.0), Some(2));
assert_eq!(hit_test_double_tap(&m, &c, 50.0, 150.0), None);
assert_eq!(hit_test_double_tap(&m, &c, 300.0, 300.0), None);
assert_eq!(hit_test_long_press(&m, &c, 300.0, 300.0), None);
}
}