use std::time::{Duration, Instant};
use smithay::reexports::wayland_server::Display;
use crate::backend::interface::TtyBackendHandle;
use crate::compositor::interaction::{HitNode, PointerState, ResizeHandle};
use crate::compositor::root::Halley;
use super::handles::{commit_handle_from_drag, weights_from_handle};
use super::{
active_node_screen_rect, advance_resize_anim, begin_resize, finalize_resize,
handle_resize_motion, resize_rect_nearly_eq,
};
use crate::input::pointer::button::ButtonFrame;
fn single_monitor_tiling_tuning() -> halley_config::RuntimeTuning {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.cluster_default_layout = halley_config::ClusterDefaultLayout::Tiling;
tuning.tty_viewports = vec![halley_config::ViewportOutputConfig {
connector: "monitor_a".to_string(),
enabled: true,
offset_x: 0,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
}];
tuning
}
fn resize_button_frame() -> ButtonFrame {
resize_button_frame_at(400.0, 300.0)
}
fn resize_button_frame_at(sx: f32, sy: f32) -> ButtonFrame {
ButtonFrame {
ws_w: 800,
ws_h: 600,
global_sx: sx,
global_sy: sy,
sx,
sy,
world_now: halley_core::field::Vec2 { x: sx, y: sy },
workspace_active: false,
}
}
fn nodes_overlap(
state: &Halley,
a: halley_core::field::NodeId,
b: halley_core::field::NodeId,
) -> bool {
let a_node = state.model.field.node(a).expect("a node");
let b_node = state.model.field.node(b).expect("b node");
let ea = state.collision_extents_for_node(a_node);
let eb = state.collision_extents_for_node(b_node);
let gap = state.non_overlap_gap_world();
let req_x = state.required_sep_x(a_node.pos.x, ea, b_node.pos.x, eb, gap);
let req_y = state.required_sep_y(a_node.pos.y, ea, b_node.pos.y, eb, gap);
(a_node.pos.x - b_node.pos.x).abs() < req_x && (a_node.pos.y - b_node.pos.y).abs() < req_y
}
#[test]
fn drag_direction_maps_to_y_down_resize_handles() {
assert_eq!(commit_handle_from_drag(0.0, -40.0), ResizeHandle::Top);
assert_eq!(commit_handle_from_drag(0.0, 40.0), ResizeHandle::Bottom);
assert_eq!(commit_handle_from_drag(40.0, -40.0), ResizeHandle::TopRight);
assert_eq!(
commit_handle_from_drag(-40.0, 40.0),
ResizeHandle::BottomLeft
);
}
#[test]
fn top_and_bottom_weights_follow_screen_space_motion() {
assert_eq!(weights_from_handle(ResizeHandle::Top), (0.0, 0.0, 1.0, 0.0));
assert_eq!(
weights_from_handle(ResizeHandle::Bottom),
(0.0, 0.0, 0.0, 1.0)
);
assert_eq!(
weights_from_handle(ResizeHandle::TopLeft),
(1.0, 0.0, 1.0, 0.0)
);
assert_eq!(
weights_from_handle(ResizeHandle::BottomRight),
(0.0, 1.0, 0.0, 1.0)
);
}
#[test]
fn begin_resize_blocks_active_tiled_workspace_members() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, single_monitor_tiling_tuning());
let backend = TtyBackendHandle::new(800, 600);
let master = st.model.field.spawn_surface(
"master",
halley_core::field::Vec2 { x: 120.0, y: 120.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
let stack = st.model.field.spawn_surface(
"stack",
halley_core::field::Vec2 { x: 520.0, y: 120.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
for id in [master, stack] {
st.assign_node_to_monitor(id, "monitor_a");
}
let cid = st.create_cluster(vec![master, stack]).expect("cluster");
let core = st.collapse_cluster(cid).expect("core");
st.assign_node_to_monitor(core, "monitor_a");
assert!(st.enter_cluster_workspace_by_core(core, "monitor_a", Instant::now()));
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: master,
move_surface: false,
is_core: false,
},
resize_button_frame(),
);
assert!(ps.resize.is_none());
assert!(st.input.interaction_state.resize_active.is_none());
}
#[test]
fn begin_resize_allows_non_tiled_active_windows() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, single_monitor_tiling_tuning());
let backend = TtyBackendHandle::new(800, 600);
let window = st.model.field.spawn_surface(
"window",
halley_core::field::Vec2 { x: 300.0, y: 220.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, "monitor_a");
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: window,
move_surface: false,
is_core: false,
},
resize_button_frame(),
);
assert!(ps.resize.is_some());
assert_eq!(st.input.interaction_state.resize_active, Some(window));
}
#[test]
fn begin_resize_blocks_pinned_active_windows() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, single_monitor_tiling_tuning());
let backend = TtyBackendHandle::new(800, 600);
let window = st.model.field.spawn_surface(
"window",
halley_core::field::Vec2 { x: 300.0, y: 220.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, "monitor_a");
assert!(st.set_node_user_pinned(window, true));
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: window,
move_surface: false,
is_core: false,
},
resize_button_frame(),
);
assert!(ps.resize.is_none());
assert_eq!(st.input.interaction_state.resize_active, None);
assert!(st.node_user_pinned(window));
}
#[test]
fn interactive_resize_moves_overlapped_unpinned_landmark() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut tuning = single_monitor_tiling_tuning();
tuning.animations.smooth_resize.enabled = false;
let mut st = Halley::new_for_test(&dh, tuning);
let backend = TtyBackendHandle::new(800, 600);
let window = st.model.field.spawn_surface(
"window",
halley_core::field::Vec2 { x: 300.0, y: 220.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, "monitor_a");
let rect =
active_node_screen_rect(&st, 800, 600, window, Instant::now(), None).expect("window rect");
let center_y = (rect.1 + rect.3) * 0.5;
let landmark_pos = crate::spatial::screen_to_world(&st, 800, 600, rect.2 + 80.0, center_y);
let landmark = st.model.field.spawn_surface(
"landmark",
landmark_pos,
halley_core::field::Vec2 { x: 160.0, y: 120.0 },
);
st.assign_node_to_monitor(landmark, "monitor_a");
let _ = st
.model
.field
.set_state(landmark, halley_core::field::NodeState::Node);
let landmark_before = st.model.field.node(landmark).expect("landmark").pos;
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: window,
move_surface: false,
is_core: false,
},
resize_button_frame_at(rect.2 - 2.0, center_y),
);
assert!(ps.resize.is_some());
assert!(handle_resize_motion(
&mut st,
&mut ps,
800,
600,
rect.2 + 420.0,
center_y,
&backend,
));
assert_eq!(
st.model.field.node(landmark).expect("landmark").pos,
landmark_before
);
finalize_resize(&mut st, &mut ps, &backend);
assert_ne!(
st.model.field.node(landmark).expect("landmark").pos,
landmark_before
);
assert!(
st.ui
.render_state
.landmark_slide_animations
.contains_key(&landmark)
);
assert!(!nodes_overlap(&st, window, landmark));
}
#[test]
fn interactive_resize_does_not_move_pinned_landmark() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut tuning = single_monitor_tiling_tuning();
tuning.animations.smooth_resize.enabled = false;
let mut st = Halley::new_for_test(&dh, tuning);
let backend = TtyBackendHandle::new(800, 600);
let window = st.model.field.spawn_surface(
"window",
halley_core::field::Vec2 { x: 300.0, y: 220.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, "monitor_a");
let rect =
active_node_screen_rect(&st, 800, 600, window, Instant::now(), None).expect("window rect");
let center_y = (rect.1 + rect.3) * 0.5;
let landmark_pos = crate::spatial::screen_to_world(&st, 800, 600, rect.2 + 80.0, center_y);
let landmark = st.model.field.spawn_surface(
"landmark",
landmark_pos,
halley_core::field::Vec2 { x: 160.0, y: 120.0 },
);
st.assign_node_to_monitor(landmark, "monitor_a");
let _ = st
.model
.field
.set_state(landmark, halley_core::field::NodeState::Node);
assert!(st.set_node_user_pinned(landmark, true));
let landmark_before = st.model.field.node(landmark).expect("landmark").pos;
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: window,
move_surface: false,
is_core: false,
},
resize_button_frame_at(rect.2 - 2.0, center_y),
);
assert!(ps.resize.is_some());
assert!(handle_resize_motion(
&mut st,
&mut ps,
800,
600,
rect.2 + 420.0,
center_y,
&backend,
));
assert_eq!(
st.model.field.node(landmark).expect("landmark").pos,
landmark_before
);
finalize_resize(&mut st, &mut ps, &backend);
assert_eq!(
st.model.field.node(landmark).expect("landmark").pos,
landmark_before
);
}
#[test]
fn smooth_resize_continues_advancing_across_quick_pointer_updates() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut tuning = single_monitor_tiling_tuning();
tuning.animations.smooth_resize.enabled = true;
tuning.animations.smooth_resize.duration_ms = 400;
let mut st = Halley::new_for_test(&dh, tuning);
let backend = TtyBackendHandle::new(800, 600);
let window = st.model.field.spawn_surface(
"window",
halley_core::field::Vec2 { x: 300.0, y: 220.0 },
halley_core::field::Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, "monitor_a");
let mut ps = PointerState::default();
begin_resize(
&mut st,
&mut ps,
&backend,
HitNode {
node_id: window,
move_surface: false,
is_core: false,
},
resize_button_frame(),
);
assert!(handle_resize_motion(
&mut st, &mut ps, 800, 600, 520.0, 380.0, &backend,
));
let first = ps.resize.expect("resize in progress");
assert!(
!resize_rect_nearly_eq(first.preview_right_px, first.target_right_px)
|| !resize_rect_nearly_eq(first.preview_bottom_px, first.target_bottom_px),
"smooth resize should lag behind the cursor-driven target while dragging"
);
let first_preview_right = first.preview_right_px;
let first_target_right = first.target_right_px;
let tick_one_at = first.last_smooth_tick_at + Duration::from_millis(16);
let ticked = advance_resize_anim(&mut st, &mut ps, tick_one_at).expect("resize tick one");
assert_eq!(ticked, window);
let after_tick_one = ps.resize.expect("resize after first tick");
assert!(
after_tick_one.preview_right_px > first_preview_right + 0.1,
"preview should move continuously toward the target during drag"
);
assert!(after_tick_one.preview_right_px < after_tick_one.target_right_px);
assert!(handle_resize_motion(
&mut st, &mut ps, 800, 600, 620.0, 460.0, &backend,
));
let second = ps.resize.expect("resize after second pointer update");
assert!(
second.target_right_px > first_target_right,
"second pointer update should move the target farther out"
);
assert!(
second.preview_right_px >= after_tick_one.preview_right_px - 0.5,
"preview should not reset or jump backward on rapid pointer updates"
);
let tick_two_at = second.last_smooth_tick_at + Duration::from_millis(16);
let ticked = advance_resize_anim(&mut st, &mut ps, tick_two_at).expect("resize tick two");
assert_eq!(ticked, window);
let after_tick_two = ps.resize.expect("resize after second tick");
assert!(
after_tick_two.preview_right_px > second.preview_right_px + 0.1,
"preview should keep advancing instead of hesitating after repeated quick motion"
);
assert!(after_tick_two.preview_right_px < after_tick_two.target_right_px);
let release_preview_right = after_tick_two.preview_right_px;
let release_target_right = after_tick_two.target_right_px;
finalize_resize(&mut st, &mut ps, &backend);
assert!(
ps.resize.is_none(),
"resize should stop immediately at release instead of entering a settle animation"
);
let final_rect = active_node_screen_rect(&st, 800, 600, window, Instant::now(), None)
.expect("final window rect");
assert!(
final_rect.2 < release_target_right - 8.0,
"release should not finish the full trajectory to the old cursor target"
);
assert!(
final_rect.2 >= release_preview_right - 4.0,
"release should stop near the current preview instead of snapping backward"
);
assert!(
st.input.interaction_state.resize_active.is_none(),
"resize interaction should end immediately on release"
);
}