use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::time::Instant;
mod placement;
use eventline::debug;
use halley_config::{InitialWindowOverlapPolicy, InitialWindowSpawnPlacement, PanToNewMode};
use halley_core::decay::DecayLevel;
use halley_core::field::{NodeId, Vec2};
use halley_core::viewport::Viewport;
use smithay::desktop::utils::bbox_from_surface_tree;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use crate::compositor::ctx::SpawnCtx;
use crate::compositor::focus::state::FocusState;
use crate::compositor::monitor::camera::camera_controller;
use crate::compositor::monitor::state::MonitorState;
use crate::compositor::overlap::system::CollisionExtents;
use crate::compositor::root::Halley;
use crate::compositor::spawn::read;
use crate::compositor::spawn::read::RevealNewToplevelPlan;
use crate::compositor::spawn::rules::{InitialWindowIntent, ResolvedInitialWindowRule};
use crate::compositor::spawn::state::{MonitorSpawnState, SpawnState};
use crate::window::active_window_frame_pad_px;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct InitialToplevelSize {
pub(crate) node_size: (i32, i32),
pub(crate) configure_size: Option<(i32, i32)>,
}
fn detected_initial_toplevel_size(toplevel: &ToplevelSurface) -> Option<(i32, i32)> {
let wl = toplevel.wl_surface();
let min_size = with_states(wl, |states| {
let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current();
(state.min_size.w, state.min_size.h)
});
let geometry = with_states(wl, |states| {
states
.cached_state
.get::<SurfaceCachedState>()
.current()
.geometry
});
if let Some(geometry) = geometry {
return Some((
geometry.size.w.max(min_size.0).max(96),
geometry.size.h.max(min_size.1).max(72),
));
}
if let Some(size) = toplevel.current_state().size {
return Some((
size.w.max(min_size.0).max(96),
size.h.max(min_size.1).max(72),
));
}
let bbox = bbox_from_surface_tree(wl, (0, 0));
if bbox.size.w > 0 && bbox.size.h > 0 {
return Some((
bbox.size.w.max(min_size.0).max(96),
bbox.size.h.max(min_size.1).max(72),
));
}
None
}
pub(crate) fn initial_toplevel_size(
ctx: &mut SpawnCtx<'_>,
toplevel: &ToplevelSurface,
intent: &InitialWindowIntent,
) -> InitialToplevelSize {
let st = &ctx.st;
let defer_rule_resolution =
crate::compositor::spawn::rules::needs_deferred_rule_recheck(st, intent);
let predicted_monitor = st.spawn_target_monitor_for_intent(intent);
let stack_mode_open = st
.model
.cluster_state
.cluster_bloom_open
.contains_key(predicted_monitor.as_str());
if !defer_rule_resolution
&& !stack_mode_open
&& intent.rule.cluster_participation
== halley_config::InitialWindowClusterParticipation::Layout
&& let Some(cid) = st.active_cluster_workspace_for_monitor(predicted_monitor.as_str())
&& let Some(rect) = st.cluster_spawn_rect_for_new_member(predicted_monitor.as_str(), cid)
{
let width = rect.w.max(64.0).round() as i32;
let height = rect.h.max(64.0).round() as i32;
return InitialToplevelSize {
node_size: (width, height),
configure_size: Some((width, height)),
};
}
let detected = detected_initial_toplevel_size(toplevel);
let node_size = detected.unwrap_or_else(|| {
(
(st.model.viewport.size.x * 0.46).round() as i32,
(st.model.viewport.size.y * 0.42).round() as i32,
)
});
InitialToplevelSize {
node_size,
configure_size: None,
}
}
pub(crate) fn reveal_new_toplevel_node(
ctx: &mut SpawnCtx<'_>,
id: NodeId,
is_transient: bool,
now: Instant,
) {
ctx.st.reveal_new_toplevel_node(id, is_transient, now);
}
pub(crate) struct SpawnRevealController<T> {
st: T,
}
pub(crate) fn spawn_reveal_controller<T>(st: T) -> SpawnRevealController<T> {
SpawnRevealController { st }
}
impl<T: Deref<Target = Halley>> Deref for SpawnRevealController<T> {
type Target = Halley;
fn deref(&self) -> &Self::Target {
self.st.deref()
}
}
impl<T: DerefMut<Target = Halley>> DerefMut for SpawnRevealController<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.st.deref_mut()
}
}
impl<T: DerefMut<Target = Halley>> SpawnRevealController<T> {
pub(crate) fn maybe_start_pending_spawn_pan(&mut self, now: Instant) {
if self.model.spawn_state.active_spawn_pan.is_some() {
return;
}
let now_ms = self.now_ms(now);
while let Some(next) = self.model.spawn_state.pending_spawn_pan_queue.pop_front() {
if self.model.field.node(next.node_id).is_none() {
continue;
}
let did_pan = self.animate_viewport_center_to_delayed(
next.target_center,
now,
Halley::VIEWPORT_PAN_PRELOAD_MS,
);
self.model.spawn_state.active_spawn_pan =
Some(crate::compositor::spawn::state::ActiveSpawnPan {
node_id: next.node_id,
pan_start_at_ms: now_ms.saturating_add(if did_pan {
Halley::VIEWPORT_PAN_PRELOAD_MS
} else {
0
}),
reveal_at_ms: now_ms.saturating_add(if did_pan {
Halley::VIEWPORT_PAN_PRELOAD_MS + Halley::VIEWPORT_PAN_DURATION_MS
} else {
0
}),
});
break;
}
}
pub(crate) fn tick_pending_spawn_pan(&mut self, now: Instant, now_ms: u64) {
let Some(active) = self.model.spawn_state.active_spawn_pan else {
self.maybe_start_pending_spawn_pan(now);
return;
};
if self.model.field.node(active.node_id).is_none() {
self.model.spawn_state.active_spawn_pan = None;
self.maybe_start_pending_spawn_pan(now);
return;
}
let pan_finished = now_ms >= active.reveal_at_ms
|| (now_ms >= active.pan_start_at_ms
&& self.input.interaction_state.viewport_pan_anim.is_none());
if !pan_finished {
return;
}
let _ = self.model.field.set_detached(active.node_id, false);
let _ = self
.model
.field
.set_decay_level(active.node_id, DecayLevel::Hot);
let intrinsic_size = self
.model
.field
.node(active.node_id)
.map(|node| node.intrinsic_size);
if let Some(intrinsic_size) = intrinsic_size {
self.model
.workspace_state
.last_active_size
.insert(active.node_id, intrinsic_size);
}
let duration_ms = self.runtime.tuning.window_open_duration_ms();
if self.runtime.tuning.window_open_animation_enabled() {
crate::compositor::workspace::state::mark_active_transition(
&mut **self,
active.node_id,
now,
duration_ms,
);
}
self.record_focus_trail_visit(active.node_id);
self.model.focus_state.suppress_trail_record_once = true;
self.set_interaction_focus(Some(active.node_id), 30_000, now);
self.model.spawn_state.active_spawn_pan = None;
self.maybe_start_pending_spawn_pan(now);
}
pub(crate) fn reveal_new_toplevel_node(
&mut self,
id: NodeId,
is_transient: bool,
now: Instant,
) {
let node_monitor = self
.model
.monitor_state
.node_monitor
.get(&id)
.cloned()
.unwrap_or_else(|| self.model.monitor_state.current_monitor.clone());
let cluster_local = self
.model
.field
.cluster_id_for_member_public(id)
.is_some_and(|cid| {
self.active_cluster_workspace_for_monitor(node_monitor.as_str()) == Some(cid)
});
if self
.model
.spawn_state
.pending_tiled_insert_reveal_at_ms
.contains_key(&id)
{
return;
}
if cluster_local {
let _ = self.model.field.set_detached(id, false);
self.set_recent_top_node(id, now + std::time::Duration::from_millis(1200));
self.set_interaction_focus(Some(id), 30_000, now);
self.model
.spawn_state
.pending_spawn_activate_at_ms
.remove(&id);
let duration_ms = self.runtime.tuning.window_open_duration_ms();
if self.runtime.tuning.window_open_animation_enabled() {
crate::compositor::workspace::state::mark_active_transition(
&mut **self,
id,
now,
duration_ms,
);
}
return;
}
if self.model.spawn_state.pending_initial_reveal.contains(&id) {
return;
}
if self
.model
.spawn_state
.applied_window_rules
.get(&id)
.is_some_and(|rule| rule.suppress_reveal_pan)
{
let _ = self.model.field.set_detached(id, false);
self.set_recent_top_node(id, now + std::time::Duration::from_millis(1200));
self.record_focus_trail_visit(id);
self.model.focus_state.suppress_trail_record_once = true;
self.set_interaction_focus(Some(id), 30_000, now);
self.model
.spawn_state
.pending_spawn_activate_at_ms
.remove(&id);
let duration_ms = self.runtime.tuning.window_open_duration_ms();
if self.runtime.tuning.window_open_animation_enabled() {
crate::compositor::workspace::state::mark_active_transition(
&mut **self,
id,
now,
duration_ms,
);
}
return;
}
match self.resolve_spawn_reveal_plan(id, is_transient) {
RevealNewToplevelPlan::AlreadyQueued => {}
RevealNewToplevelPlan::ActivateNow => {
let _ = self.model.field.set_detached(id, false);
self.record_focus_trail_visit(id);
self.model.focus_state.suppress_trail_record_once = true;
self.set_interaction_focus(Some(id), 30_000, now);
self.model
.spawn_state
.pending_spawn_activate_at_ms
.remove(&id);
let duration_ms = self.runtime.tuning.window_open_duration_ms();
if self.runtime.tuning.window_open_animation_enabled() {
crate::compositor::workspace::state::mark_active_transition(
&mut **self,
id,
now,
duration_ms,
);
}
}
RevealNewToplevelPlan::QueuePan { target_center } => {
let _ = self.model.field.set_detached(id, true);
self.model
.spawn_state
.pending_spawn_activate_at_ms
.remove(&id);
self.model.spawn_state.pending_spawn_pan_queue.push_back(
crate::compositor::spawn::state::PendingSpawnPan {
node_id: id,
target_center,
},
);
self.maybe_start_pending_spawn_pan(now);
}
}
}
fn resolve_spawn_reveal_plan(
&self,
id: NodeId,
is_transient: bool,
) -> read::RevealNewToplevelPlan {
read::spawn_read_context(self).reveal_new_toplevel_plan(self, id, is_transient)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compositor::spawn::rules::{InitialWindowIntent, ResolvedInitialWindowRule};
fn test_intent(
overlap_policy: InitialWindowOverlapPolicy,
spawn_placement: InitialWindowSpawnPlacement,
parent_node: Option<NodeId>,
) -> InitialWindowIntent {
InitialWindowIntent {
app_id: Some("firefox".to_string()),
title: None,
parent_node,
rule: ResolvedInitialWindowRule {
overlap_policy,
spawn_placement,
cluster_participation: halley_config::InitialWindowClusterParticipation::Layout,
},
matched_rule: true,
is_transient: parent_node.is_some(),
prefer_app_intent: matches!(spawn_placement, InitialWindowSpawnPlacement::App),
}
}
#[test]
fn star_offsets_are_center_then_right_left_up_down() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let state = Halley::new_for_test(&dh, tuning);
let offsets = state.star_candidate_offsets(Vec2 { x: 100.0, y: 80.0 });
assert_eq!(offsets[0], Vec2 { x: 0.0, y: 0.0 });
let step_x = state.spawn_star_step_x(Vec2 { x: 100.0, y: 80.0 });
let step_y = state.spawn_star_step_y(Vec2 { x: 100.0, y: 80.0 });
assert_eq!(offsets[1], Vec2 { x: step_x, y: 0.0 });
assert_eq!(offsets[2], Vec2 { x: -step_x, y: 0.0 });
assert_eq!(offsets[3], Vec2 { x: 0.0, y: -step_y });
assert_eq!(offsets[4], Vec2 { x: 0.0, y: step_y });
}
#[test]
fn first_spawn_in_star_is_center() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 0.0, y: 0.0 };
state.model.viewport.size = Vec2 {
x: 1600.0,
y: 1200.0,
};
let (_, pos, needs_pan) = state.pick_spawn_position(Vec2 { x: 100.0, y: 80.0 });
assert_eq!(pos, Vec2 { x: 0.0, y: 0.0 });
assert!(!needs_pan);
}
#[test]
fn second_spawn_uses_first_available_star_slot() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 0.0, y: 0.0 };
state.model.viewport.size = Vec2 {
x: 1600.0,
y: 1200.0,
};
let size = Vec2 { x: 100.0, y: 80.0 };
let first = state
.model
.field
.spawn_surface("first", Vec2 { x: 0.0, y: 0.0 }, size);
let _ = state
.model
.field
.set_state(first, halley_core::field::NodeState::Active);
state
.model
.focus_state
.last_surface_focus_ms
.insert(first, 1);
state.model.focus_state.primary_interaction_focus = Some(first);
state.assign_node_to_current_monitor(first);
let current_monitor = state.model.monitor_state.current_monitor.clone();
state.update_spawn_patch(
current_monitor.as_str(),
Vec2 { x: 0.0, y: 0.0 },
Some(first),
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
);
let (_, pos, needs_pan) = state.pick_spawn_position(size);
let expected = state
.right_spawn_candidate_for_focus(first, size)
.expect("right spawn candidate");
assert_eq!(pos, expected);
assert!(!needs_pan);
}
#[test]
fn current_spawn_focus_keeps_focused_window_anchor() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 1200.0, y: 0.0 };
state.model.viewport.size = Vec2 { x: 800.0, y: 600.0 };
let focused = state.model.field.spawn_surface(
"focused",
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 100.0, y: 80.0 },
);
state
.model
.focus_state
.last_surface_focus_ms
.insert(focused, 1);
state.model.focus_state.primary_interaction_focus = Some(focused);
state.assign_node_to_current_monitor(focused);
state
.model
.focus_state
.monitor_focus
.insert(state.model.monitor_state.current_monitor.clone(), focused);
assert_eq!(
state.current_spawn_focus(state.model.monitor_state.current_monitor.as_str()),
(Some(focused), Vec2 { x: 0.0, y: 0.0 })
);
}
#[test]
fn view_mode_spawns_near_viewport_center_without_pan() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 1200.0, y: 0.0 };
state.model.viewport.size = Vec2 { x: 800.0, y: 600.0 };
let focused = state.model.field.spawn_surface(
"focused",
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 100.0, y: 80.0 },
);
let _ = state
.model
.field
.set_state(focused, halley_core::field::NodeState::Active);
state
.model
.focus_state
.last_surface_focus_ms
.insert(focused, 1);
state.model.focus_state.primary_interaction_focus = Some(focused);
state.assign_node_to_current_monitor(focused);
{
let current_monitor = state.model.monitor_state.current_monitor.clone();
let viewport_center = state.model.viewport.center;
let spawn = state.spawn_monitor_state_mut(current_monitor.as_str());
spawn.spawn_anchor_mode = crate::compositor::spawn::state::SpawnAnchorMode::View;
spawn.spawn_view_anchor = viewport_center;
}
let (_, pos, needs_pan) = state.pick_spawn_position(Vec2 { x: 100.0, y: 80.0 });
assert!(!needs_pan);
assert_eq!(pos, state.model.viewport.center);
}
#[test]
fn focus_mode_uses_next_free_neighbor_around_last_focus() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 500.0, y: 0.0 };
state.model.viewport.size = Vec2 { x: 800.0, y: 600.0 };
let focused = state.model.field.spawn_surface(
"focused",
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 120.0, y: 90.0 },
);
let _ = state
.model
.field
.set_state(focused, halley_core::field::NodeState::Active);
state
.model
.focus_state
.last_surface_focus_ms
.insert(focused, 1);
state.model.focus_state.primary_interaction_focus = Some(focused);
state.assign_node_to_current_monitor(focused);
state
.model
.focus_state
.monitor_focus
.insert(state.model.monitor_state.current_monitor.clone(), focused);
let current_monitor = state.model.monitor_state.current_monitor.clone();
state.update_spawn_patch(
current_monitor.as_str(),
Vec2 { x: 0.0, y: 0.0 },
Some(focused),
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
);
let size = Vec2 { x: 120.0, y: 90.0 };
let existing = state
.model
.field
.spawn_surface("existing", Vec2 { x: 143.0, y: 0.0 }, size);
state.assign_node_to_current_monitor(existing);
let (_, pos, needs_pan) = state.pick_spawn_position(size);
assert_eq!(pos, Vec2 { x: -143.0, y: 0.0 });
assert!(!needs_pan);
}
#[test]
fn view_mode_continues_local_build_up_around_new_area() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 1200.0, y: 0.0 };
state.model.viewport.size = Vec2 { x: 800.0, y: 600.0 };
{
let current_monitor = state.model.monitor_state.current_monitor.clone();
let viewport_center = state.model.viewport.center;
let spawn = state.spawn_monitor_state_mut(current_monitor.as_str());
spawn.spawn_anchor_mode = crate::compositor::spawn::state::SpawnAnchorMode::View;
spawn.spawn_view_anchor = viewport_center;
}
let size = Vec2 { x: 100.0, y: 80.0 };
let first = state.pick_spawn_position(size).1;
let first_id = state.model.field.spawn_surface("first", first, size);
state.assign_node_to_current_monitor(first_id);
let second = state.pick_spawn_position(size).1;
let step = state.spawn_star_step(size);
assert_eq!(first, Vec2 { x: 1200.0, y: 0.0 });
assert_eq!(
second,
Vec2 {
x: 1200.0 + step,
y: 0.0
}
);
}
#[test]
fn focused_monitor_drives_spawn_even_when_current_monitor_differs() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let _ = state.activate_monitor("left");
let focused = state.model.field.spawn_surface(
"focused",
Vec2 {
x: 1200.0,
y: 300.0,
},
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(focused, "right");
state.set_interaction_focus(Some(focused), 30_000, Instant::now());
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
let expected = state
.right_spawn_candidate_for_focus(focused, Vec2 { x: 120.0, y: 90.0 })
.expect("right spawn candidate");
assert_eq!(monitor, "right");
assert_eq!(pos, expected);
}
#[test]
fn hover_focus_mode_uses_empty_pointer_monitor_for_spawn_target() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.input.focus_mode = halley_config::InputFocusMode::Hover;
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let focused = state.model.field.spawn_surface(
"focused",
Vec2 {
x: 1200.0,
y: 300.0,
},
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(focused, "right");
state.set_interaction_focus(Some(focused), 30_000, Instant::now());
state.input.interaction_state.last_pointer_screen_global = Some((200.0, 120.0));
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
assert_eq!(state.resolve_spawn_target_monitor(), "left");
assert_eq!(monitor, "left");
assert_eq!(pos, state.viewport_center_for_monitor("left"));
}
#[test]
fn monitor_local_last_input_beats_stale_monitor_focus_for_spawn_anchor() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let stale = state.model.field.spawn_surface(
"stale",
Vec2 {
x: 1040.0,
y: 300.0,
},
Vec2 { x: 120.0, y: 90.0 },
);
let latest = state.model.field.spawn_surface(
"latest",
Vec2 {
x: 1320.0,
y: 300.0,
},
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(stale, "right");
state.assign_node_to_monitor(latest, "right");
state
.model
.focus_state
.monitor_focus
.insert("right".to_string(), stale);
state
.model
.focus_state
.last_surface_focus_ms
.insert(stale, 1);
state
.model
.focus_state
.last_surface_focus_ms
.insert(latest, 2);
state.set_interaction_monitor("right");
state.set_focused_monitor("right");
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
let expected = state
.right_spawn_candidate_for_focus(latest, Vec2 { x: 120.0, y: 90.0 })
.expect("right spawn candidate");
assert_eq!(monitor, "right");
assert_eq!(pos, expected);
}
#[test]
fn spawn_buildup_stays_isolated_per_monitor() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let size = Vec2 { x: 100.0, y: 80.0 };
let step = state.spawn_star_step(size);
let _ = state.activate_monitor("left");
state.set_interaction_monitor("left");
state.set_focused_monitor("left");
{
let spawn = state.spawn_monitor_state_mut("left");
spawn.spawn_anchor_mode = crate::compositor::spawn::state::SpawnAnchorMode::View;
spawn.spawn_view_anchor = Vec2 { x: 400.0, y: 300.0 };
}
let first_left = state.pick_spawn_position(size).1;
let left_id = state.model.field.spawn_surface("left-1", first_left, size);
state.assign_node_to_monitor(left_id, "left");
let _ = state.activate_monitor("right");
state.set_interaction_monitor("right");
state.set_focused_monitor("right");
{
let spawn = state.spawn_monitor_state_mut("right");
spawn.spawn_anchor_mode = crate::compositor::spawn::state::SpawnAnchorMode::View;
spawn.spawn_view_anchor = Vec2 {
x: 1200.0,
y: 300.0,
};
}
let first_right = state.pick_spawn_position(size).1;
let right_id = state
.model
.field
.spawn_surface("right-1", first_right, size);
state.assign_node_to_monitor(right_id, "right");
let _ = state.activate_monitor("left");
state.set_interaction_monitor("left");
state.set_focused_monitor("left");
let second_left = state.pick_spawn_position(size).1;
assert_eq!(first_left, Vec2 { x: 400.0, y: 300.0 });
assert_eq!(
first_right,
Vec2 {
x: 1200.0,
y: 300.0
}
);
assert_eq!(
second_left,
Vec2 {
x: 400.0 + step,
y: 300.0,
}
);
}
#[test]
fn focus_mode_keeps_monitor_local_patch_after_auto_focusing_new_spawn() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 0.0, y: 0.0 };
state.model.viewport.size = Vec2 {
x: 1600.0,
y: 1200.0,
};
let size = Vec2 { x: 120.0, y: 90.0 };
let anchor = state
.model
.field
.spawn_surface("anchor", Vec2 { x: 0.0, y: 0.0 }, size);
let _ = state
.model
.field
.set_state(anchor, halley_core::field::NodeState::Active);
state.assign_node_to_current_monitor(anchor);
state.model.focus_state.primary_interaction_focus = Some(anchor);
state
.model
.focus_state
.last_surface_focus_ms
.insert(anchor, 1);
state
.model
.focus_state
.monitor_focus
.insert(state.model.monitor_state.current_monitor.clone(), anchor);
let first = state.pick_spawn_position(size).1;
let first_id = state.model.field.spawn_surface("first", first, size);
state.assign_node_to_current_monitor(first_id);
state.set_interaction_focus(Some(first_id), 30_000, Instant::now());
let second = state.pick_spawn_position(size).1;
let first_expected = state
.right_spawn_candidate_for_focus(anchor, size)
.expect("right spawn candidate");
let second_expected = state
.right_spawn_candidate_for_focus(first_id, size)
.expect("right spawn candidate");
assert_eq!(first, first_expected);
assert_eq!(second, second_expected);
}
#[test]
fn focus_monitor_view_switches_spawn_to_clicked_monitor() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let left = state.model.field.spawn_surface(
"left",
Vec2 { x: 400.0, y: 300.0 },
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(left, "left");
state.set_interaction_focus(Some(left), 30_000, Instant::now());
state.focus_monitor_view("right", Instant::now());
assert_eq!(state.interaction_monitor(), "right");
assert_eq!(state.focused_monitor(), "right");
assert_eq!(state.model.focus_state.primary_interaction_focus, None);
assert_eq!(
state.spawn_monitor_state("right").spawn_anchor_mode,
crate::compositor::spawn::state::SpawnAnchorMode::View
);
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
assert_eq!(monitor, "right");
assert_eq!(
pos,
Vec2 {
x: 1200.0,
y: 300.0
}
);
}
#[test]
fn focused_monitor_beats_interaction_monitor_drift_for_spawn_target() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let left = state.model.field.spawn_surface(
"left",
Vec2 { x: 400.0, y: 300.0 },
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(left, "left");
state.set_interaction_focus(Some(left), 30_000, Instant::now());
state.focus_monitor_view("right", Instant::now());
state.set_interaction_monitor("left");
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
assert_eq!(state.focused_monitor(), "right");
assert_eq!(monitor, "right");
assert_eq!(
pos,
Vec2 {
x: 1200.0,
y: 300.0
}
);
}
#[test]
fn focused_monitor_beats_stale_primary_focus_monitor_for_spawn_target() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let left = state.model.field.spawn_surface(
"left",
Vec2 { x: 400.0, y: 300.0 },
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(left, "left");
state.model.focus_state.primary_interaction_focus = Some(left);
state
.model
.focus_state
.last_surface_focus_ms
.insert(left, 1);
state.focus_monitor_view("right", Instant::now());
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
assert_eq!(monitor, "right");
assert_eq!(
pos,
Vec2 {
x: 1200.0,
y: 300.0
}
);
}
#[test]
fn pending_spawn_monitor_beats_focus_churn_for_next_toplevel() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let left = state.model.field.spawn_surface(
"left",
Vec2 { x: 400.0, y: 300.0 },
Vec2 { x: 120.0, y: 90.0 },
);
state.assign_node_to_monitor(left, "left");
state.set_interaction_focus(Some(left), 30_000, Instant::now());
state.model.spawn_state.pending_spawn_monitor = Some("right".to_string());
let (monitor, pos, _) = state.pick_spawn_position(Vec2 { x: 120.0, y: 90.0 });
assert_eq!(monitor, "right");
assert_eq!(
pos,
Vec2 {
x: 1200.0,
y: 300.0
}
);
assert!(state.model.spawn_state.pending_spawn_monitor.is_none());
}
#[test]
fn shorter_secondary_keeps_building_to_the_right_offscreen() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".to_string(),
enabled: true,
offset_x: 0,
offset_y: 0,
width: 2560,
height: 1440,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 2560,
offset_y: 0,
width: 1920,
height: 1200,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.focus_monitor_view("right", Instant::now());
let size = Vec2 { x: 883.0, y: 504.0 };
let first = state.pick_spawn_position(size).1;
let first_id = state.model.field.spawn_surface("first", first, size);
state.assign_node_to_monitor(first_id, "right");
state.set_interaction_focus(Some(first_id), 30_000, Instant::now());
let second = state.pick_spawn_position(size).1;
let second_id = state.model.field.spawn_surface("second", second, size);
state.assign_node_to_monitor(second_id, "right");
state.set_interaction_focus(Some(second_id), 30_000, Instant::now());
let third = state.pick_spawn_position(size).1;
let second_expected = state
.right_spawn_candidate_for_focus(first_id, size)
.expect("right spawn candidate");
let third_expected = state
.right_spawn_candidate_for_focus(second_id, size)
.expect("right spawn candidate");
assert_eq!(
first,
Vec2 {
x: 3520.0,
y: 600.0
}
);
assert_eq!(second, second_expected);
assert_eq!(third, third_expected);
}
#[test]
fn focus_mode_checks_neighbors_in_right_left_up_down_order() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let size = Vec2 { x: 120.0, y: 90.0 };
let focused = state
.model
.field
.spawn_surface("focused", Vec2 { x: 0.0, y: 0.0 }, size);
state.assign_node_to_current_monitor(focused);
state.set_interaction_focus(Some(focused), 30_000, Instant::now());
let right = state
.spawn_candidate_for_focus_dir(focused, size, Vec2 { x: 1.0, y: 0.0 })
.expect("right");
let left = state
.spawn_candidate_for_focus_dir(focused, size, Vec2 { x: -1.0, y: 0.0 })
.expect("left");
let up = state
.spawn_candidate_for_focus_dir(focused, size, Vec2 { x: 0.0, y: -1.0 })
.expect("up");
let down = state
.spawn_candidate_for_focus_dir(focused, size, Vec2 { x: 0.0, y: 1.0 })
.expect("down");
let right_id = state.model.field.spawn_surface("right", right, size);
let left_id = state.model.field.spawn_surface("left", left, size);
let up_id = state.model.field.spawn_surface("up", up, size);
state.assign_node_to_current_monitor(right_id);
state.assign_node_to_current_monitor(left_id);
state.assign_node_to_current_monitor(up_id);
let chosen = state.pick_spawn_position(size).1;
assert_eq!(chosen, down);
}
#[test]
fn reveal_new_toplevel_skips_pan_when_spawn_is_already_visible() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 700.0, y: 0.0 };
state.model.viewport.size = Vec2 {
x: 1600.0,
y: 1200.0,
};
let id = state.model.field.spawn_surface(
"new",
Vec2 { x: 920.0, y: 0.0 },
Vec2 { x: 100.0, y: 80.0 },
);
state.reveal_new_toplevel_node(id, false, Instant::now());
assert!(state.model.spawn_state.active_spawn_pan.is_none());
assert!(state.model.spawn_state.pending_spawn_pan_queue.is_empty());
assert!(state.input.interaction_state.viewport_pan_anim.is_none());
assert_eq!(state.model.focus_state.primary_interaction_focus, Some(id));
}
#[test]
fn reveal_new_toplevel_pans_when_spawn_is_partially_offscreen() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 700.0, y: 0.0 };
state.model.viewport.size = Vec2 {
x: 1600.0,
y: 1200.0,
};
let id = state.model.field.spawn_surface(
"partial",
Vec2 { x: 1460.0, y: 0.0 },
Vec2 { x: 240.0, y: 160.0 },
);
state.reveal_new_toplevel_node(id, false, Instant::now());
assert_eq!(
state
.model
.spawn_state
.active_spawn_pan
.map(|pan| pan.node_id),
Some(id)
);
}
#[test]
fn reveal_new_toplevel_pans_when_spawn_is_offscreen() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.viewport.center = Vec2 { x: 0.0, y: 0.0 };
state.model.viewport.size = Vec2 { x: 800.0, y: 600.0 };
let id = state.model.field.spawn_surface(
"new",
Vec2 { x: 1200.0, y: 0.0 },
Vec2 { x: 100.0, y: 80.0 },
);
state.reveal_new_toplevel_node(id, false, Instant::now());
assert_eq!(
state
.model
.spawn_state
.active_spawn_pan
.map(|pan| pan.node_id),
Some(id)
);
}
#[test]
fn center_all_can_overlap_parent_anchor_directly() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let size = Vec2 { x: 120.0, y: 90.0 };
let parent = state
.model
.field
.spawn_surface("parent", Vec2 { x: 0.0, y: 0.0 }, size);
let other = state
.model
.field
.spawn_surface("other", Vec2 { x: 0.0, y: 0.0 }, size);
for id in [parent, other] {
state.assign_node_to_current_monitor(id);
let _ = state
.model
.field
.set_state(id, halley_core::field::NodeState::Active);
}
let intent = test_intent(
InitialWindowOverlapPolicy::All,
InitialWindowSpawnPlacement::Center,
Some(parent),
);
let (_, pos, _) = state.pick_spawn_position_with_intent(size, &intent);
assert_eq!(pos, Vec2 { x: 0.0, y: 0.0 });
}
#[test]
fn adjacent_parent_only_avoids_unrelated_windows() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let size = Vec2 { x: 120.0, y: 90.0 };
let parent = state
.model
.field
.spawn_surface("parent", Vec2 { x: 0.0, y: 0.0 }, size);
let blocked = state.model.field.spawn_surface(
"blocked",
state
.spawn_candidate_for_focus_dir(parent, size, Vec2 { x: 1.0, y: 0.0 })
.expect("right candidate"),
size,
);
for id in [parent, blocked] {
state.assign_node_to_current_monitor(id);
let _ = state
.model
.field
.set_state(id, halley_core::field::NodeState::Active);
}
let intent = test_intent(
InitialWindowOverlapPolicy::ParentOnly,
InitialWindowSpawnPlacement::Adjacent,
Some(parent),
);
let (_, pos, _) = state.pick_spawn_position_with_intent(size, &intent);
assert_eq!(
pos,
state
.spawn_candidate_for_focus_dir(parent, size, Vec2 { x: -1.0, y: 0.0 })
.expect("left candidate")
);
}
#[test]
fn adjacent_overlap_on_fullscreen_monitor_anchors_over_fullscreen() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.cluster_default_layout = halley_config::ClusterDefaultLayout::Tiling;
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let fullscreen = state.model.field.spawn_surface(
"fullscreen",
Vec2 { x: 320.0, y: 240.0 },
Vec2 { x: 640.0, y: 480.0 },
);
state.assign_node_to_current_monitor(fullscreen);
let _ = state
.model
.field
.set_state(fullscreen, halley_core::field::NodeState::Active);
state.model.fullscreen_state.fullscreen_active_node.insert(
state.model.monitor_state.current_monitor.clone(),
fullscreen,
);
state.set_interaction_focus(Some(fullscreen), 30_000, std::time::Instant::now());
let intent = test_intent(
InitialWindowOverlapPolicy::All,
InitialWindowSpawnPlacement::Adjacent,
None,
);
let (_, pos, _) =
state.pick_spawn_position_with_intent(Vec2 { x: 220.0, y: 160.0 }, &intent);
assert_eq!(pos, Vec2 { x: 320.0, y: 240.0 });
}
#[test]
fn cursor_placement_uses_pointer_monitor() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".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,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.input.interaction_state.last_pointer_screen_global = Some((900.0, 120.0));
let intent = test_intent(
InitialWindowOverlapPolicy::None,
InitialWindowSpawnPlacement::Cursor,
None,
);
assert_eq!(state.spawn_target_monitor_for_intent(&intent), "right");
}
#[test]
fn pending_initial_reveal_blocks_initial_reveal() {
let tuning = halley_config::RuntimeTuning::default();
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
let id = state.model.field.spawn_surface(
"window",
Vec2 {
x: 5000.0,
y: 5000.0,
},
Vec2 { x: 320.0, y: 240.0 },
);
state.assign_node_to_current_monitor(id);
let _ = state
.model
.field
.set_state(id, halley_core::field::NodeState::Active);
state.model.spawn_state.pending_initial_reveal.insert(id);
state.reveal_new_toplevel_node(id, false, Instant::now());
assert!(state.model.spawn_state.active_spawn_pan.is_none());
assert!(state.model.spawn_state.pending_spawn_pan_queue.is_empty());
assert_ne!(state.model.focus_state.primary_interaction_focus, Some(id));
}
}