From 0253f10dcca5a2ac384ec07dd80a10e9497f7671 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 21:44:56 +0200 Subject: [PATCH 1/7] xdg-popup: fix XdgPopup::node_output --- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 1560242b..6f532121 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -317,7 +317,7 @@ impl Node for XdgPopup { } fn node_output(&self) -> Option> { - self.xdg.workspace.get().map(|w| w.output.get()) + Some(self.xdg.surface.output.get()) } fn node_location(&self) -> Option { From a5e8b39e4f3cdadb0e7be19daac60a761eeee968 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 22:26:57 +0200 Subject: [PATCH 2/7] toplevel: store if ancestor is fullscreen --- src/tree/container.rs | 10 ++++++++++ src/tree/containing.rs | 3 +++ src/tree/toplevel.rs | 31 +++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/tree/container.rs b/src/tree/container.rs index bcf01e7a..14e5aac1 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -2108,6 +2108,10 @@ impl ContainingNode for ContainerNode { fn cnode_get_float(self: Rc) -> Option> { self.tl_data().float.get() } + + fn cnode_self_or_ancestor_fullscreen(&self) -> bool { + self.tl_data().self_or_ancestor_is_fullscreen.get() + } } impl ToplevelNodeBase for ContainerNode { @@ -2229,6 +2233,12 @@ impl ToplevelNodeBase for ContainerNode { child.node.tl_set_float(float); } } + + fn tl_mark_ancestor_fullscreen_ext(&self, fullscreen: bool) { + for child in self.children.iter() { + child.node.tl_mark_ancestor_fullscreen(fullscreen); + } + } } fn direction_to_split(dir: Direction) -> (ContainerSplit, bool) { diff --git a/src/tree/containing.rs b/src/tree/containing.rs index c8b2d9ba..3a8c0f67 100644 --- a/src/tree/containing.rs +++ b/src/tree/containing.rs @@ -41,4 +41,7 @@ pub trait ContainingNode: Node { fn cnode_get_float(self: Rc) -> Option> { None } + fn cnode_self_or_ancestor_fullscreen(&self) -> bool { + false + } } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 33def51e..f472374e 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -70,6 +70,8 @@ pub trait ToplevelNode: ToplevelNodeBase { fn tl_pinned(&self) -> bool; fn tl_set_pinned(&self, self_pinned: bool, pinned: bool); fn tl_set_float(&self, float: Option<&Rc>); + fn tl_mark_ancestor_fullscreen(&self, fullscreen: bool); + fn tl_mark_fullscreen(&self, fullscreen: bool); } impl ToplevelNode for T { @@ -110,6 +112,9 @@ impl ToplevelNode for T { fn tl_set_parent(&self, parent: Rc) { let data = self.tl_data(); + if !data.is_fullscreen.get() { + self.tl_mark_ancestor_fullscreen(parent.cnode_self_or_ancestor_fullscreen()); + } let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); if parent_was_none { data.mapped_during_iteration.set(data.state.eng.iteration()); @@ -227,6 +232,22 @@ impl ToplevelNode for T { self.tl_data().float.set(float.cloned()); self.tl_push_float(float); } + + fn tl_mark_ancestor_fullscreen(&self, fullscreen: bool) { + let old = self + .tl_data() + .self_or_ancestor_is_fullscreen + .replace(fullscreen); + if old == fullscreen { + return; + } + self.tl_mark_ancestor_fullscreen_ext(fullscreen); + } + + fn tl_mark_fullscreen(&self, fullscreen: bool) { + self.tl_data().is_fullscreen.set(fullscreen); + self.tl_mark_ancestor_fullscreen(fullscreen); + } } pub trait ToplevelNodeBase: Node { @@ -290,6 +311,10 @@ pub trait ToplevelNodeBase: Node { fn tl_push_float(&self, float: Option<&Rc>) { let _ = float; } + + fn tl_mark_ancestor_fullscreen_ext(&self, fullscreen: bool) { + let _ = fullscreen; + } } pub struct FullscreenedData { @@ -346,6 +371,7 @@ pub struct ToplevelData { pub float_height: Cell, pub pinned: Cell, pub is_fullscreen: Cell, + pub self_or_ancestor_is_fullscreen: Cell, pub fullscrceen_data: RefCell>, pub workspace: CloneCell>>, pub title: RefCell, @@ -401,6 +427,7 @@ impl ToplevelData { float_height: Default::default(), pinned: Cell::new(false), is_fullscreen: Default::default(), + self_or_ancestor_is_fullscreen: Default::default(), fullscrceen_data: Default::default(), workspace: Default::default(), title: RefCell::new(title), @@ -731,7 +758,7 @@ impl ToplevelData { workspace: ws.clone(), }); drop(data); - self.is_fullscreen.set(true); + node.tl_mark_fullscreen(true); self.property_changed(TL_CHANGED_FULLSCREEN); node.tl_set_parent(ws.clone()); ws.set_fullscreen_node(&node); @@ -758,7 +785,7 @@ impl ToplevelData { return; } }; - self.is_fullscreen.set(false); + node.tl_mark_fullscreen(false); self.property_changed(TL_CHANGED_FULLSCREEN); match fd.workspace.fullscreen.get() { None => { From dbc954dded75b1e8338f9b29f4c07315e530947d Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 21:57:47 +0200 Subject: [PATCH 3/7] tree: add Node::node_layer --- src/ifs/wl_surface.rs | 14 +++- .../wl_surface/ext_session_lock_surface_v1.rs | 12 ++- src/ifs/wl_surface/tray.rs | 21 ++++- src/ifs/wl_surface/wl_subsurface.rs | 6 +- src/ifs/wl_surface/x_surface.rs | 9 +- src/ifs/wl_surface/x_surface/xwindow.rs | 11 ++- src/ifs/wl_surface/xdg_surface.rs | 38 ++++++++- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 16 +++- .../wl_surface/xdg_surface/xdg_toplevel.rs | 14 +++- src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 39 +++++++-- .../wl_surface/zwp_input_popup_surface_v2.rs | 5 ++ src/tree.rs | 84 ++++++++++++++++++- src/tree/container.rs | 10 ++- src/tree/display.rs | 10 ++- src/tree/float.rs | 11 ++- src/tree/output.rs | 10 ++- src/tree/placeholder.rs | 9 +- src/tree/toplevel.rs | 12 ++- src/tree/workspace.rs | 9 +- 19 files changed, 294 insertions(+), 46 deletions(-) diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 473a38bc..6d1bb32b 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -76,8 +76,8 @@ use { renderer::Renderer, tree::{ BeforeLatchListener, BeforeLatchResult, ContainerNode, FindTreeResult, FoundNode, - LatchListener, Node, NodeId, NodeLocation, NodeVisitor, NodeVisitorBase, OutputNode, - PlaceholderNode, PresentationListener, ToplevelNode, VblankListener, + LatchListener, Node, NodeId, NodeLayerLink, NodeLocation, NodeVisitor, NodeVisitorBase, + OutputNode, PlaceholderNode, PresentationListener, ToplevelNode, VblankListener, }, utils::{ cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -372,6 +372,8 @@ enum CommitAction { } trait SurfaceExt { + fn node_layer(&self) -> NodeLayerLink; + fn commit_requested(self: Rc, pending: &mut Box) -> CommitAction { let _ = pending; CommitAction::ContinueCommit @@ -444,6 +446,10 @@ trait SurfaceExt { pub struct NoneSurfaceExt; impl SurfaceExt for NoneSurfaceExt { + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Display + } + fn is_some(&self) -> bool { false } @@ -1796,6 +1802,10 @@ impl Node for WlSurface { Some(self.location.get()) } + fn node_layer(&self) -> NodeLayerLink { + self.ext.get().node_layer() + } + fn node_active_changed(&self, active: bool) { if let Some(tl) = self.toplevel.get() { tl.tl_surface_active_changed(active); diff --git a/src/ifs/wl_surface/ext_session_lock_surface_v1.rs b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs index 5bce2961..f0931675 100644 --- a/src/ifs/wl_surface/ext_session_lock_surface_v1.rs +++ b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs @@ -11,8 +11,8 @@ use { object::{Object, Version}, rect::Rect, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, - OutputNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + NodeVisitor, OutputNode, }, utils::numcell::NumCell, wire::{ExtSessionLockSurfaceV1Id, WlSurfaceId, ext_session_lock_surface_v1::*}, @@ -95,6 +95,10 @@ impl ExtSessionLockSurfaceV1 { } impl SurfaceExt for ExtSessionLockSurfaceV1 { + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Lock + } + fn extents_changed(&self) { self.client.state.tree_changed(); } @@ -138,6 +142,10 @@ impl Node for ExtSessionLockSurfaceV1 { self.surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Lock + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/ifs/wl_surface/tray.rs b/src/ifs/wl_surface/tray.rs index 558a6be8..3aef12c0 100644 --- a/src/ifs/wl_surface/tray.rs +++ b/src/ifs/wl_surface/tray.rs @@ -11,8 +11,8 @@ use { }, rect::Rect, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, - OutputNode, StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + NodeVisitor, OutputNode, StackedNode, }, utils::{ copyhashmap::CopyHashMap, @@ -216,6 +216,13 @@ impl XdgPopupParent for Popup { // nothing } + fn node_layer(&self) -> NodeLayerLink { + let Some(link) = self.stack_link.borrow().as_ref().map(|w| w.to_ref()) else { + return NodeLayerLink::Display; + }; + NodeLayerLink::Stacked(link) + } + fn tray_item(&self) -> Option { Some(self.parent.data().tray_item_id) } @@ -230,6 +237,10 @@ impl XdgPopupParent for Popup { } impl SurfaceExt for T { + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Output + } + fn before_apply_commit( self: Rc, pending: &mut PendingState, @@ -308,6 +319,10 @@ impl Node for T { self.data().surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Output + } + fn node_find_tree_at( &self, x: i32, @@ -384,7 +399,7 @@ fn get_popup( }; seat.add_tray_item_popup(item, &popup); let stack = data.client.state.root.stacked.clone(); - popup.xdg.set_popup_stack(&stack); + popup.xdg.set_popup_stack(&stack, false); popup.xdg.set_output(&node); let user = Rc::new(Popup { parent: item.clone(), diff --git a/src/ifs/wl_surface/wl_subsurface.rs b/src/ifs/wl_surface/wl_subsurface.rs index 0b6935e5..56d255b9 100644 --- a/src/ifs/wl_surface/wl_subsurface.rs +++ b/src/ifs/wl_surface/wl_subsurface.rs @@ -7,7 +7,7 @@ use { }, leaks::Tracker, object::{Object, Version}, - tree::Node, + tree::{Node, NodeLayerLink}, utils::{ clonecell::CloneCell, linkedlist::{LinkedNode, NodeRef}, @@ -361,6 +361,10 @@ impl Object for WlSubsurface { simple_add_obj!(WlSubsurface); impl SurfaceExt for WlSubsurface { + fn node_layer(&self) -> NodeLayerLink { + self.parent.node_layer() + } + fn commit_requested(self: Rc, pending: &mut Box) -> CommitAction { if self.sync() { let mut parent_pending = self.pending(); diff --git a/src/ifs/wl_surface/x_surface.rs b/src/ifs/wl_surface/x_surface.rs index 3fe6ffbc..1c3e295c 100644 --- a/src/ifs/wl_surface/x_surface.rs +++ b/src/ifs/wl_surface/x_surface.rs @@ -5,7 +5,7 @@ use { x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow}, }, leaks::Tracker, - tree::{Node, ToplevelNode, ToplevelNodeBase}, + tree::{Node, NodeLayerLink, ToplevelNode, ToplevelNodeBase}, utils::clonecell::CloneCell, xwayland::XWaylandEvent, }, @@ -23,6 +23,13 @@ pub struct XSurface { } impl SurfaceExt for XSurface { + fn node_layer(&self) -> NodeLayerLink { + let Some(win) = self.xwindow.get() else { + return NodeLayerLink::Display; + }; + win.node_layer() + } + fn after_apply_commit(self: Rc) { if let Some(xwindow) = self.xwindow.get() { xwindow.map_status_changed(); diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 3f2895d3..90b9b1c9 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -12,8 +12,8 @@ use { state::State, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeLocation, NodeVisitor, OutputNode, StackedNode, TileDragDestination, ToplevelData, - ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, + NodeLayerLink, NodeLocation, NodeVisitor, OutputNode, StackedNode, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, @@ -375,6 +375,13 @@ impl Node for Xwindow { self.x.surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + if let Some(link) = self.display_link.borrow().as_ref() { + return NodeLayerLink::Stacked(link.to_ref()); + } + self.toplevel_data.node_layer() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index 8116452b..f74bfebe 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -19,7 +19,8 @@ use { object::Object, rect::Rect, tree::{ - FindTreeResult, FoundNode, Node, NodeLocation, OutputNode, StackedNode, WorkspaceNode, + FindTreeResult, FoundNode, Node, NodeLayerLink, NodeLocation, OutputNode, StackedNode, + WorkspaceNode, }, utils::{ clonecell::CloneCell, @@ -75,6 +76,7 @@ pub struct XdgSurface { pub absolute_desired_extents: Cell, ext: CloneCell>>, popup_display_stack: CloneCell>>>, + is_above_layers: Cell, popups: CopyHashMap>, pub workspace: CloneCell>>, pub tracker: Tracker, @@ -152,6 +154,16 @@ impl XdgPopupParent for Popup { } } + fn node_layer(&self) -> NodeLayerLink { + let Some(link) = self.display_link.borrow().as_ref().map(|w| w.to_ref()) else { + return NodeLayerLink::Display; + }; + match self.popup.xdg.is_above_layers.get() { + true => NodeLayerLink::StackedAboveLayers(link), + false => NodeLayerLink::Stacked(link), + } + } + fn tray_item(&self) -> Option { self.parent.clone().tray_item() } @@ -201,6 +213,8 @@ pub trait XdgSurfaceExt: Debug { } fn make_visible(self: Rc); + + fn node_layer(&self) -> NodeLayerLink; } impl XdgSurface { @@ -217,6 +231,7 @@ impl XdgSurface { absolute_desired_extents: Cell::new(Default::default()), ext: Default::default(), popup_display_stack: CloneCell::new(surface.client.state.root.stacked.clone()), + is_above_layers: Cell::new(false), popups: Default::default(), workspace: Default::default(), tracker: Default::default(), @@ -331,7 +346,12 @@ impl XdgSurface { }) } - pub fn set_popup_stack(&self, stack: &Rc>>) { + pub fn set_popup_stack( + &self, + stack: &Rc>>, + is_above_layers: bool, + ) { + self.is_above_layers.set(is_above_layers); let prev = self.popup_display_stack.set(stack.clone()); if rc_eq(&prev, stack) { return; @@ -340,7 +360,7 @@ impl XdgSurface { if let Some(dl) = &*popup.display_link.borrow() { stack.add_last_existing(dl); } - popup.popup.xdg.set_popup_stack(stack); + popup.popup.xdg.set_popup_stack(stack, is_above_layers); } } @@ -423,7 +443,10 @@ impl XdgSurfaceRequestHandler for XdgSurface { workspace_link: Default::default(), }); popup.parent.set(Some(user.clone())); - popup.xdg.set_popup_stack(&parent.popup_display_stack.get()); + popup.xdg.set_popup_stack( + &parent.popup_display_stack.get(), + parent.is_above_layers.get(), + ); popup.xdg.set_output(&parent.surface.output.get()); parent.popups.set(req.id, user); } @@ -526,6 +549,13 @@ impl Object for XdgSurface { dedicated_add_obj!(XdgSurface, XdgSurfaceId, xdg_surfaces); impl SurfaceExt for XdgSurface { + fn node_layer(&self) -> NodeLayerLink { + let Some(ext) = self.ext.get() else { + return NodeLayerLink::Display; + }; + ext.node_layer() + } + fn before_apply_commit( self: Rc, pending: &mut PendingState, diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 6f532121..dedf8585 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -19,8 +19,8 @@ use { rect::Rect, renderer::Renderer, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, - OutputNode, StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + NodeVisitor, OutputNode, StackedNode, }, utils::clonecell::CloneCell, wire::{XdgPopupId, xdg_popup::*}, @@ -46,6 +46,7 @@ pub trait XdgPopupParent { fn post_commit(&self); fn visible(&self) -> bool; fn make_visible(self: Rc); + fn node_layer(&self) -> NodeLayerLink; fn tray_item(&self) -> Option { None } @@ -324,6 +325,10 @@ impl Node for XdgPopup { self.xdg.surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + XdgSurfaceExt::node_layer(self) + } + fn node_find_tree_at( &self, x: i32, @@ -441,6 +446,13 @@ impl XdgSurfaceExt for XdgPopup { fn make_visible(self: Rc) { self.node_make_visible(); } + + fn node_layer(&self) -> NodeLayerLink { + let Some(parent) = self.parent.get() else { + return NodeLayerLink::Display; + }; + parent.node_layer() + } } #[derive(Debug, Error)] diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index be3144e1..11ac6f33 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -27,9 +27,9 @@ use { state::State, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeLocation, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelNodeId, ToplevelType, WorkspaceNode, - default_tile_drag_destination, + NodeLayerLink, NodeLocation, NodeVisitor, OutputNode, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, ToplevelType, + WorkspaceNode, default_tile_drag_destination, }, utils::{clonecell::CloneCell, hash_map_ext::HashMapExt}, wire::{XdgToplevelId, xdg_toplevel::*}, @@ -577,6 +577,10 @@ impl Node for XdgToplevel { self.xdg.surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + self.toplevel_data.node_layer() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } @@ -788,6 +792,10 @@ impl XdgSurfaceExt for XdgToplevel { fn make_visible(self: Rc) { self.node_make_visible(); } + + fn node_layer(&self) -> NodeLayerLink { + self.toplevel_data.node_layer() + } } #[derive(Debug, Error)] diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 1ff98f94..98e9d890 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -15,8 +15,8 @@ use { rect::Rect, renderer::Renderer, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, - OutputNode, StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + NodeVisitor, OutputNode, StackedNode, }, utils::{ bitflags::BitflagsExt, @@ -65,7 +65,7 @@ pub struct ZwlrLayerSurfaceV1 { exclusive_zone: Cell, margin: Cell<(i32, i32, i32, i32)>, keyboard_interactivity: Cell, - link: Cell>>>, + link: RefCell>>>, seat_state: NodeSeatState, last_configure: Cell<(i32, i32)>, exclusive_edge: Cell>, @@ -171,7 +171,7 @@ impl ZwlrLayerSurfaceV1 { exclusive_zone: Cell::new(ExclusiveZone::MoveSelf), margin: Cell::new((0, 0, 0, 0)), keyboard_interactivity: Cell::new(0), - link: Cell::new(None), + link: Default::default(), seat_state: Default::default(), last_configure: Default::default(), exclusive_edge: Default::default(), @@ -291,7 +291,7 @@ impl ZwlrLayerSurfaceV1RequestHandler for ZwlrLayerSurfaceV1 { return Err(ZwlrLayerSurfaceV1Error::PopupHasParent); } let stack = self.client.state.root.stacked_above_layers.clone(); - popup.xdg.set_popup_stack(&stack); + popup.xdg.set_popup_stack(&stack, true); let user = Rc::new(Popup { parent: slf.clone(), popup: popup.clone(), @@ -534,7 +534,7 @@ impl ZwlrLayerSurfaceV1 { } pub fn destroy_node(&self) { - self.link.set(None); + self.link.borrow_mut().take(); self.mapped.set(false); self.surface.destroy_node(); self.seat_state.destroy_node(self); @@ -562,6 +562,18 @@ impl ZwlrLayerSurfaceV1 { } impl SurfaceExt for ZwlrLayerSurfaceV1 { + fn node_layer(&self) -> NodeLayerLink { + let Some(link) = self.link.borrow().as_ref().map(|l| l.to_ref()) else { + return NodeLayerLink::Display; + }; + match self.layer.get() { + 0 => NodeLayerLink::Layer0(link), + 1 => NodeLayerLink::Layer1(link), + 2 => NodeLayerLink::Layer2(link), + _ => NodeLayerLink::Layer3(link), + } + } + fn before_apply_commit( self: Rc, pending: &mut PendingState, @@ -587,7 +599,7 @@ impl SurfaceExt for ZwlrLayerSurfaceV1 { } } else if buffer_is_some { let layer = &output.layers[self.layer.get() as usize]; - self.link.set(Some(layer.add_last(self.clone()))); + *self.link.borrow_mut() = Some(layer.add_last(self.clone())); self.mapped.set(true); self.compute_position(); self.update_exclusive_size(); @@ -663,6 +675,10 @@ impl Node for ZwlrLayerSurfaceV1 { self.surface.node_location() } + fn node_layer(&self) -> NodeLayerLink { + SurfaceExt::node_layer(self) + } + fn node_find_tree_at( &self, x: i32, @@ -728,6 +744,13 @@ impl XdgPopupParent for Popup { fn make_visible(self: Rc) { // nothing } + + fn node_layer(&self) -> NodeLayerLink { + let Some(link) = self.stack_link.borrow().as_ref().map(|w| w.to_ref()) else { + return NodeLayerLink::Display; + }; + NodeLayerLink::StackedAboveLayers(link) + } } object_base! { @@ -738,7 +761,7 @@ object_base! { impl Object for ZwlrLayerSurfaceV1 { fn break_loops(&self) { self.destroy_node(); - self.link.set(None); + self.link.borrow_mut().take(); } } diff --git a/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs b/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs index 5580e9fd..3550b94d 100644 --- a/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs +++ b/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs @@ -9,6 +9,7 @@ use { object::{Object, Version}, rect::Rect, state::State, + tree::NodeLayerLink, wire::{WlSurfaceId, ZwpInputPopupSurfaceV2Id, zwp_input_popup_surface_v2::*}, }, std::{cell::Cell, rc::Rc}, @@ -27,6 +28,10 @@ pub struct ZwpInputPopupSurfaceV2 { } impl SurfaceExt for ZwpInputPopupSurfaceV2 { + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::InputMethod + } + fn after_apply_commit(self: Rc) { self.update_visible(); if self.surface.visible.get() { diff --git a/src/tree.rs b/src/tree.rs index 47191ab9..14c9d010 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -13,14 +13,15 @@ use { }, wl_pointer::PendingScroll, }, - wl_surface::{WlSurface, tray::TrayItemId}, + wl_surface::{WlSurface, tray::TrayItemId, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1}, }, keyboard::KeyboardState, rect::Rect, renderer::Renderer, - utils::numcell::NumCell, + utils::{linkedlist::NodeRef, numcell::NumCell}, }, jay_config::Direction as JayDirection, + linearize::{Linearize, LinearizeExt}, std::{ fmt::{Debug, Display}, rc::Rc, @@ -121,6 +122,84 @@ pub enum NodeLocation { Output(OutputNodeId), } +#[derive(Copy, Clone, Linearize, Eq, PartialEq, Debug)] +pub enum NodeLayer { + Display, + Layer0, + Layer1, + Output, + Workspace, + Tiled, + Fullscreen, + Stacked, + Layer2, + Layer3, + StackedAboveLayers, + Lock, + InputMethod, +} + +pub enum NodeLayerLink { + Display, + Layer0(#[expect(dead_code)] NodeRef>), + Layer1(#[expect(dead_code)] NodeRef>), + Output, + Workspace, + Tiled, + Fullscreen, + Stacked(#[expect(dead_code)] NodeRef>), + Layer2(#[expect(dead_code)] NodeRef>), + Layer3(#[expect(dead_code)] NodeRef>), + StackedAboveLayers(#[expect(dead_code)] NodeRef>), + Lock, + InputMethod, +} + +impl NodeLayerLink { + #[expect(dead_code)] + pub fn layer(&self) -> NodeLayer { + macro_rules! map { + ($($id:ident,)*) => { + match self { + $( + Self::$id { .. } => NodeLayer::$id, + )* + } + }; + } + map! { + Display, + Layer0, + Layer1, + Output, + Workspace, + Tiled, + Fullscreen, + Stacked, + Layer2, + Layer3, + StackedAboveLayers, + Lock, + InputMethod, + } + } +} + +impl NodeLayer { + #[expect(dead_code)] + pub fn prev(self) -> Self { + if self == NodeLayer::Display { + return NodeLayer::InputMethod; + } + Self::from_linear(self.linearize() - 1).unwrap_or(NodeLayer::InputMethod) + } + + #[expect(dead_code)] + pub fn next(self) -> Self { + Self::from_linear(self.linearize() + 1).unwrap_or(NodeLayer::Display) + } +} + pub trait Node: 'static { fn node_id(&self) -> NodeId; fn node_seat_state(&self) -> &NodeSeatState; @@ -130,6 +209,7 @@ pub trait Node: 'static { fn node_absolute_position(&self) -> Rect; fn node_output(&self) -> Option>; fn node_location(&self) -> Option; + fn node_layer(&self) -> NodeLayerLink; fn node_child_title_changed(self: Rc, child: &dyn Node, title: &str) { let _ = child; diff --git a/src/tree/container.rs b/src/tree/container.rs index 14e5aac1..0111b430 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -18,9 +18,9 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, - NodeId, NodeLocation, OutputNode, TddType, TileDragDestination, ToplevelData, - ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_bounds, - toplevel_set_floating, walker::NodeVisitor, + NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, + default_tile_drag_bounds, toplevel_set_floating, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, @@ -1553,6 +1553,10 @@ impl Node for ContainerNode { Some(self.location.get()) } + fn node_layer(&self) -> NodeLayerLink { + self.toplevel_data.node_layer() + } + fn node_child_title_changed(self: Rc, child: &dyn Node, title: &str) { if let Some(child) = self.child_nodes.borrow().get(&child.node_id()) { self.update_child_title(child, title); diff --git a/src/tree/display.rs b/src/tree/display.rs index a7aebf23..1d9ab082 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -8,9 +8,9 @@ use { renderer::Renderer, state::State, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, OutputNode, - StackedNode, TileDragDestination, WorkspaceDragDestination, WorkspaceNodeId, - walker::NodeVisitor, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, + WorkspaceNodeId, walker::NodeVisitor, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, @@ -157,6 +157,10 @@ impl Node for DisplayNode { None } + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Display + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/tree/float.rs b/src/tree/float.rs index 57f35461..36722a3d 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -15,8 +15,8 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeLocation, OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, - WorkspaceNode, toplevel_set_floating, walker::NodeVisitor, + NodeLayerLink, NodeLocation, OutputNode, PinnedNode, StackedNode, TileDragDestination, + ToplevelNode, WorkspaceNode, toplevel_set_floating, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -709,6 +709,13 @@ impl Node for FloatNode { Some(self.location.get()) } + fn node_layer(&self) -> NodeLayerLink { + let Some(l) = self.display_link.borrow().as_ref().map(|l| l.to_ref()) else { + return NodeLayerLink::Display; + }; + NodeLayerLink::Stacked(l) + } + fn node_child_title_changed(self: Rc, _child: &dyn Node, title: &str) { self.update_child_title(title); } diff --git a/src/tree/output.rs b/src/tree/output.rs index f5db42c1..4f1dee00 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -43,9 +43,9 @@ use { state::State, text::TextTexture, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, - PinnedNode, StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, - WorkspaceNode, WorkspaceNodeId, walker::NodeVisitor, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, + NodeLocation, PinnedNode, StackedNode, TddType, TileDragDestination, + WorkspaceDragDestination, WorkspaceNode, WorkspaceNodeId, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, bitflags::BitflagsExt, clonecell::CloneCell, @@ -1478,6 +1478,10 @@ impl Node for OutputNode { Some(NodeLocation::Output(self.id)) } + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Output + } + fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { if self.state.lock.locked.get() { if let Some(lock) = self.lock_surface.get() { diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index e20c78d9..d391b002 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -11,8 +11,9 @@ use { text::TextTexture, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeLocation, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, + NodeLayerLink, NodeLocation, NodeVisitor, OutputNode, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, + default_tile_drag_destination, }, utils::{ asyncevent::AsyncEvent, errorfmt::ErrorFmt, on_drop_event::OnDropEvent, @@ -181,6 +182,10 @@ impl Node for PlaceholderNode { self.location.get() } + fn node_layer(&self) -> NodeLayerLink { + self.toplevel.node_layer() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index f472374e..9e42820a 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -28,7 +28,7 @@ use { state::State, tree::{ ContainerNode, ContainerSplit, ContainingNode, Direction, FloatNode, Node, NodeId, - OutputNode, PlaceholderNode, WorkspaceNode, + NodeLayerLink, OutputNode, PlaceholderNode, WorkspaceNode, }, utils::{ array_to_tuple::ArrayToTuple, @@ -902,6 +902,16 @@ impl ToplevelData { parent.cnode_make_visible(slf); } } + + pub fn node_layer(&self) -> NodeLayerLink { + if self.self_or_ancestor_is_fullscreen.get() { + return NodeLayerLink::Fullscreen; + } + if let Some(float) = self.float.get() { + return float.node_layer(); + } + NodeLayerLink::Tiled + } } impl Drop for ToplevelData { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 8b118bef..bcc99517 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -21,8 +21,9 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, - NodeId, NodeLocation, NodeVisitorBase, OutputNode, OutputNodeId, PlaceholderNode, - StackedNode, ToplevelNode, container::ContainerNode, walker::NodeVisitor, + NodeId, NodeLayerLink, NodeLocation, NodeVisitorBase, OutputNode, OutputNodeId, + PlaceholderNode, StackedNode, ToplevelNode, container::ContainerNode, + walker::NodeVisitor, }, utils::{ clonecell::CloneCell, @@ -321,6 +322,10 @@ impl Node for WorkspaceNode { Some(self.location()) } + fn node_layer(&self) -> NodeLayerLink { + NodeLayerLink::Workspace + } + fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { if let Some(fs) = self.fullscreen.get() { fs.node_do_focus(seat, direction); From 5a004c3d3125121a1059819bca2dfa0d21a55d7e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 21:59:43 +0200 Subject: [PATCH 4/7] tree: add Node::node_accepts_focus --- src/ifs/wl_surface/x_surface/xwindow.rs | 4 ++++ src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 4 ++++ src/tree.rs | 5 +++++ src/tree/float.rs | 7 +++++++ 4 files changed, 20 insertions(+) diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 90b9b1c9..4c29833d 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -382,6 +382,10 @@ impl Node for Xwindow { self.toplevel_data.node_layer() } + fn node_accepts_focus(&self) -> bool { + self.tl_accepts_keyboard_focus() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 98e9d890..9f8089a7 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -679,6 +679,10 @@ impl Node for ZwlrLayerSurfaceV1 { SurfaceExt::node_layer(self) } + fn node_accepts_focus(&self) -> bool { + self.keyboard_interactivity.get() != KI_NONE + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/tree.rs b/src/tree.rs index 14c9d010..63fe4c88 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -216,6 +216,11 @@ pub trait Node: 'static { let _ = title; } + #[expect(dead_code)] + fn node_accepts_focus(&self) -> bool { + true + } + fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { let _ = seat; let _ = direction; diff --git a/src/tree/float.rs b/src/tree/float.rs index 36722a3d..15b3f6b1 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -720,6 +720,13 @@ impl Node for FloatNode { self.update_child_title(title); } + fn node_accepts_focus(&self) -> bool { + if let Some(c) = self.child.get() { + return c.tl_accepts_keyboard_focus(); + } + false + } + fn node_find_tree_at( &self, x: i32, From c034ea7604c9b3713b58d41b6cecd09203201bd9 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 22:00:32 +0200 Subject: [PATCH 5/7] tree: implement Node::node_accepts_focus for more nodes --- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 8 ++++++-- src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 10 +++++++--- src/tree/float.rs | 6 ++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index dedf8585..d70f8236 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -19,8 +19,8 @@ use { rect::Rect, renderer::Renderer, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, - NodeVisitor, OutputNode, StackedNode, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, + NodeLocation, NodeVisitor, OutputNode, StackedNode, }, utils::clonecell::CloneCell, wire::{XdgPopupId, xdg_popup::*}, @@ -329,6 +329,10 @@ impl Node for XdgPopup { XdgSurfaceExt::node_layer(self) } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { + seat.focus_node(self.xdg.surface.clone()); + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 9f8089a7..aa8a9bf2 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -3,7 +3,7 @@ use { client::{Client, ClientError}, ifs::{ wl_output::OutputGlobalOpt, - wl_seat::NodeSeatState, + wl_seat::{NodeSeatState, WlSeatGlobal}, wl_surface::{ PendingState, SurfaceExt, SurfaceRole, WlSurface, WlSurfaceError, xdg_surface::xdg_popup::{XdgPopup, XdgPopupParent}, @@ -15,8 +15,8 @@ use { rect::Rect, renderer::Renderer, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, - NodeVisitor, OutputNode, StackedNode, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, + NodeLocation, NodeVisitor, OutputNode, StackedNode, }, utils::{ bitflags::BitflagsExt, @@ -683,6 +683,10 @@ impl Node for ZwlrLayerSurfaceV1 { self.keyboard_interactivity.get() != KI_NONE } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { + seat.focus_node(self.surface.clone()) + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/tree/float.rs b/src/tree/float.rs index 15b3f6b1..63a63e23 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -727,6 +727,12 @@ impl Node for FloatNode { false } + fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { + if let Some(c) = self.child.get() { + c.node_do_focus(seat, direction); + } + } + fn node_find_tree_at( &self, x: i32, From bd85db5b59b96f53552ff5a439361848b09acb50 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 22:01:50 +0200 Subject: [PATCH 6/7] config: add focus-below and focus-above actions --- jay-config/src/_private/client.rs | 6 +- jay-config/src/_private/ipc.rs | 6 +- jay-config/src/input.rs | 13 +++ src/config/handler.rs | 18 ++- src/ifs/wl_seat.rs | 141 ++++++++++++++++++++++- src/tree.rs | 16 +-- toml-config/src/config.rs | 6 +- toml-config/src/config/parsers/action.rs | 4 +- toml-config/src/lib.rs | 4 + toml-spec/spec/spec.generated.json | 4 +- toml-spec/spec/spec.generated.md | 8 ++ toml-spec/spec/spec.yaml | 4 + 12 files changed, 211 insertions(+), 19 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 4c14d30e..45729b09 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -15,7 +15,7 @@ use { client::{Client, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ - FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, Timeline, + FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, }, keyboard::{ @@ -379,6 +379,10 @@ impl ConfigClient { }); } + pub fn seat_focus_layer_rel(&self, seat: Seat, direction: LayerDirection) { + self.send(&ClientMessage::SeatFocusLayerRel { seat, direction }); + } + pub fn seat_focus(&self, seat: Seat, direction: Direction) { self.send(&ClientMessage::SeatFocus { seat, direction }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 9ad25abc..700335bc 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -4,7 +4,7 @@ use { Axis, Direction, PciId, Workspace, client::{Client, ClientMatcher}, input::{ - FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, Timeline, + FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, }, keyboard::{Keymap, mods::Modifiers, syms::KeySym}, @@ -737,6 +737,10 @@ pub enum ClientMessage<'a> { seat: Seat, same_workspace: bool, }, + SeatFocusLayerRel { + seat: Seat, + direction: LayerDirection, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index c63cb809..e1f12633 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -189,6 +189,13 @@ pub enum Timeline { Newer, } +/// A direction for layer traversal. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum LayerDirection { + Below, + Above, +} + /// A seat. #[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] pub struct Seat(pub u64); @@ -303,6 +310,12 @@ impl Seat { get!().seat_focus_history_set_same_workspace(self, same_workspace) } + /// Moves the keyboard focus of the seat to the layer above or below the current + /// layer. + pub fn focus_layer_rel(self, direction: LayerDirection) { + get!().seat_focus_layer_rel(self, direction) + } + /// Moves the keyboard focus of the seat in the specified direction. pub fn focus(self, direction: Direction) { get!().seat_focus(self, direction) diff --git a/src/config/handler.rs b/src/config/handler.rs index d26ed820..07e9bdf9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -54,7 +54,7 @@ use { Axis, Direction, Workspace, client::{Client as ConfigClient, ClientMatcher}, input::{ - FocusFollowsMouseMode, InputDevice, Seat, Timeline, + FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, Timeline, acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile}, capability::{ CAP_GESTURE, CAP_KEYBOARD, CAP_POINTER, CAP_SWITCH, CAP_TABLET_PAD, @@ -2185,6 +2185,19 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_focus_layer_rel( + &self, + seat: Seat, + direction: LayerDirection, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + match direction { + LayerDirection::Below => seat.focus_layer_below(), + LayerDirection::Above => seat.focus_layer_above(), + } + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -3039,6 +3052,9 @@ impl ConfigProxyHandler { } => self .handle_seat_focus_history_set_same_workspace(seat, same_workspace) .wrn("seat_focus_history_set_same_workspace")?, + ClientMessage::SeatFocusLayerRel { seat, direction } => self + .handle_seat_focus_layer_rel(seat, direction) + .wrn("seat_focus_layer_rel")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 1f0a8f9b..4cc0058a 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -70,6 +70,7 @@ use { dnd_icon::DndIcon, tray::{DynTrayItem, TrayItemId}, xdg_surface::xdg_popup::XdgPopup, + zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, }, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, @@ -80,9 +81,10 @@ use { rect::Rect, state::{DeviceHandlerData, State}, tree::{ - ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, NodeLocation, - OutputNode, ToplevelNode, WorkspaceNode, generic_node_visitor, toplevel_create_split, - toplevel_parent_container, toplevel_set_floating, toplevel_set_workspace, + ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, NodeLayer, + NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode, + generic_node_visitor, toplevel_create_split, toplevel_parent_container, + toplevel_set_floating, toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, @@ -801,6 +803,139 @@ impl WlSeatGlobal { self.focus_history_same_workspace.set(same_workspace); } + fn focus_layer_rel( + self: &Rc, + next_layer: impl Fn(NodeLayer) -> NodeLayer, + layer_node_next: impl Fn( + &NodeRef>, + ) -> Option>>, + stacked_node_next: impl Fn( + &NodeRef>, + ) -> Option>>, + layer_list_iter: impl Fn(&LinkedList>) -> LI, + stacked_list_iter: impl Fn(&LinkedList>) -> SI, + ) where + LI: Iterator>>, + SI: Iterator>>, + { + fn node_viable(n: &(impl Node + ?Sized)) -> bool { + n.node_visible() && n.node_accepts_focus() + } + + let current = self.keyboard_node.get(); + let Some(output) = current.node_output() else { + return; + }; + let current_layer = current.node_layer(); + match ¤t_layer { + NodeLayerLink::Layer0(l) + | NodeLayerLink::Layer1(l) + | NodeLayerLink::Layer2(l) + | NodeLayerLink::Layer3(l) => { + if let Some(n) = layer_node_next(l) + && node_viable(&**n) + { + n.deref() + .clone() + .node_do_focus(self, Direction::Unspecified); + return; + } + } + NodeLayerLink::Stacked(l) | NodeLayerLink::StackedAboveLayers(l) => { + if let Some(n) = stacked_node_next(l) + && node_viable(&**n) + && n.node_output().map(|o| o.id) == Some(output.id) + { + n.deref() + .clone() + .node_do_focus(self, Direction::Unspecified); + return; + } + } + NodeLayerLink::Display => {} + NodeLayerLink::Output => {} + NodeLayerLink::Workspace => {} + NodeLayerLink::Tiled => {} + NodeLayerLink::Fullscreen => {} + NodeLayerLink::Lock => {} + NodeLayerLink::InputMethod => {} + } + let handle_layer_shell = |l: &LinkedList>| { + for n in layer_list_iter(l) { + if node_viable(&**n) { + return Some(n.deref().clone() as Rc); + } + } + None + }; + let handle_stacked = |l: &LinkedList>| { + for n in stacked_list_iter(l) { + if node_viable(&**n) && n.node_output().map(|o| o.id) == Some(output.id) { + return Some(n.deref().clone() as Rc); + } + } + None + }; + let ws = output.workspace.get(); + let first = next_layer(current_layer.layer()); + let mut layer = first; + loop { + let node = match layer { + NodeLayer::Display => None, + NodeLayer::Layer0 => handle_layer_shell(&output.layers[0]), + NodeLayer::Layer1 => handle_layer_shell(&output.layers[1]), + NodeLayer::Output => None, + NodeLayer::Workspace => None, + NodeLayer::Tiled => ws + .as_ref() + .and_then(|w| w.container.get()) + .map(|n| n as Rc), + NodeLayer::Fullscreen => ws + .as_ref() + .and_then(|w| w.fullscreen.get()) + .map(|n| n as Rc), + NodeLayer::Stacked => handle_stacked(&self.state.root.stacked), + NodeLayer::Layer2 => handle_layer_shell(&output.layers[2]), + NodeLayer::Layer3 => handle_layer_shell(&output.layers[3]), + NodeLayer::StackedAboveLayers => { + handle_stacked(&self.state.root.stacked_above_layers) + } + NodeLayer::Lock => None, + NodeLayer::InputMethod => None, + }; + if let Some(n) = node { + if node_viable(&*n) { + n.node_do_focus(self, Direction::Unspecified); + return; + } + } + layer = next_layer(layer); + if layer == first { + return; + } + } + } + + pub fn focus_layer_below(self: &Rc) { + self.focus_layer_rel( + |l| l.prev(), + |n| n.prev(), + |n| n.prev(), + |l| l.rev_iter(), + |l| l.rev_iter(), + ); + } + + pub fn focus_layer_above(self: &Rc) { + self.focus_layer_rel( + |l| l.next(), + |n| n.next(), + |n| n.next(), + |l| l.iter(), + |l| l.iter(), + ); + } + fn set_selection_( self: &Rc, field: &CloneCell>>, diff --git a/src/tree.rs b/src/tree.rs index 63fe4c88..db016d30 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -141,22 +141,21 @@ pub enum NodeLayer { pub enum NodeLayerLink { Display, - Layer0(#[expect(dead_code)] NodeRef>), - Layer1(#[expect(dead_code)] NodeRef>), + Layer0(NodeRef>), + Layer1(NodeRef>), Output, Workspace, Tiled, Fullscreen, - Stacked(#[expect(dead_code)] NodeRef>), - Layer2(#[expect(dead_code)] NodeRef>), - Layer3(#[expect(dead_code)] NodeRef>), - StackedAboveLayers(#[expect(dead_code)] NodeRef>), + Stacked(NodeRef>), + Layer2(NodeRef>), + Layer3(NodeRef>), + StackedAboveLayers(NodeRef>), Lock, InputMethod, } impl NodeLayerLink { - #[expect(dead_code)] pub fn layer(&self) -> NodeLayer { macro_rules! map { ($($id:ident,)*) => { @@ -186,7 +185,6 @@ impl NodeLayerLink { } impl NodeLayer { - #[expect(dead_code)] pub fn prev(self) -> Self { if self == NodeLayer::Display { return NodeLayer::InputMethod; @@ -194,7 +192,6 @@ impl NodeLayer { Self::from_linear(self.linearize() - 1).unwrap_or(NodeLayer::InputMethod) } - #[expect(dead_code)] pub fn next(self) -> Self { Self::from_linear(self.linearize() + 1).unwrap_or(NodeLayer::Display) } @@ -216,7 +213,6 @@ pub trait Node: 'static { let _ = title; } - #[expect(dead_code)] fn node_accepts_focus(&self) -> bool { true } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 758f6070..12051f42 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -23,7 +23,10 @@ use { ahash::AHashMap, jay_config::{ Axis, Direction, Workspace, - input::{SwitchEvent, Timeline, acceleration::AccelProfile, clickmethod::ClickMethod}, + input::{ + LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile, + clickmethod::ClickMethod, + }, keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym}, logging::LogLevel, status::MessageFormat, @@ -72,6 +75,7 @@ pub enum SimpleCommand { ShowBar(bool), ToggleBar, FocusHistory(Timeline), + FocusLayerRel(LayerDirection), } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 9322a784..f4746cf2 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -34,7 +34,7 @@ use { jay_config::{ Axis::{Horizontal, Vertical}, get_workspace, - input::Timeline, + input::{LayerDirection, Timeline}, }, thiserror::Error, }; @@ -139,6 +139,8 @@ impl ActionParser<'_> { "toggle-bar" => ToggleBar, "focus-prev" => FocusHistory(Timeline::Older), "focus-next" => FocusHistory(Timeline::Newer), + "focus-below" => FocusLayerRel(LayerDirection::Below), + "focus-above" => FocusLayerRel(LayerDirection::Above), _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index b13733ce..47c80080 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -160,6 +160,10 @@ impl Action { let persistent = state.persistent.clone(); B::new(move || persistent.seat.focus_history(timeline)) } + SimpleCommand::FocusLayerRel(direction) => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.focus_layer_rel(direction)) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 18acd81d..75eadf01 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1614,7 +1614,9 @@ "hide-bar", "toggle-bar", "focus-prev", - "focus-next" + "focus-next", + "focus-below", + "focus-above" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 3229d088..a97edd37 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3675,6 +3675,14 @@ The string should have one of the following values: Focuses the next window in the focus history. +- `focus-below`: + + Focuses the layer below the currently focused layer. + +- `focus-above`: + + Focuses the layer above the currently focused layer. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index b69f1177..67ff709f 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -864,6 +864,10 @@ SimpleActionName: description: Focuses the previous window in the focus history. - value: focus-next description: Focuses the next window in the focus history. + - value: focus-below + description: Focuses the layer below the currently focused layer. + - value: focus-above + description: Focuses the layer above the currently focused layer. Color: From 57a49d5299012469ed4155a4458206328c75f7eb Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 23:00:28 +0200 Subject: [PATCH 7/7] config: add focus-tiles action --- jay-config/src/_private/client.rs | 4 ++++ jay-config/src/_private/ipc.rs | 3 +++ jay-config/src/input.rs | 5 +++++ src/config/handler.rs | 9 ++++++++ src/ifs/wl_seat.rs | 26 ++++++++++++++++++++++++ toml-config/src/config.rs | 1 + toml-config/src/config/parsers/action.rs | 1 + toml-config/src/lib.rs | 4 ++++ toml-spec/spec/spec.generated.json | 3 ++- toml-spec/spec/spec.generated.md | 4 ++++ toml-spec/spec/spec.yaml | 2 ++ 11 files changed, 61 insertions(+), 1 deletion(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 45729b09..b9200780 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -383,6 +383,10 @@ impl ConfigClient { self.send(&ClientMessage::SeatFocusLayerRel { seat, direction }); } + pub fn seat_focus_tiles(&self, seat: Seat) { + self.send(&ClientMessage::SeatFocusTiles { seat }); + } + pub fn seat_focus(&self, seat: Seat, direction: Direction) { self.send(&ClientMessage::SeatFocus { seat, direction }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 700335bc..6d370b4b 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -741,6 +741,9 @@ pub enum ClientMessage<'a> { seat: Seat, direction: LayerDirection, }, + SeatFocusTiles { + seat: Seat, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index e1f12633..f1319203 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -316,6 +316,11 @@ impl Seat { get!().seat_focus_layer_rel(self, direction) } + /// Moves the keyboard focus to the tile layer. + pub fn focus_tiles(self) { + get!().seat_focus_tiles(self) + } + /// Moves the keyboard focus of the seat in the specified direction. pub fn focus(self, direction: Direction) { get!().seat_focus(self, direction) diff --git a/src/config/handler.rs b/src/config/handler.rs index 07e9bdf9..995880ae 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2198,6 +2198,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_focus_tiles(&self, seat: Seat) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.focus_tiles(); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -3055,6 +3061,9 @@ impl ConfigProxyHandler { ClientMessage::SeatFocusLayerRel { seat, direction } => self .handle_seat_focus_layer_rel(seat, direction) .wrn("seat_focus_layer_rel")?, + ClientMessage::SeatFocusTiles { seat } => { + self.handle_seat_focus_tiles(seat).wrn("seat_focus_tiles")? + } } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 4cc0058a..abda9a9e 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -936,6 +936,32 @@ impl WlSeatGlobal { ); } + pub fn focus_tiles(self: &Rc) { + let current = self.keyboard_node.get(); + if matches!( + current.node_layer().layer(), + NodeLayer::Tiled | NodeLayer::Fullscreen, + ) { + return; + } + let Some(output) = current.node_output() else { + return; + }; + let Some(ws) = output.workspace.get() else { + return; + }; + let node = match ws.fullscreen.get() { + Some(fs) => fs as Rc, + _ => match ws.container.get() { + Some(c) => c, + _ => return, + }, + }; + if node.node_visible() && node.node_accepts_focus() { + node.node_do_focus(self, Direction::Unspecified); + } + } + fn set_selection_( self: &Rc, field: &CloneCell>>, diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 12051f42..75b17815 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -76,6 +76,7 @@ pub enum SimpleCommand { ToggleBar, FocusHistory(Timeline), FocusLayerRel(LayerDirection), + FocusTiles, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index f4746cf2..48428cf1 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -141,6 +141,7 @@ impl ActionParser<'_> { "focus-next" => FocusHistory(Timeline::Newer), "focus-below" => FocusLayerRel(LayerDirection::Below), "focus-above" => FocusLayerRel(LayerDirection::Above), + "focus-tiles" => FocusTiles, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 47c80080..7caac63c 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -164,6 +164,10 @@ impl Action { let persistent = state.persistent.clone(); B::new(move || persistent.seat.focus_layer_rel(direction)) } + SimpleCommand::FocusTiles => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.focus_tiles()) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 75eadf01..90a3d3e3 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1616,7 +1616,8 @@ "focus-prev", "focus-next", "focus-below", - "focus-above" + "focus-above", + "focus-tiles" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index a97edd37..1abda90a 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3683,6 +3683,10 @@ The string should have one of the following values: Focuses the layer above the currently focused layer. +- `focus-tiles`: + + Focuses the tile layer. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 67ff709f..ba6c5f69 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -868,6 +868,8 @@ SimpleActionName: description: Focuses the layer below the currently focused layer. - value: focus-above description: Focuses the layer above the currently focused layer. + - value: focus-tiles + description: Focuses the tile layer. Color: