From bd85db5b59b96f53552ff5a439361848b09acb50 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 22:01:50 +0200 Subject: [PATCH] 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: