From 71e5ecf285e1fbfe60e0826bd6824d3ce1b81e89 Mon Sep 17 00:00:00 2001 From: KotovGGG <162052215+KotovGGG@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:39:28 +0300 Subject: [PATCH] test(ui): verify floating frame compatibility and resize constraints --- crates/ui/README.md | 13 +++++ crates/ui/src/docking/chrome.rs | 30 ++++++++++++ crates/ui/src/docking/host_hooks_tests.rs | 60 +++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/crates/ui/README.md b/crates/ui/README.md index a039e12f..16bb8d95 100644 --- a/crates/ui/README.md +++ b/crates/ui/README.md @@ -34,6 +34,19 @@ only. `FloatingFrame::resize_axes` defaults to both axes and preserves an enable axis at corners when the other is disabled. A host with its own title bar can disable the separate top gutter through `reserve_top_resize_band(false)`. +Compatibility and target-size audit: before these hooks, resizable frames already +reserved a 4 pt top band. That remains the default; `false` is the new host opt-out. +The unchanged stock edge hit bands are 4 pt thick; corners are at most 12 pt. +They do **not** meet a 24-by-24 logical-pixel target at 1x. This PR preserves +those hit rectangles and does not claim accessibility compliance. Focused +resize handles also support arrow keys (8 pt steps). Enlarging pointer targets +needs separate overlap, title-bar and viewport-edge acceptance checks. + +`chrome::compatibility_tests::defaults_preserve_pre_host_hook_frame_geometry` +locks down the pre-hook hit rectangles/defaults. `host_hooks_tests` exercises all +four axis settings with actual corner and side pointer drags, minimum/maximum +sizes, the viewport edge, and top-gutter opt-out geometry. + Exhaustive `DockStyle` struct literals need `dock_gap: None`; constructors such as `from_ui` and `from_visuals` supply the compatible default. diff --git a/crates/ui/src/docking/chrome.rs b/crates/ui/src/docking/chrome.rs index 4a2b6da7..3841c256 100644 --- a/crates/ui/src/docking/chrome.rs +++ b/crates/ui/src/docking/chrome.rs @@ -520,3 +520,33 @@ pub fn live_move(ctx: &Context, bar: &Response, rect: Rect, bounds: Rect) -> Opt forget(); None } + +#[cfg(test)] +mod compatibility_tests { + use super::*; + + #[test] + fn defaults_preserve_pre_host_hook_frame_geometry() { + let rect = Rect::from_min_size(pos2(100.0, 100.0), vec2(300.0, 200.0)); + let frame = FloatingFrame::new(Id::unique("defaults"), rect); + assert_eq!(frame.resize_axes, [true, true]); + assert!(frame.reserve_top_resize_band); + assert!(frame.resizable); + // Literal pre-PR hit rectangles: preserve these rather than silently enlarging them. + let expected = [ + [388.0, 288.0, 400.0, 300.0], + [100.0, 112.0, 104.0, 288.0], + [396.0, 112.0, 400.0, 288.0], + [112.0, 296.0, 388.0, 300.0], + [100.0, 288.0, 112.0, 300.0], + [112.0, 100.0, 388.0, 104.0], + [100.0, 100.0, 112.0, 104.0], + [388.0, 100.0, 400.0, 104.0], + ]; + for ((_, hit, _), [x0, y0, x1, y1]) in handles(rect).into_iter().zip(expected) { + assert_eq!(hit, Rect::from_min_max(pos2(x0, y0), pos2(x1, y1))); + // Audit result: legacy targets do NOT meet a 24-by-24 logical-pixel target. + assert!(hit.width() < 24.0 || hit.height() < 24.0); + } + } +} diff --git a/crates/ui/src/docking/host_hooks_tests.rs b/crates/ui/src/docking/host_hooks_tests.rs index ba6cb41f..8415b76a 100644 --- a/crates/ui/src/docking/host_hooks_tests.rs +++ b/crates/ui/src/docking/host_hooks_tests.rs @@ -363,3 +363,63 @@ fn owned_minimized_groups_skip_the_trailing_stock_target() { assert_eq!(group_drop, !owned, "owned={owned}: {:?}", out.actions); } } + +#[test] +fn disabled_edge_axes_ignore_pointer_drags_and_enabled_edges_obey_limits() { + for axes in [[true, true], [true, false], [false, true], [false, false]] { + for horizontal in [true, false] { + for grow in [true, false] { + let ctx = Context::default(); + let rect = Rect::from_min_size(pos2(400.0, 300.0), vec2(300.0, 200.0)); + let bounds = Rect::from_min_max(pos2(0.0, 0.0), pos2(720.0, 520.0)); + let frame = |events| { + let mut resized = None; + ctx.run_ui(input(events), |ui| { + let style = chrome::ChromeStyle::from_ui(ui); + resized = chrome::FloatingFrame::new(Id::unique("edge-axis"), rect) + .resize_axes(axes) + .bounds(bounds) + .limits(PanelLimits { min: vec2(160.0, 120.0), max: vec2(310.0, 210.0) }) + .show(ui.ctx(), &style, |_, _| ()) + .resized; + }) + .drop_without_applying_deltas(); + resized + }; + frame(vec![]); + frame(vec![]); + let from = if horizontal { pos2(rect.right() - 2.0, rect.center().y) } else { pos2(rect.center().x, rect.bottom() - 2.0) }; + frame(vec![Event::PointerMoved(from), button(from, true)]); + let delta = if horizontal { vec2(10.0, 0.0) } else { vec2(0.0, 10.0) }; + frame(vec![Event::PointerMoved(from + delta)]); + let result = frame(vec![Event::PointerMoved(from + delta * if grow { 100.0 } else { -100.0 })]); + let enabled = if horizontal { axes[0] } else { axes[1] }; + let expected = if horizontal { vec2(if grow { 310.0 } else { 160.0 }, 200.0) } else { vec2(300.0, if grow { 210.0 } else { 120.0 }) }; + assert_eq!(result, enabled.then(|| Rect::from_min_size(rect.min, expected)), "{axes:?} horizontal={horizontal} grow={grow}"); + } + } + } +} + +#[test] +fn frame_at_viewport_edge_cannot_resize_beyond_it() { + let ctx = Context::default(); + let rect = Rect::from_min_max(pos2(400.0, 300.0), pos2(800.0, 600.0)); + let frame = |events| { + let mut result = None; + ctx.run_ui(input(events), |ui| { + let style = chrome::ChromeStyle::from_ui(ui); + let response = chrome::FloatingFrame::new(Id::unique("viewport-edge"), rect).show(ui.ctx(), &style, |_, _| ()); + assert_eq!(response.rect, rect); + result = response.resized; + }) + .drop_without_applying_deltas(); + result + }; + frame(vec![]); + frame(vec![]); + let from = rect.max - vec2(2.0, 2.0); + frame(vec![Event::PointerMoved(from), button(from, true)]); + frame(vec![Event::PointerMoved(from + vec2(10.0, 10.0))]); + assert_eq!(frame(vec![Event::PointerMoved(from + vec2(100.0, 100.0))]), None); +}