use super::*;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Point, Rect};
use crate::widget::active_modal_path;
use crate::widgets::window::{ClickAwayAction, Window};
use crate::Stack;
use std::sync::Arc;
fn unit_scale() {
crate::device_scale::set_device_scale(1.0);
crate::ux_scale::set_ux_scale(1.0);
}
const VP_W: f64 = 420.0;
const VP_H: f64 = 520.0;
struct OffsetHost {
origin: Rect,
bounds: Rect,
children: Vec<Box<dyn Widget>>,
}
impl Widget for OffsetHost {
fn type_name(&self) -> &'static str {
"OffsetHost"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, _b: Rect) {}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, _available: Size) -> Size {
self.bounds = self.origin;
let o = self.origin;
let child = &mut self.children[0];
child.layout(Size::new(o.width, o.height));
Size::new(self.origin.width, self.origin.height)
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
}
fn modal_window_world(app: &App) -> Rect {
let path = active_modal_path(app.root()).expect("a modal dialog must be open");
let mut widget: &dyn Widget = app.root();
let (mut ox, mut oy) = (0.0, 0.0);
for &idx in &path {
let child: &Box<dyn Widget> = &widget.children()[idx];
let b = child.bounds();
ox += b.x;
oy += b.y;
widget = child.as_ref();
}
let b = widget.bounds();
Rect::new(ox, oy, b.width, b.height)
}
fn drag(app: &mut App, from: Point, to: Point) {
app.on_mouse_down(from.x, VP_H - from.y, MouseButton::Left, Modifiers::default());
app.on_mouse_move(to.x, VP_H - to.y);
}
fn end_drag(app: &mut App, at: Point) {
app.on_mouse_up(at.x, VP_H - at.y, MouseButton::Left, Modifiers::default());
}
fn paint_once(app: &mut App) {
app.layout(Size::new(VP_W, VP_H));
let mut fb = Framebuffer::new(VP_W as u32, VP_H as u32);
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
app.paint(&mut ctx);
}
fn clear_snap_registry() {
for (id, _) in crate::snap::targets_snapshot() {
crate::snap::unregister_target(id);
}
}
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 0.5
}
fn build(target: Rect, host_origin: Rect, dlg_local: Rect) -> App {
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let target_content = SizedBox::new().with_width(target.width).with_height(target.height);
let target_win = Window::new("Target", Arc::clone(&font), Box::new(target_content))
.with_bounds(target)
.with_auto_size(false)
.with_resizable(false)
.with_gl_backbuffer(false);
let dlg_content = SizedBox::new().with_width(dlg_local.width).with_height(dlg_local.height);
let dialog = Window::new("Dlg", Arc::clone(&font), Box::new(dlg_content))
.with_bounds(dlg_local)
.with_auto_size(false)
.with_resizable(false)
.with_constrain(true)
.with_gl_backbuffer(false)
.with_modal(true)
.with_click_away(ClickAwayAction::None);
let host = OffsetHost {
origin: host_origin,
bounds: Rect::default(),
children: vec![Box::new(dialog)],
};
let root = Stack::new()
.with_hit_children_only(false)
.add(Box::new(target_win))
.add(Box::new(host));
App::new(Box::new(root))
}
#[test]
fn nested_modal_snaps_in_canvas_absolute_space() {
unit_scale();
crate::snap::set_enabled(true);
clear_snap_registry();
let target = Rect::new(200.0, 200.0, 120.0, 100.0);
let host_origin = Rect::new(30.0, 40.0, 200.0, 200.0);
let dlg_local = Rect::new(20.0, 20.0, 120.0, 150.0);
let mut app = build(target, host_origin, dlg_local);
paint_once(&mut app);
let w0 = modal_window_world(&app);
assert!(approx_eq(w0.x, 50.0) && approx_eq(w0.y, 60.0), "dialog starts at canvas (50,60), got {w0:?}");
let grab = Point::new(w0.x + w0.width * 0.5, w0.y + w0.height - 8.0);
let to = Point::new(grab.x + 153.0, grab.y);
drag(&mut app, grab, to);
let w1 = modal_window_world(&app);
assert!(
approx_eq(w1.x, 200.0),
"the nested dialog's canvas-absolute LEFT edge must snap EXACTLY to the \
target window's left edge (x=200); got {w1:?}. Before the coordinate-space \
fix the snap runs in slot-local space and never engages."
);
end_drag(&mut app, to);
}
#[test]
fn nested_modal_registers_canvas_absolute_target() {
unit_scale();
crate::snap::set_enabled(true);
clear_snap_registry();
let target = Rect::new(200.0, 200.0, 120.0, 100.0);
let host_origin = Rect::new(30.0, 40.0, 200.0, 200.0);
let dlg_local = Rect::new(20.0, 20.0, 120.0, 150.0);
let mut app = build(target, host_origin, dlg_local);
paint_once(&mut app);
app.layout(Size::new(VP_W, VP_H));
let want = modal_window_world(&app); let targets = crate::snap::targets_snapshot();
let found = targets.iter().any(|(_, r)| {
approx_eq(r.x, want.x)
&& approx_eq(r.y, want.y)
&& approx_eq(r.width, want.width)
&& approx_eq(r.height, want.height)
});
assert!(
found,
"the nested dialog must register its CANVAS-ABSOLUTE rect {want:?} as a snap \
target; registry held {targets:?}. Before the fix it registered its \
slot-local rect, corrupting snap targets for every other window."
);
}
#[test]
fn modal_clamp_keeps_dialog_inside_at_both_vertical_edges() {
unit_scale();
crate::snap::set_enabled(false); clear_snap_registry();
let host_origin = Rect::new(30.0, 40.0, 200.0, 200.0);
let dlg_local = Rect::new(20.0, 20.0, 120.0, 150.0);
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let dlg_content = SizedBox::new().with_width(120.0).with_height(150.0);
let dialog = Window::new("Dlg", Arc::clone(&font), Box::new(dlg_content))
.with_bounds(dlg_local)
.with_auto_size(false)
.with_resizable(false)
.with_constrain(true)
.with_gl_backbuffer(false)
.with_modal(true)
.with_click_away(ClickAwayAction::None);
let host = OffsetHost {
origin: host_origin,
bounds: Rect::default(),
children: vec![Box::new(dialog)],
};
let root = Stack::new().with_hit_children_only(false).add(Box::new(host));
let mut app = App::new(Box::new(root));
paint_once(&mut app);
let w = modal_window_world(&app);
let grab = Point::new(w.x + w.width * 0.5, w.y + w.height - 8.0);
drag(&mut app, grab, Point::new(grab.x, -600.0));
end_drag(&mut app, Point::new(grab.x, -600.0));
paint_once(&mut app);
let wb = modal_window_world(&app);
assert!(
wb.y >= -0.5,
"dragging past the BOTTOM edge must clamp the dialog fully on-screen \
(bottom edge y >= 0); got {wb:?}"
);
assert!(
wb.y + wb.height <= VP_H + 0.5,
"the bottom-clamped dialog must not overshoot the top edge; got {wb:?}"
);
let w = modal_window_world(&app);
let grab = Point::new(w.x + w.width * 0.5, w.y + w.height - 8.0);
drag(&mut app, grab, Point::new(grab.x, 1400.0));
end_drag(&mut app, Point::new(grab.x, 1400.0));
paint_once(&mut app);
let wt = modal_window_world(&app);
assert!(
wt.y + wt.height <= VP_H + 0.5,
"dragging past the TOP edge must clamp the dialog fully on-screen \
(top edge y + height <= viewport height); got {wt:?}"
);
assert!(
wt.y >= -0.5,
"the top-clamped dialog must not overshoot the bottom edge; got {wt:?}"
);
}