use super::support::*;
use crate::tree::viewport;
use crate::viewport::{PanBounds, ViewportRequest, ViewportView};
const R: Rect = Rect::new(0.0, 0.0, 400.0, 300.0);
const ORIGIN: (f32, f32) = (0.0, 0.0);
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 0.05, "{a} != {b}");
}
fn vp_tree(w: f32, h: f32) -> El {
viewport([button("x")
.key("box")
.width(Size::Fixed(w))
.height(Size::Fixed(h))])
.key("vp")
}
fn vp_id(tree: &El) -> String {
find_id(tree, "vp").expect("viewport id")
}
#[test]
fn transform_bakes_into_descendant_rects() {
let mut tree = vp_tree(100.0, 80.0);
let mut s = UiState::new();
assign_ids(&mut tree);
layout(&mut tree, &mut s, R); let content = find_rect(&tree, &s, "box").expect("box rect");
let view = ViewportView {
pan: (40.0, 20.0),
zoom: 2.0,
};
s.set_viewport_view(vp_id(&tree), view);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box rect");
let (ex, ey) = view.project((content.x, content.y), ORIGIN);
approx(after.x, ex);
approx(after.y, ey);
approx(after.w, content.w * 2.0);
approx(after.h, content.h * 2.0);
}
#[test]
fn identity_view_leaves_rects_untouched() {
let mut a = vp_tree(120.0, 90.0);
let mut sa = UiState::new();
assign_ids(&mut a);
layout(&mut a, &mut sa, R);
let ra = find_rect(&a, &sa, "box").expect("box");
let mut b = vp_tree(120.0, 90.0);
let mut sb = UiState::new();
assign_ids(&mut b);
layout(&mut b, &mut sb, R);
let rb = find_rect(&b, &sb, "box").expect("box");
approx(ra.x, rb.x);
approx(ra.w, rb.w);
}
#[test]
fn hit_test_follows_the_transform() {
let mut tree = vp_tree(100.0, 80.0);
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (60.0, 30.0),
zoom: 1.5,
},
);
layout(&mut tree, &mut s, R);
let box_rect = find_rect(&tree, &s, "box").expect("box");
let hit = hit_test(&tree, &s, (box_rect.center_x(), box_rect.center_y()));
assert_eq!(hit.as_deref(), Some("box"));
}
#[test]
fn fit_content_frames_and_centers() {
let mut tree = vp_tree(200.0, 100.0);
let mut s = UiState::new();
assign_ids(&mut tree);
s.push_viewport_requests(vec![ViewportRequest::FitContent {
key: "vp".into(),
padding: 20.0,
}]);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
approx(after.w, 200.0 * 1.8);
approx(after.h, 100.0 * 1.8);
approx(after.center_x(), R.center_x());
approx(after.center_y(), R.center_y());
approx(s.viewport_view(&vp_id(&tree)).zoom, 1.8);
}
#[test]
fn reset_view_request_restores_identity() {
let mut tree = vp_tree(100.0, 80.0);
let mut s = UiState::new();
assign_ids(&mut tree);
layout(&mut tree, &mut s, R);
let content = find_rect(&tree, &s, "box").expect("box");
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (90.0, 70.0),
zoom: 3.0,
},
);
s.push_viewport_requests(vec![ViewportRequest::ResetView { key: "vp".into() }]);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
approx(after.x, content.x);
approx(after.w, content.w);
let v = s.viewport_view(&vp_id(&tree));
approx(v.zoom, 1.0);
approx(v.pan.0, 0.0);
}
#[test]
fn pan_is_clamped_so_oversized_content_cannot_leave_a_gutter() {
let mut tree = vp_tree(800.0, 600.0);
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (1000.0, 1000.0),
zoom: 1.0,
},
);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
assert!(after.x <= 0.05, "left edge not clamped: {}", after.x);
assert!(
after.right() >= R.right() - 0.05,
"right edge not clamped: {}",
after.right()
);
assert!(after.y <= 0.05, "top edge not clamped: {}", after.y);
assert!(after.bottom() >= R.bottom() - 0.05);
}
#[test]
fn center_bounds_let_any_node_reach_mid_frame() {
let mut tree = vp_tree(800.0, 600.0).pan_bounds(PanBounds::Center);
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (5000.0, 5000.0),
zoom: 1.0,
},
);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
approx(after.x, R.center_x());
approx(after.y, R.center_y());
}
#[test]
fn free_bounds_leave_pan_unclamped() {
let mut tree = vp_tree(800.0, 600.0).pan_bounds(PanBounds::Free);
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (5000.0, 5000.0),
zoom: 1.0,
},
);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
approx(after.x, 5000.0);
approx(after.y, 5000.0);
}
#[test]
fn smaller_content_is_kept_inside_the_viewport() {
let mut tree = vp_tree(100.0, 80.0);
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (-9000.0, -9000.0),
zoom: 1.0,
},
);
layout(&mut tree, &mut s, R);
let after = find_rect(&tree, &s, "box").expect("box");
assert!(after.x >= -0.05, "left: {}", after.x);
assert!(
after.right() <= R.right() + 0.05,
"right: {}",
after.right()
);
}
#[test]
fn wheel_zoom_is_cursor_anchored() {
let mut tree = vp_tree(200.0, 150.0);
let mut s = UiState::new();
assign_ids(&mut tree);
layout(&mut tree, &mut s, R);
let id = vp_id(&tree);
let cursor = (300.0, 220.0);
let before = s.viewport_view(&id).unproject(cursor, ORIGIN);
assert!(s.viewport_wheel_zoom(cursor.0, cursor.1, -1.0));
let v = s.viewport_view(&id);
assert!(v.zoom > 1.0, "zoom should increase, was {}", v.zoom);
let after = v.unproject(cursor, ORIGIN);
approx(before.0, after.0);
approx(before.1, after.1);
}
#[test]
fn paint_scales_descendant_font_size_and_line_height_with_zoom() {
use crate::ir::DrawOp;
use crate::paint::draw_ops::draw_ops;
let mut tree = viewport([crate::text("zoom me")
.font_size(20.0)
.line_height(28.0)
.key("label")])
.key("vp");
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (0.0, 0.0),
zoom: 0.5,
},
);
layout(&mut tree, &mut s, R);
let label_id = find_id(&tree, "label").expect("label id");
let glyph = draw_ops(&tree, &s).into_iter().find_map(|op| match op {
DrawOp::GlyphRun {
id,
size,
line_height,
..
} if id == label_id => Some((size, line_height)),
_ => None,
});
let (size, line_height) = glyph.expect("glyph run for label");
approx(size, 10.0);
approx(line_height, 14.0);
}
#[test]
fn paint_scales_attributed_paragraph_with_zoom() {
use crate::ir::DrawOp;
use crate::paint::draw_ops::draw_ops;
use crate::tree::text_runs;
let mut tree = viewport([text_runs([
crate::text("hello ").font_size(20.0).line_height(28.0),
crate::text("world").font_size(20.0).line_height(28.0),
])
.key("para")])
.key("vp");
let mut s = UiState::new();
assign_ids(&mut tree);
s.set_viewport_view(
vp_id(&tree),
ViewportView {
pan: (0.0, 0.0),
zoom: 0.5,
},
);
layout(&mut tree, &mut s, R);
let para_id = find_id(&tree, "para").expect("para id");
let metrics = draw_ops(&tree, &s).into_iter().find_map(|op| match op {
DrawOp::AttributedText {
id,
size,
line_height,
..
} if id == para_id => Some((size, line_height)),
_ => None,
});
let (size, line_height) = metrics.expect("attributed text for para");
approx(size, 10.0);
approx(line_height, 14.0);
}
fn hover_envelope(tree: &mut El, state: &mut UiState, key: &str) -> Option<f32> {
state.set_animation_mode(AnimationMode::Settled);
state.hovered = Some(target(tree, state, key));
state.apply_to_state();
state.tick_visual_animations(tree, Instant::now(), &Palette::default());
envelope_for(tree, state, key, EnvelopeKind::Hover)
}
#[test]
fn keyed_node_tracks_hover_envelope_by_default() {
let mut tree = column([button("x").key("normal")]);
let mut s = UiState::new();
layout(&mut tree, &mut s, R);
assert_eq!(hover_envelope(&mut tree, &mut s, "normal"), Some(1.0));
}
#[test]
fn no_hover_suppresses_the_hover_envelope() {
let mut tree = column([button("x").key("quiet").no_hover()]);
let mut s = UiState::new();
layout(&mut tree, &mut s, R);
assert_eq!(hover_envelope(&mut tree, &mut s, "quiet"), Some(0.0));
}
#[test]
fn viewport_does_not_track_hover_envelope() {
let mut tree = viewport([button("x").key("box")]).key("vp");
let mut s = UiState::new();
layout(&mut tree, &mut s, R);
assert_eq!(hover_envelope(&mut tree, &mut s, "vp"), Some(0.0));
}
#[test]
fn wheel_zoom_clamps_to_max() {
let mut tree = vp_tree(100.0, 100.0).max_zoom(2.0);
let mut s = UiState::new();
assign_ids(&mut tree);
layout(&mut tree, &mut s, R);
let id = vp_id(&tree);
for _ in 0..50 {
s.viewport_wheel_zoom(200.0, 150.0, -1.0);
}
approx(s.viewport_view(&id).zoom, 2.0);
}