use std::collections::HashMap;
use halley_core::cluster_layout::{ClusterWorkspaceLayoutKind, cluster_visible_limit};
use halley_core::field::NodeId;
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
use smithay::wayland::compositor::get_parent;
use crate::compositor::root::Halley;
pub(crate) fn is_active_cluster_workspace_member(
st: &Halley,
node_id: halley_core::field::NodeId,
) -> bool {
st.model
.field
.cluster_id_for_member_public(node_id)
.zip(st.model.monitor_state.node_monitor.get(&node_id))
.is_some_and(|(cid, monitor)| st.active_cluster_workspace_for_monitor(monitor) == Some(cid))
}
pub(crate) fn active_stacking_front_member_for_monitor(
st: &Halley,
monitor: &str,
) -> Option<halley_core::field::NodeId> {
if !matches!(
st.runtime.tuning.cluster_layout_kind(),
ClusterWorkspaceLayoutKind::Stacking
) {
return None;
}
let cid = st.active_cluster_workspace_for_monitor(monitor)?;
st.model.field.cluster(cid)?.members().first().copied()
}
pub(crate) fn active_stacking_visible_members_for_monitor(
st: &Halley,
monitor: &str,
) -> Vec<halley_core::field::NodeId> {
if !matches!(
st.runtime.tuning.cluster_layout_kind(),
ClusterWorkspaceLayoutKind::Stacking
) {
return Vec::new();
}
let Some(cid) = st.active_cluster_workspace_for_monitor(monitor) else {
return Vec::new();
};
let Some(cluster) = st.model.field.cluster(cid) else {
return Vec::new();
};
let visible_len = cluster_visible_limit(
ClusterWorkspaceLayoutKind::Stacking,
st.runtime.tuning.active_cluster_visible_limit(),
)
.min(cluster.members().len());
cluster
.members()
.iter()
.take(visible_len)
.copied()
.collect()
}
pub(crate) fn is_active_stacking_workspace_member(
st: &Halley,
node_id: halley_core::field::NodeId,
) -> bool {
let Some(monitor) = st.model.monitor_state.node_monitor.get(&node_id) else {
return false;
};
active_stacking_visible_members_for_monitor(st, monitor.as_str()).contains(&node_id)
}
pub(crate) fn node_allows_interactive_resize(st: &Halley, node_id: NodeId) -> bool {
st.model
.field
.node(node_id)
.is_some_and(|node| node.state == halley_core::field::NodeState::Active)
&& !node_blocks_interactive_transform(st, node_id)
&& !crate::compositor::workspace::state::node_in_maximize_session(st, node_id)
&& !is_active_stacking_workspace_member(st, node_id)
&& !(matches!(
st.runtime.tuning.cluster_layout_kind(),
ClusterWorkspaceLayoutKind::Tiling
) && is_active_cluster_workspace_member(st, node_id))
}
pub(crate) fn node_blocks_interactive_transform(st: &Halley, node_id: NodeId) -> bool {
st.model.field.node(node_id).is_some_and(|node| node.pinned)
|| st.fullscreen_monitor_for_node(node_id).is_some()
|| active_pointer_constraint_belongs_to_node(st, node_id)
|| (crate::window::node_is_game_like(st, node_id)
&& node_covers_assigned_output(st, node_id))
}
fn active_pointer_constraint_belongs_to_node(st: &Halley, node_id: NodeId) -> bool {
crate::compositor::interaction::pointer::active_constrained_pointer_surface(st)
.and_then(|(surface, _)| root_surface_node_id(st, &surface))
== Some(node_id)
}
fn root_surface_node_id(st: &Halley, surface: &WlSurface) -> Option<NodeId> {
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
st.model.surface_to_node.get(&root.id()).copied()
}
fn node_covers_assigned_output(st: &Halley, node_id: NodeId) -> bool {
let Some(node) = st.model.field.node(node_id) else {
return false;
};
if node.kind != halley_core::field::NodeKind::Surface || !st.model.field.is_visible(node_id) {
return false;
}
let Some(monitor_name) = st.model.monitor_state.node_monitor.get(&node_id) else {
return false;
};
let Some(space) = st.model.monitor_state.monitors.get(monitor_name.as_str()) else {
return false;
};
let tolerance = 4.0;
let output_size = space.viewport.size;
let output_center = space.viewport.center;
let size = node.footprint;
size.x >= output_size.x - tolerance
&& size.y >= output_size.y - tolerance
&& (node.pos.x - output_center.x).abs() <= tolerance
&& (node.pos.y - output_center.y).abs() <= tolerance
}
pub(crate) fn stacking_render_order_map(
members: &[halley_core::field::NodeId],
max_visible: usize,
) -> HashMap<halley_core::field::NodeId, usize> {
let visible_len =
cluster_visible_limit(ClusterWorkspaceLayoutKind::Stacking, max_visible).min(members.len());
members
.iter()
.take(visible_len)
.enumerate()
.map(|(index, &node_id)| (node_id, visible_len.saturating_sub(index + 1)))
.collect()
}
pub(crate) fn active_stacking_render_order_for_monitor(
st: &Halley,
monitor: &str,
) -> HashMap<halley_core::field::NodeId, usize> {
if !matches!(
st.runtime.tuning.cluster_layout_kind(),
ClusterWorkspaceLayoutKind::Stacking
) {
return HashMap::new();
}
let Some(cid) = st.active_cluster_workspace_for_monitor(monitor) else {
return HashMap::new();
};
let Some(cluster) = st.model.field.cluster(cid) else {
return HashMap::new();
};
stacking_render_order_map(
cluster.members(),
st.runtime.tuning.active_cluster_visible_limit(),
)
}
pub(crate) fn stack_focus_target_for_node(
st: &Halley,
node_id: halley_core::field::NodeId,
) -> Option<halley_core::field::NodeId> {
let monitor = st.model.monitor_state.node_monitor.get(&node_id)?;
let cid = st.model.field.cluster_id_for_member_public(node_id)?;
(st.active_cluster_workspace_for_monitor(monitor.as_str()) == Some(cid))
.then(|| active_stacking_front_member_for_monitor(st, monitor.as_str()))
.flatten()
}
#[cfg(test)]
mod tests {
use super::*;
use halley_core::field::Vec2;
use smithay::reexports::wayland_server::Display;
use std::time::Instant;
#[test]
fn active_cluster_workspace_member_matches_current_monitor_workspace() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
let monitor = st.model.monitor_state.current_monitor.clone();
let master = st.model.field.spawn_surface(
"master",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let stack = st.model.field.spawn_surface(
"stack",
Vec2 { x: 500.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(master, monitor.as_str());
st.assign_node_to_monitor(stack, monitor.as_str());
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.as_str());
assert!(!is_active_cluster_workspace_member(&st, master));
assert!(st.toggle_cluster_workspace_by_core(core, Instant::now()));
assert!(is_active_cluster_workspace_member(&st, master));
assert!(is_active_cluster_workspace_member(&st, stack));
assert!(!is_active_cluster_workspace_member(&st, core));
}
#[test]
fn active_stacking_helpers_report_front_member_and_render_order() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut tuning = halley_config::RuntimeTuning::default();
tuning.stacking_max_visible = 3;
let mut st = Halley::new_for_test(&dh, tuning);
let monitor = st.model.monitor_state.current_monitor.clone();
let a = st.model.field.spawn_surface(
"A",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let b = st.model.field.spawn_surface(
"B",
Vec2 { x: 120.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let c = st.model.field.spawn_surface(
"C",
Vec2 { x: 140.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
let d = st.model.field.spawn_surface(
"D",
Vec2 { x: 160.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
for id in [a, b, c, d] {
st.assign_node_to_monitor(id, monitor.as_str());
}
let cid = st.create_cluster(vec![a, b, c, d]).expect("cluster");
let core = st.collapse_cluster(cid).expect("core");
st.assign_node_to_monitor(core, monitor.as_str());
assert!(st.toggle_cluster_workspace_by_core(core, Instant::now()));
assert_eq!(
active_stacking_front_member_for_monitor(&st, monitor.as_str()),
Some(a)
);
let ranks = active_stacking_render_order_for_monitor(&st, monitor.as_str());
assert_eq!(ranks.get(&a), Some(&2));
assert_eq!(ranks.get(&b), Some(&1));
assert_eq!(ranks.get(&c), Some(&0));
assert_eq!(ranks.get(&d), None);
assert_eq!(stack_focus_target_for_node(&st, c), Some(a));
}
#[test]
fn maximize_session_blocks_interactive_resize() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut tuning = halley_config::RuntimeTuning::default();
tuning.animations.maximize.enabled = false;
let mut st = Halley::new_for_test(&dh, tuning);
let monitor = st.model.monitor_state.current_monitor.clone();
let window = st.model.field.spawn_surface(
"window",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, monitor.as_str());
assert!(
crate::compositor::actions::window::toggle_node_maximize_state(
&mut st,
window,
Instant::now(),
monitor.as_str(),
)
);
assert!(!node_allows_interactive_resize(&st, window));
}
#[test]
fn output_sized_game_like_surface_blocks_interactive_resize() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
let monitor = st.model.monitor_state.current_monitor.clone();
let space = st
.model
.monitor_state
.monitors
.get(monitor.as_str())
.expect("monitor")
.clone();
let window = st.model.field.spawn_surface(
"borderless-game",
space.viewport.center,
space.viewport.size,
);
st.assign_node_to_monitor(window, monitor.as_str());
st.model
.node_app_ids
.insert(window, "steam_app_123".to_string());
assert!(node_blocks_interactive_transform(&st, window));
assert!(!node_allows_interactive_resize(&st, window));
}
#[test]
fn output_sized_ordinary_surface_allows_interactive_resize() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
let monitor = st.model.monitor_state.current_monitor.clone();
let space = st
.model
.monitor_state
.monitors
.get(monitor.as_str())
.expect("monitor")
.clone();
let window =
st.model
.field
.spawn_surface("firefox", space.viewport.center, space.viewport.size);
st.assign_node_to_monitor(window, monitor.as_str());
st.model.node_app_ids.insert(window, "firefox".to_string());
assert!(!node_blocks_interactive_transform(&st, window));
assert!(node_allows_interactive_resize(&st, window));
}
#[test]
fn ordinary_active_surface_allows_interactive_resize() {
let dh = Display::<Halley>::new().expect("display").handle();
let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
let monitor = st.model.monitor_state.current_monitor.clone();
let window = st.model.field.spawn_surface(
"window",
Vec2 { x: 100.0, y: 100.0 },
Vec2 { x: 320.0, y: 240.0 },
);
st.assign_node_to_monitor(window, monitor.as_str());
assert!(!node_blocks_interactive_transform(&st, window));
assert!(node_allows_interactive_resize(&st, window));
}
}