use std::collections::HashSet;
use std::time::Instant;
use halley_core::decay::DecayLevel;
use halley_core::field::{NodeId, Vec2, Visibility};
use smithay::reexports::wayland_server::{
Resource, backend::ObjectId, protocol::wl_surface::WlSurface,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::ToplevelSurface;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::compositor::activity::CommitActivity;
use crate::compositor::ctx::SurfaceLifecycleCtx;
use crate::compositor::root::Halley;
use crate::compositor::spawn::rules::InitialWindowIntent;
mod cleanup;
mod surface;
use cleanup::{
arm_queued_overflow_promotion, capture_queued_overflow_promotion, drop_surface_impl,
};
use surface::{
committed_window_geometry, ensure_node_for_surface_impl,
exit_monitor_fullscreen_for_new_toplevel, note_commit, refresh_node_identity_for_surface,
should_join_active_cluster_layout, surface_tree_root,
};
const CLUSTER_OVERFLOW_PROMOTION_ANIM_MS: u64 = 360;
pub(crate) fn refresh_surface_identity(
ctx: &mut SurfaceLifecycleCtx<'_>,
surface: &WlSurface,
fallback_label: &str,
) {
refresh_node_identity_for_surface(ctx.st, surface, fallback_label);
}
pub(crate) fn on_surface_commit(
ctx: &mut SurfaceLifecycleCtx<'_>,
surface: &WlSurface,
now: Instant,
) {
note_commit(ctx.st, surface, now);
}
pub(crate) fn ensure_node_for_surface(
ctx: &mut SurfaceLifecycleCtx<'_>,
surface: &WlSurface,
label: &str,
size_px: (i32, i32),
intent: &InitialWindowIntent,
) -> NodeId {
ensure_node_for_surface_impl(ctx.st, surface, label, size_px, intent)
}
#[allow(dead_code)]
pub(crate) fn drop_surface(ctx: &mut SurfaceLifecycleCtx<'_>, surface: &WlSurface) {
drop_surface_impl(ctx.st, surface);
}
pub(crate) fn on_toplevel_destroyed(ctx: &mut SurfaceLifecycleCtx<'_>, surface: ToplevelSurface) {
let st = &mut ctx.st;
let key = surface.wl_surface().id();
let closing_id = st.model.surface_to_node.get(&key).copied();
let had_keyboard_focus = st
.platform
.seat
.get_keyboard()
.and_then(|kb| kb.current_focus())
.is_some_and(|focused| surface_tree_root(&focused).id() == key);
let had_pointer_focus = st
.platform
.seat
.get_pointer()
.and_then(|ptr| ptr.current_focus())
.is_some_and(|focused| surface_tree_root(&focused).id() == key);
let focused_monitor = st
.model
.surface_to_node
.get(&key)
.and_then(|id| st.model.monitor_state.node_monitor.get(id))
.cloned();
if had_keyboard_focus || had_pointer_focus {
eventline::debug!(
"toplevel_destroyed with active focus (keyboard={} pointer={}); scheduling input state reset",
had_keyboard_focus,
had_pointer_focus
);
st.input.interaction_state.reset_input_state_requested = true;
if let Some(ref focused_monitor) = focused_monitor {
st.model.spawn_state.pending_spawn_monitor = Some(focused_monitor.clone());
eventline::debug!(
"pending spawn monitor latched from destroyed toplevel: {}",
focused_monitor
);
}
}
if had_keyboard_focus {
st.clear_keyboard_focus();
}
if had_keyboard_focus
&& st.runtime.tuning.close_restore_focus
&& let (Some(closing_id), Some(focused_monitor)) = (closing_id, focused_monitor.as_deref())
{
let now = Instant::now();
let suppress_restore_pan = st.node_has_overlap_policy(closing_id);
if let Some(cid) = st.active_cluster_workspace_for_monitor(focused_monitor) {
if matches!(
st.runtime.tuning.cluster_layout_kind(),
halley_core::cluster_layout::ClusterWorkspaceLayoutKind::Tiling
) {
} else {
let mut next_to_focus = None;
if let Some(cluster) = st.model.field.cluster(cid) {
let members = cluster.members();
if let Some(pos) = members.iter().position(|&id| id == closing_id) {
if pos + 1 < members.len() {
next_to_focus = Some(members[pos + 1]);
} else if pos > 0 {
next_to_focus = Some(members[pos - 1]);
}
}
}
if let Some(next) = next_to_focus {
st.set_interaction_focus(Some(next), 30_000, now);
} else if let Some(previous) =
st.previous_window_from_trail_on_close(focused_monitor, closing_id)
{
st.set_interaction_focus(Some(previous), 30_000, now);
} else if let Some(fallback) = st
.last_focused_surface_node_for_monitor(focused_monitor)
.filter(|&id| id != closing_id)
{
st.set_interaction_focus(Some(fallback), 30_000, now);
}
}
} else if let Some(previous) =
st.previous_window_from_trail_on_close(focused_monitor, closing_id)
{
let _ = st.restore_focus_to_node_after_close(
focused_monitor,
previous,
now,
suppress_restore_pan,
);
} else if let Some(fallback) = st
.last_focused_surface_node_for_monitor(focused_monitor)
.filter(|&id| id != closing_id)
.or_else(|| {
st.last_focused_surface_node()
.filter(|&id| id != closing_id)
})
{
let _ = st.restore_focus_to_node_after_close(
focused_monitor,
fallback,
now,
suppress_restore_pan,
);
}
} else if had_keyboard_focus
&& !st.runtime.tuning.close_restore_focus
&& let Some(focused_monitor) = focused_monitor.as_deref()
{
st.model
.focus_state
.blocked_monitor_focus_restore
.insert(focused_monitor.to_string());
}
if had_pointer_focus {
crate::compositor::interaction::pointer::clear_pointer_focus(st);
}
drop_surface_impl(st, surface.wl_surface());
}
pub(crate) fn reconcile_surface_bindings(st: &mut Halley) {
cleanup::reconcile_surface_bindings(st);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compositor::spawn::rules::{InitialWindowIntent, ResolvedInitialWindowRule};
fn single_monitor_tuning() -> halley_config::RuntimeTuning {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.cluster_default_layout = halley_config::ClusterDefaultLayout::Tiling;
tuning.tile_max_stack = 2;
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
}
#[test]
fn committed_window_geometry_prefers_xdg_geometry_size() {
let (geometry, size) =
committed_window_geometry((4, 6), (1200, 920), Some((12, 18, 840, 620)));
assert_eq!(geometry, (12.0, 18.0, 840.0, 620.0));
assert_eq!(size, Vec2 { x: 840.0, y: 620.0 });
}
#[test]
fn committed_window_geometry_falls_back_to_bbox_size() {
let (geometry, size) = committed_window_geometry((4, 6), (1200, 920), None);
assert_eq!(geometry, (4.0, 6.0, 1200.0, 920.0));
assert_eq!(
size,
Vec2 {
x: 1200.0,
y: 920.0
}
);
}
#[test]
fn new_toplevel_on_fullscreen_monitor_exits_only_that_monitor_fullscreen() {
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 fullscreen_left = state.model.field.spawn_surface(
"fullscreen-left",
Vec2 { x: 400.0, y: 300.0 },
Vec2 { x: 200.0, y: 140.0 },
);
let fullscreen_right = state.model.field.spawn_surface(
"fullscreen-right",
Vec2 {
x: 1200.0,
y: 300.0,
},
Vec2 { x: 200.0, y: 140.0 },
);
state.assign_node_to_monitor(fullscreen_left, "left");
state.assign_node_to_monitor(fullscreen_right, "right");
state
.model
.fullscreen_state
.fullscreen_active_node
.insert("left".to_string(), fullscreen_left);
state
.model
.fullscreen_state
.fullscreen_active_node
.insert("right".to_string(), fullscreen_right);
exit_monitor_fullscreen_for_new_toplevel(&mut state, "left", Instant::now());
assert!(
!state
.model
.fullscreen_state
.fullscreen_active_node
.contains_key("left")
);
assert_eq!(
state
.model
.fullscreen_state
.fullscreen_active_node
.get("right"),
Some(&fullscreen_right)
);
}
#[test]
fn tiled_cluster_layout_participation_honors_layout_and_float() {
let layout_intent = InitialWindowIntent {
app_id: Some("firefox".to_string()),
title: None,
parent_node: None,
rule: ResolvedInitialWindowRule {
overlap_policy: halley_config::InitialWindowOverlapPolicy::None,
spawn_placement: halley_config::InitialWindowSpawnPlacement::Adjacent,
cluster_participation: halley_config::InitialWindowClusterParticipation::Layout,
},
matched_rule: true,
is_transient: false,
prefer_app_intent: false,
};
let float_intent = InitialWindowIntent {
rule: ResolvedInitialWindowRule {
cluster_participation: halley_config::InitialWindowClusterParticipation::Float,
..layout_intent.rule
},
..layout_intent.clone()
};
assert!(should_join_active_cluster_layout(
true,
false,
&layout_intent
));
assert!(!should_join_active_cluster_layout(
true,
false,
&float_intent
));
assert!(!should_join_active_cluster_layout(
true,
true,
&layout_intent
));
}
#[test]
fn queued_tiled_promotion_after_close_preserves_existing_focus() {
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, single_monitor_tuning());
let master = state.model.field.spawn_surface(
"master",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let stack = state.model.field.spawn_surface(
"stack",
Vec2 { x: 500.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let stack_b = state.model.field.spawn_surface(
"stack-b",
Vec2 { x: 500.0, y: 250.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let queued = state.model.field.spawn_surface(
"queued",
Vec2 { x: 500.0, y: 400.0 },
Vec2 { x: 320.0, y: 240.0 },
);
for id in [master, stack, stack_b, queued] {
state.assign_node_to_monitor(id, "monitor_a");
state
.ui
.render_state
.cache
.window_geometry
.insert(id, (0.0, 0.0, 320.0, 240.0));
}
let cid = state
.model
.field
.create_cluster(vec![master, stack, stack_b, queued])
.expect("cluster");
let core = state.model.field.collapse_cluster(cid).expect("core");
state.assign_node_to_monitor(core, "monitor_a");
let now = Instant::now();
let now_ms = state.now_ms(now);
assert!(state.enter_cluster_workspace_by_core(core, "monitor_a", now));
state.set_interaction_focus(Some(stack), 30_000, now);
let promotion = capture_queued_overflow_promotion(&state, master).expect("promotion");
assert!(state.remove_node_from_field(master, now_ms));
arm_queued_overflow_promotion(&mut state, promotion, now_ms);
assert!(
state
.model
.spawn_state
.pending_tiled_insert_preserve_focus
.contains(&queued)
);
crate::compositor::spawn::state::process_pending_spawn_activations(
&mut state,
now + std::time::Duration::from_millis(CLUSTER_OVERFLOW_PROMOTION_ANIM_MS),
now_ms.saturating_add(CLUSTER_OVERFLOW_PROMOTION_ANIM_MS),
);
assert_eq!(
state.model.focus_state.primary_interaction_focus,
Some(stack)
);
assert!(
!state
.model
.spawn_state
.pending_tiled_insert_reveal_at_ms
.contains_key(&queued)
);
let queued_node = state.model.field.node(queued).expect("queued node");
assert!(!queued_node.visibility.has(Visibility::DETACHED));
assert!(!queued_node.visibility.has(Visibility::HIDDEN_BY_CLUSTER));
assert!(
!state
.model
.spawn_state
.pending_tiled_insert_preserve_focus
.contains(&queued)
);
}
#[test]
fn stacking_cluster_close_restore_focus_to_next_member() {
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut tuning = single_monitor_tuning();
tuning.cluster_default_layout = halley_config::ClusterDefaultLayout::Stacking;
let mut state = Halley::new_for_test(&dh, tuning);
let a = state.model.field.spawn_surface(
"A",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let b = state.model.field.spawn_surface(
"B",
Vec2 { x: 120.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
for id in [a, b] {
state.assign_node_to_monitor(id, "monitor_a");
}
let cid = state
.model
.field
.create_cluster(vec![a, b])
.expect("cluster");
let core = state.model.field.collapse_cluster(cid).expect("core");
state.assign_node_to_monitor(core, "monitor_a");
let now = Instant::now();
assert!(state.enter_cluster_workspace_by_core(core, "monitor_a", now));
state.set_interaction_focus(Some(a), 30_000, now);
assert_eq!(state.model.focus_state.primary_interaction_focus, Some(a));
let focused_monitor = "monitor_a";
let closing_id = a;
let next_to_focus =
if let Some(cid) = state.active_cluster_workspace_for_monitor(focused_monitor) {
if !matches!(
state.runtime.tuning.cluster_layout_kind(),
halley_core::cluster_layout::ClusterWorkspaceLayoutKind::Tiling
) {
let mut next = None;
if let Some(cluster) = state.model.field.cluster(cid) {
let members = cluster.members();
if let Some(pos) = members.iter().position(|&id| id == closing_id) {
if pos + 1 < members.len() {
next = Some(members[pos + 1]);
} else if pos > 0 {
next = Some(members[pos - 1]);
}
}
}
next
} else {
None
}
} else {
None
};
assert_eq!(next_to_focus, Some(b));
}
}