From 73436f5677439aeb733ce84fdcd511538522b85e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 16:16:27 +0200 Subject: [PATCH 1/8] container: fix member order --- src/tree/container.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/tree/container.rs b/src/tree/container.rs index b867d928..3e570aed 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -1543,6 +1543,10 @@ impl Node for ContainerNode { .unwrap() } + fn node_output(&self) -> Option> { + self.toplevel_data.output_opt() + } + 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); @@ -1675,6 +1679,14 @@ impl Node for ContainerNode { .node_do_focus(seat, Direction::Unspecified); } + fn node_on_leave(&self, seat: &WlSeatGlobal) { + let mut seats = self.cursors.borrow_mut(); + let id = CursorType::Seat(seat.id()); + if let Some(seat_state) = seats.get_mut(&id) { + seat_state.op = None; + } + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, x: Fixed, y: Fixed) { // log::info!("node_on_pointer_enter"); self.pointer_move( @@ -1687,14 +1699,6 @@ impl Node for ContainerNode { ); } - fn node_on_leave(&self, seat: &WlSeatGlobal) { - let mut seats = self.cursors.borrow_mut(); - let id = CursorType::Seat(seat.id()); - if let Some(seat_state) = seats.get_mut(&id) { - seat_state.op = None; - } - } - fn node_on_pointer_unfocus(&self, seat: &Rc) { // log::info!("unfocus"); let mut seats = self.cursors.borrow_mut(); @@ -1781,10 +1785,6 @@ impl Node for ContainerNode { fn node_is_container(&self) -> bool { true } - - fn node_output(&self) -> Option> { - self.toplevel_data.output_opt() - } } impl ContainingNode for ContainerNode { From 6866f62a5528ab47437528b1f95c1bad52cd0bd7 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 11:11:54 +0200 Subject: [PATCH 2/8] container: fix cnode_workspace --- src/tree/container.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tree/container.rs b/src/tree/container.rs index 3e570aed..950d23eb 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -2084,6 +2084,7 @@ impl ToplevelNodeBase for ContainerNode { } fn tl_set_workspace_ext(&self, ws: &Rc) { + self.workspace.set(ws.clone()); for child in self.children.iter() { child.node.clone().tl_set_workspace(ws); } From f75051281b8fb3665a0ed7689d3e866869dff2ab Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 19:57:22 +0200 Subject: [PATCH 3/8] placeholder: fix member order --- src/tree/placeholder.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index fbf3362c..b814b207 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -170,6 +170,10 @@ impl Node for PlaceholderNode { self.toplevel.pos.get() } + fn node_output(&self) -> Option> { + self.toplevel.output_opt() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } @@ -205,16 +209,12 @@ impl Node for PlaceholderNode { seat.enter_toplevel(self.clone()); } - fn node_is_placeholder(&self) -> bool { - true - } - fn node_into_toplevel(self: Rc) -> Option> { Some(self) } - fn node_output(&self) -> Option> { - self.toplevel.output_opt() + fn node_is_placeholder(&self) -> bool { + true } } From 289c201a69422cf8bca03b0927fdb64d185f38bb Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 19 Jul 2025 11:21:45 +0200 Subject: [PATCH 4/8] tree: add Node::node_location --- src/compositor.rs | 1 + src/ifs/ext_session_lock_v1.rs | 3 ++- src/ifs/wl_seat.rs | 11 +++++++---- src/ifs/wl_surface.rs | 18 +++++++++++++----- src/ifs/wl_surface/cursor.rs | 5 +++-- .../wl_surface/ext_session_lock_surface_v1.rs | 9 ++++++++- src/ifs/wl_surface/tray.rs | 11 ++++++++--- src/ifs/wl_surface/wl_subsurface.rs | 3 ++- src/ifs/wl_surface/x_surface/xwindow.rs | 15 +++++++++++---- src/ifs/wl_surface/xdg_surface.rs | 9 ++++++--- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 8 ++++++-- src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs | 8 ++++++-- src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 11 ++++++++--- src/tree.rs | 10 ++++++++++ src/tree/container.rs | 11 +++++++++-- src/tree/display.rs | 9 +++++++-- src/tree/float.rs | 11 +++++++++-- src/tree/output.rs | 11 ++++++++--- src/tree/placeholder.rs | 15 +++++++++++++-- src/tree/workspace.rs | 18 +++++++++++++++--- 20 files changed, 152 insertions(+), 45 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 609dc995..992bb189 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -752,6 +752,7 @@ fn create_dummy_output(state: &Rc) { state: state.clone(), is_dummy: true, output: CloneCell::new(dummy_output.clone()), + output_id: Cell::new(dummy_output.id), position: Default::default(), container: Default::default(), stacked: Default::default(), diff --git a/src/ifs/ext_session_lock_v1.rs b/src/ifs/ext_session_lock_v1.rs index 584a833c..0c193ae3 100644 --- a/src/ifs/ext_session_lock_v1.rs +++ b/src/ifs/ext_session_lock_v1.rs @@ -6,6 +6,7 @@ use { }, leaks::Tracker, object::{Object, Version}, + tree::NodeLocation, wire::{ExtSessionLockV1Id, ext_session_lock_v1::*}, }, std::{cell::Cell, rc::Rc}, @@ -87,7 +88,7 @@ impl ExtSessionLockV1RequestHandler for ExtSessionLockV1 { node.set_lock_surface(Some(new.clone())); let pos = node.global.pos.get(); new.change_extents(pos); - new.surface.set_output(&node); + new.surface.set_output(&node, NodeLocation::Output(node.id)); self.client.state.tree_changed(); } Ok(()) diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 359ea8c2..791d5781 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -79,8 +79,8 @@ use { rect::Rect, state::{DeviceHandlerData, State}, tree::{ - ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, OutputNode, - ToplevelNode, WorkspaceNode, generic_node_visitor, toplevel_create_split, + 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, }, utils::{ @@ -717,7 +717,9 @@ impl WlSeatGlobal { serial: u64, ) -> Result<(), WlSeatError> { if let Some(icon) = &icon { - icon.surface().set_output(&self.pointer_cursor.output()); + let output = self.pointer_cursor.output(); + icon.surface() + .set_output(&output, NodeLocation::Output(output.id)); } self.pointer_owner .start_drag(self, origin, source, icon, serial) @@ -1082,7 +1084,8 @@ impl WlSeatGlobal { impl CursorUserOwner for WlSeatGlobal { fn output_changed(&self, output: &Rc) { if let Some(dnd) = self.pointer_owner.dnd_icon() { - dnd.surface().set_output(output); + dnd.surface() + .set_output(output, NodeLocation::Output(output.id)); } if let Some(drag) = self.pointer_owner.toplevel_drag() && let Some(tl) = drag.toplevel.get() diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index c61d3bf4..2307a353 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, NodeVisitor, NodeVisitorBase, OutputNode, PlaceholderNode, - PresentationListener, ToplevelNode, VblankListener, + LatchListener, Node, NodeId, NodeLocation, NodeVisitor, NodeVisitorBase, OutputNode, + PlaceholderNode, PresentationListener, ToplevelNode, VblankListener, }, utils::{ cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -319,6 +319,7 @@ pub struct WlSurface { idle_inhibitors: SmallMap, 1>, viewporter: CloneCell>>, output: CloneCell>, + location: Cell, fractional_scale: CloneCell>>, pub constraints: SmallMap, 1>, xwayland_serial: Cell>, @@ -623,6 +624,7 @@ pub struct StackElement { impl WlSurface { pub fn new(id: WlSurfaceId, client: &Rc, version: Version, slf: &Weak) -> Self { + let dummy_output = client.state.dummy_output.get().unwrap(); Self { id, node_id: client.state.node_ids.next(), @@ -662,7 +664,8 @@ impl WlSurface { tracker: Default::default(), idle_inhibitors: Default::default(), viewporter: Default::default(), - output: CloneCell::new(client.state.dummy_output.get().unwrap()), + location: Cell::new(NodeLocation::Output(dummy_output.id)), + output: CloneCell::new(dummy_output), fractional_scale: Default::default(), constraints: Default::default(), xwayland_serial: Default::default(), @@ -715,7 +718,8 @@ impl WlSurface { self.output.get() } - pub fn set_output(&self, output: &Rc) { + pub fn set_output(&self, output: &Rc, location: NodeLocation) { + self.location.set(location); let old = self.output.set(output.clone()); if old.id == output.id { return; @@ -737,7 +741,7 @@ impl WlSurface { let children = self.children.borrow_mut(); if let Some(children) = &*children { for ss in children.subsurfaces.values() { - ss.surface.set_output(output); + ss.surface.set_output(output, location); } } } @@ -1788,6 +1792,10 @@ impl Node for WlSurface { Some(self.output.get()) } + fn node_location(&self) -> Option { + Some(self.location.get()) + } + 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/cursor.rs b/src/ifs/wl_surface/cursor.rs index d9007dd1..2f5b5cd3 100644 --- a/src/ifs/wl_surface/cursor.rs +++ b/src/ifs/wl_surface/cursor.rs @@ -8,7 +8,7 @@ use { rect::Rect, renderer::Renderer, scale::Scale, - tree::{Node, NodeVisitorBase, OutputNode}, + tree::{Node, NodeLocation, NodeVisitorBase, OutputNode}, }, std::{cell::Cell, ops::Deref, rc::Rc}, }; @@ -133,7 +133,8 @@ impl Cursor for CursorSurface { } fn set_output(&self, output: &Rc) { - self.surface.set_output(output); + self.surface + .set_output(output, NodeLocation::Output(output.id)); } fn handle_set(self: Rc) { 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 578edb43..5bce2961 100644 --- a/src/ifs/wl_surface/ext_session_lock_surface_v1.rs +++ b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs @@ -10,7 +10,10 @@ use { leaks::Tracker, object::{Object, Version}, rect::Rect, - tree::{FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode}, + tree::{ + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, + OutputNode, + }, utils::numcell::NumCell, wire::{ExtSessionLockSurfaceV1Id, WlSurfaceId, ext_session_lock_surface_v1::*}, }, @@ -131,6 +134,10 @@ impl Node for ExtSessionLockSurfaceV1 { self.output.node() } + fn node_location(&self) -> Option { + self.surface.node_location() + } + 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 a0082e80..d2acefae 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, NodeVisitor, OutputNode, - StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, + OutputNode, StackedNode, }, utils::{ copyhashmap::CopyHashMap, @@ -300,6 +300,10 @@ impl Node for T { self.data().output.node() } + fn node_location(&self) -> Option { + self.data().surface.node_location() + } + fn node_find_tree_at( &self, x: i32, @@ -328,7 +332,8 @@ fn install(item: &Rc) -> Result<(), TrayItemError> { data.surface.ext.set(item.clone()); data.surface.set_visible(false); if let Some(node) = data.output.node() { - data.surface.set_output(&node); + data.surface + .set_output(&node, NodeLocation::Output(node.id)); item.send_initial_configure(); } Ok(()) diff --git a/src/ifs/wl_surface/wl_subsurface.rs b/src/ifs/wl_surface/wl_subsurface.rs index 6dd1bf93..0b6935e5 100644 --- a/src/ifs/wl_surface/wl_subsurface.rs +++ b/src/ifs/wl_surface/wl_subsurface.rs @@ -186,7 +186,8 @@ impl WlSubsurface { update_children_attach(self)?; let (x, y) = self.parent.buffer_abs_pos.get().position(); self.surface.set_absolute_position(x, y); - self.surface.set_output(&self.parent.output.get()); + self.surface + .set_output(&self.parent.output.get(), self.parent.location.get()); Ok(()) } diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 07b3ae72..3e53d1f3 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -12,8 +12,9 @@ use { state::State, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeVisitor, OutputNode, StackedNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, + NodeLocation, NodeVisitor, OutputNode, StackedNode, TileDragDestination, ToplevelData, + ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, + default_tile_drag_destination, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, wire::WlSurfaceId, @@ -370,6 +371,10 @@ impl Node for Xwindow { self.toplevel_data.output_opt() } + fn node_location(&self) -> Option { + self.x.surface.node_location() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } @@ -446,7 +451,7 @@ impl ToplevelNodeBase for Xwindow { } fn tl_set_workspace_ext(&self, ws: &Rc) { - self.x.surface.set_output(&ws.output.get()); + self.x.surface.set_output(&ws.output.get(), ws.location()); } fn tl_change_extents_impl(self: Rc, rect: &Rect) { @@ -456,7 +461,9 @@ impl ToplevelNodeBase for Xwindow { if self.data.info.override_redirect.get() { let (x, y) = rect.center(); let output = self.data.state.find_closest_output(x, y).0; - self.x.surface.set_output(&output); + self.x + .surface + .set_output(&output, NodeLocation::Output(output.id)); } else { self.data .state diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index 46912531..b96e5eae 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -18,7 +18,9 @@ use { leaks::Tracker, object::Object, rect::Rect, - tree::{FindTreeResult, FoundNode, Node, OutputNode, StackedNode, WorkspaceNode}, + tree::{ + FindTreeResult, FoundNode, Node, NodeLocation, OutputNode, StackedNode, WorkspaceNode, + }, utils::{ clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -233,7 +235,7 @@ impl XdgSurface { fn set_workspace(&self, ws: &Rc) { self.workspace.set(Some(ws.clone())); - self.surface.set_output(&ws.output.get()); + self.surface.set_output(&ws.output.get(), ws.location()); let pu = self.popups.lock(); for pu in pu.values() { pu.popup.xdg.set_workspace(ws); @@ -241,7 +243,8 @@ impl XdgSurface { } pub fn set_output(&self, output: &Rc) { - self.surface.set_output(output); + self.surface + .set_output(output, NodeLocation::Output(output.id)); let pu = self.popups.lock(); for pu in pu.values() { pu.popup.xdg.set_output(output); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 95f56057..44d8e101 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, NodeVisitor, OutputNode, - StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, + OutputNode, StackedNode, }, utils::clonecell::CloneCell, wire::{XdgPopupId, xdg_popup::*}, @@ -319,6 +319,10 @@ impl Node for XdgPopup { self.xdg.workspace.get().map(|w| w.output.get()) } + fn node_location(&self) -> Option { + self.xdg.surface.node_location() + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index dd37baa5..4097ec19 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -27,7 +27,7 @@ use { state::State, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, + NodeLocation, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, ToplevelType, WorkspaceNode, default_tile_drag_destination, }, @@ -512,7 +512,7 @@ impl XdgToplevel { self.extents_changed(); if let Some(workspace) = self.xdg.workspace.get() { let output = workspace.output.get(); - surface.set_output(&output); + surface.set_output(&output, workspace.location()); } // { // let seats = surface.client.state.globals.lock_seats(); @@ -573,6 +573,10 @@ impl Node for XdgToplevel { self.toplevel_data.output_opt() } + fn node_location(&self) -> Option { + self.xdg.surface.node_location() + } + 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 a9d90935..643a562c 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, NodeVisitor, OutputNode, - StackedNode, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, NodeVisitor, + OutputNode, StackedNode, }, utils::{ bitflags::BitflagsExt, @@ -187,7 +187,8 @@ impl ZwlrLayerSurfaceV1 { } self.surface.ext.set(self.clone()); if let Some(output) = self.output.node() { - self.surface.set_output(&output); + self.surface + .set_output(&output, NodeLocation::Output(output.id)); } Ok(()) } @@ -658,6 +659,10 @@ impl Node for ZwlrLayerSurfaceV1 { self.output.node() } + fn node_location(&self) -> Option { + self.surface.node_location() + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/tree.rs b/src/tree.rs index 41dd7890..82298cfe 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -115,6 +115,15 @@ pub enum FindTreeUsecase { SelectWorkspace, } +#[derive(Copy, Clone)] +pub enum NodeLocation { + Workspace( + #[expect(dead_code)] OutputNodeId, + #[expect(dead_code)] WorkspaceNodeId, + ), + Output(#[expect(dead_code)] OutputNodeId), +} + pub trait Node: 'static { fn node_id(&self) -> NodeId; fn node_seat_state(&self) -> &NodeSeatState; @@ -123,6 +132,7 @@ pub trait Node: 'static { fn node_visible(&self) -> bool; fn node_absolute_position(&self) -> Rect; fn node_output(&self) -> Option>; + fn node_location(&self) -> Option; 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 950d23eb..49207dcb 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -18,8 +18,8 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, - NodeId, OutputNode, TddType, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_bounds, + NodeId, NodeLocation, OutputNode, TddType, TileDragDestination, ToplevelData, + ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_bounds, toplevel_set_floating, walker::NodeVisitor, }, utils::{ @@ -129,6 +129,7 @@ pub struct ContainerNode { focus_history: LinkedList>, child_nodes: RefCell>>, workspace: CloneCell>, + location: Cell, cursors: RefCell>, state: Rc, pub render_data: RefCell, @@ -235,6 +236,7 @@ impl ContainerNode { focus_history: Default::default(), child_nodes: RefCell::new(child_nodes), workspace: CloneCell::new(workspace.clone()), + location: Cell::new(workspace.location()), cursors: RefCell::new(Default::default()), state: state.clone(), render_data: Default::default(), @@ -1547,6 +1549,10 @@ impl Node for ContainerNode { self.toplevel_data.output_opt() } + fn node_location(&self) -> Option { + Some(self.location.get()) + } + 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); @@ -2085,6 +2091,7 @@ impl ToplevelNodeBase for ContainerNode { fn tl_set_workspace_ext(&self, ws: &Rc) { self.workspace.set(ws.clone()); + self.location.set(ws.location()); for child in self.children.iter() { child.node.clone().tl_set_workspace(ws); } diff --git a/src/tree/display.rs b/src/tree/display.rs index 91b89433..eaa5fe2b 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -8,8 +8,9 @@ use { renderer::Renderer, state::State, tree::{ - FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, OutputNode, StackedNode, - TileDragDestination, WorkspaceDragDestination, WorkspaceNodeId, walker::NodeVisitor, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, OutputNode, + StackedNode, TileDragDestination, WorkspaceDragDestination, WorkspaceNodeId, + walker::NodeVisitor, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, @@ -150,6 +151,10 @@ impl Node for DisplayNode { None } + fn node_location(&self) -> Option { + None + } + fn node_find_tree_at( &self, x: i32, diff --git a/src/tree/float.rs b/src/tree/float.rs index f712c045..db3beb73 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, - OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, - toplevel_set_floating, walker::NodeVisitor, + NodeLocation, OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, + WorkspaceNode, toplevel_set_floating, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -45,6 +45,7 @@ pub struct FloatNode { pub workspace_link: Cell>>>, pub pinned_link: RefCell>>>, pub workspace: CloneCell>, + pub location: Cell, pub child: CloneCell>>, pub active: Cell, pub seat_state: NodeSeatState, @@ -124,6 +125,7 @@ impl FloatNode { workspace_link: Cell::new(None), pinned_link: RefCell::new(None), workspace: CloneCell::new(ws.clone()), + location: Cell::new(ws.location()), child: CloneCell::new(Some(child.clone())), active: Cell::new(false), seat_state: Default::default(), @@ -423,6 +425,7 @@ impl FloatNode { self.workspace_link .set(Some(ws.stacked.add_last(self.clone()))); self.workspace.set(ws.clone()); + self.location.set(ws.location()); if update_visible { self.stacked_set_visible(ws.float_visible()); } @@ -702,6 +705,10 @@ impl Node for FloatNode { Some(self.workspace.get().output.get()) } + fn node_location(&self) -> Option { + Some(self.location.get()) + } + 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 928531f2..8588fb56 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, PinnedNode, - StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, WorkspaceNode, - WorkspaceNodeId, walker::NodeVisitor, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLocation, + PinnedNode, StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, + WorkspaceNode, WorkspaceNodeId, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, bitflags::BitflagsExt, clonecell::CloneCell, @@ -718,6 +718,7 @@ impl OutputNode { state: self.state.clone(), is_dummy: false, output: CloneCell::new(self.clone()), + output_id: Cell::new(self.id), position: Cell::new(Default::default()), container: Default::default(), stacked: Default::default(), @@ -1476,6 +1477,10 @@ impl Node for OutputNode { self.global.opt.node() } + fn node_location(&self) -> Option { + Some(NodeLocation::Output(self.id)) + } + 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 b814b207..7041d2a6 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -11,8 +11,8 @@ use { text::TextTexture, tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelType, default_tile_drag_destination, + NodeLocation, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, + ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, }, utils::{ asyncevent::AsyncEvent, errorfmt::ErrorFmt, on_drop_event::OnDropEvent, @@ -35,6 +35,7 @@ pub struct PlaceholderNode { destroyed: Cell, update_textures_scheduled: Cell, state: Rc, + location: Cell>, pub textures: RefCell>, } @@ -63,6 +64,7 @@ impl PlaceholderNode { destroyed: Default::default(), update_textures_scheduled: Cell::new(false), state: state.clone(), + location: Cell::new(node.node_location()), textures: Default::default(), } } @@ -82,6 +84,7 @@ impl PlaceholderNode { destroyed: Default::default(), update_textures_scheduled: Default::default(), state: state.clone(), + location: Default::default(), textures: Default::default(), } } @@ -174,6 +177,10 @@ impl Node for PlaceholderNode { self.toplevel.output_opt() } + fn node_location(&self) -> Option { + self.location.get() + } + fn node_do_focus(self: Rc, seat: &Rc, _direction: Direction) { seat.focus_toplevel(self.clone()); } @@ -223,6 +230,10 @@ impl ToplevelNodeBase for PlaceholderNode { &self.toplevel } + fn tl_set_workspace_ext(&self, ws: &Rc) { + self.location.set(ws.node_location()); + } + fn tl_change_extents_impl(self: Rc, rect: &Rect) { self.toplevel.pos.set(*rect); if let Some(p) = self.toplevel.parent.get() { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index f6ec75b5..643b12eb 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -21,8 +21,8 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, - NodeId, NodeVisitorBase, OutputNode, PlaceholderNode, StackedNode, ToplevelNode, - container::ContainerNode, walker::NodeVisitor, + NodeId, NodeLocation, NodeVisitorBase, OutputNode, OutputNodeId, PlaceholderNode, + StackedNode, ToplevelNode, container::ContainerNode, walker::NodeVisitor, }, utils::{ clonecell::CloneCell, @@ -49,6 +49,7 @@ pub struct WorkspaceNode { pub state: Rc, pub is_dummy: bool, pub output: CloneCell>, + pub output_id: Cell, pub position: Cell, pub container: CloneCell>>, pub stacked: LinkedList>, @@ -104,6 +105,7 @@ impl WorkspaceNode { } pub fn set_output(&self, output: &Rc) { + self.output_id.set(output.id); let old = self.output.set(output.clone()); for wh in self.ext_workspaces.lock().values() { wh.handle_new_output(output); @@ -113,12 +115,13 @@ impl WorkspaceNode { } self.update_has_captures(); struct OutputSetter<'a> { + ws: &'a WorkspaceNode, old: &'a Rc, new: &'a Rc, } impl NodeVisitorBase for OutputSetter<'_> { fn visit_surface(&mut self, node: &Rc) { - node.set_output(self.new); + node.set_output(self.new, self.ws.location()); } fn visit_container(&mut self, node: &Rc) { @@ -147,6 +150,7 @@ impl WorkspaceNode { } } let mut visitor = OutputSetter { + ws: self, old: &old, new: output, }; @@ -273,6 +277,10 @@ impl WorkspaceNode { self.output.get().schedule_update_render_data(); } } + + pub fn location(&self) -> NodeLocation { + NodeLocation::Workspace(self.output_id.get(), self.id) + } } impl Node for WorkspaceNode { @@ -309,6 +317,10 @@ impl Node for WorkspaceNode { Some(self.output.get()) } + fn node_location(&self) -> Option { + Some(self.location()) + } + fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { if let Some(fs) = self.fullscreen.get() { fs.node_do_focus(seat, direction); From 4bfa9fb7fca32094e8d7d49ca8c927738b91e900 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 20:01:27 +0200 Subject: [PATCH 5/8] tree: add Node::node_make_visible --- src/ifs/wl_surface.rs | 6 ++ src/ifs/wl_surface/tray.rs | 4 ++ src/ifs/wl_surface/x_surface/xwindow.rs | 4 ++ src/ifs/wl_surface/xdg_surface.rs | 8 +++ src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 11 +++ .../wl_surface/xdg_surface/xdg_toplevel.rs | 8 +++ src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 4 ++ src/state.rs | 70 ++++++++++--------- src/tree.rs | 4 ++ src/tree/container.rs | 26 +++++++ src/tree/containing.rs | 1 + src/tree/float.rs | 11 +++ src/tree/placeholder.rs | 4 ++ src/tree/toplevel.rs | 9 +++ src/tree/workspace.rs | 11 +++ 15 files changed, 147 insertions(+), 34 deletions(-) diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 2307a353..473a38bc 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -1818,6 +1818,12 @@ impl Node for WlSurface { self.ext.get().tray_item() } + fn node_make_visible(self: Rc) { + if let Some(tl) = self.toplevel.get() { + tl.node_make_visible(); + } + } + fn node_on_key( &self, seat: &WlSeatGlobal, diff --git a/src/ifs/wl_surface/tray.rs b/src/ifs/wl_surface/tray.rs index d2acefae..558a6be8 100644 --- a/src/ifs/wl_surface/tray.rs +++ b/src/ifs/wl_surface/tray.rs @@ -212,6 +212,10 @@ impl XdgPopupParent for Popup { self.parent.node_visible() } + fn make_visible(self: Rc) { + // nothing + } + fn tray_item(&self) -> Option { Some(self.parent.data().tray_item_id) } diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 3e53d1f3..3f2895d3 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -412,6 +412,10 @@ impl Node for Xwindow { Some(self) } + fn node_make_visible(self: Rc) { + self.toplevel_data.make_visible(&*self); + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, _x: Fixed, _y: Fixed) { seat.enter_toplevel(self.clone()); } diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index b96e5eae..8116452b 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -146,6 +146,12 @@ impl XdgPopupParent for Popup { self.parent.surface.visible.get() } + fn make_visible(self: Rc) { + if let Some(ext) = self.parent.ext.get() { + ext.make_visible(); + } + } + fn tray_item(&self) -> Option { self.parent.clone().tray_item() } @@ -193,6 +199,8 @@ pub trait XdgSurfaceExt: Debug { fn tray_item(&self) -> Option { None } + + fn make_visible(self: Rc); } impl XdgSurface { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 44d8e101..1560242b 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -45,6 +45,7 @@ pub trait XdgPopupParent { fn has_workspace_link(&self) -> bool; fn post_commit(&self); fn visible(&self) -> bool; + fn make_visible(self: Rc); fn tray_item(&self) -> Option { None } @@ -344,6 +345,12 @@ impl Node for XdgPopup { Some(self.xdg.surface.client.clone()) } + fn node_make_visible(self: Rc) { + if let Some(parent) = self.parent.get() { + parent.make_visible(); + } + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, _x: Fixed, _y: Fixed) { seat.enter_popup(&self); } @@ -430,6 +437,10 @@ impl XdgSurfaceExt for XdgPopup { fn tray_item(&self) -> Option { self.parent.get()?.tray_item() } + + fn make_visible(self: Rc) { + self.node_make_visible(); + } } #[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 4097ec19..be3144e1 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -610,6 +610,10 @@ impl Node for XdgToplevel { Some(self) } + fn node_make_visible(self: Rc) { + self.toplevel_data.make_visible(&*self) + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, _x: Fixed, _y: Fixed) { seat.enter_toplevel(self.clone()); } @@ -780,6 +784,10 @@ impl XdgSurfaceExt for XdgToplevel { .state .damage(self.node_absolute_position()); } + + fn make_visible(self: Rc) { + self.node_make_visible(); + } } #[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 643a562c..1ff98f94 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -724,6 +724,10 @@ impl XdgPopupParent for Popup { fn visible(&self) -> bool { self.parent.node_visible() } + + fn make_visible(self: Rc) { + // nothing + } } object_base! { diff --git a/src/state.rs b/src/state.rs index 1ea2ac4a..08356aec 100644 --- a/src/state.rs +++ b/src/state.rs @@ -853,41 +853,28 @@ impl State { node.node_do_focus(&seat, Direction::Unspecified); } - pub fn show_workspace(&self, seat: &Rc, name: &str) { - let (output, ws) = match self.workspaces.get(name) { - Some(ws) => { - let output = ws.output.get(); - let mut pinned_is_focused = false; - for pinned in output.pinned.iter() { - pinned - .deref() - .clone() - .node_visit(&mut generic_node_visitor(|node| { - node.node_seat_state().for_each_kb_focus(|s| { - pinned_is_focused |= s.id() == seat.id(); - }); - })); - } - let did_change = output.show_workspace(&ws); - if !pinned_is_focused { - ws.clone().node_do_focus(seat, Direction::Unspecified); - } - if !did_change { - return; - } - (output, ws) + pub fn show_workspace2(&self, seat: Option<&Rc>, ws: &Rc) { + let output = ws.output.get(); + let mut pinned_is_focused = false; + if let Some(seat) = seat { + for pinned in output.pinned.iter() { + pinned + .deref() + .clone() + .node_visit(&mut generic_node_visitor(|node| { + node.node_seat_state().for_each_kb_focus(|s| { + pinned_is_focused |= s.id() == seat.id(); + }); + })); } - _ => { - let output = seat.get_output(); - if output.is_dummy { - log::warn!("Not showing workspace because seat is on dummy output"); - return; - } - let ws = output.create_workspace(name); - output.show_workspace(&ws); - (output, ws) - } - }; + } + let did_change = output.show_workspace(&ws); + if !pinned_is_focused && let Some(seat) = seat { + ws.clone().node_do_focus(seat, Direction::Unspecified); + } + if !did_change { + return; + } ws.flush_jay_workspaces(); output.schedule_update_render_data(); self.tree_changed(); @@ -897,6 +884,21 @@ impl State { // } } + pub fn show_workspace(&self, seat: &Rc, name: &str) { + let ws = match self.workspaces.get(name) { + Some(ws) => ws, + _ => { + let output = seat.get_output(); + if output.is_dummy { + log::warn!("Not showing workspace because seat is on dummy output"); + return; + } + output.create_workspace(name) + } + }; + self.show_workspace2(Some(seat), &ws); + } + pub fn float_map_ws(&self) -> Rc { if let Some(seat) = self.seat_queue.last() { let output = seat.get_output(); diff --git a/src/tree.rs b/src/tree.rs index 82298cfe..ede90beb 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -193,6 +193,10 @@ pub trait Node: 'static { None } + fn node_make_visible(self: Rc) { + // nothing + } + // EVENT HANDLERS fn node_on_key( diff --git a/src/tree/container.rs b/src/tree/container.rs index 49207dcb..bcf01e7a 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -1636,6 +1636,10 @@ impl Node for ContainerNode { Some(self) } + fn node_make_visible(self: Rc) { + self.toplevel_data.make_visible(&*self); + } + fn node_on_button( self: Rc, seat: &Rc, @@ -1919,6 +1923,28 @@ impl ContainingNode for ContainerNode { self.workspace.get() } + fn cnode_make_visible(self: Rc, child: &dyn Node) { + let Some(child) = self + .child_nodes + .borrow() + .get(&child.node_id()) + .map(|n| n.to_ref()) + else { + return; + }; + self.toplevel_data.make_visible(&*self); + if !self.node_visible() { + return; + } + let Some(cur) = self.mono_child.get() else { + return; + }; + if cur.node.node_id() == child.node.node_id() { + return; + } + self.activate_child(&child); + } + fn cnode_set_child_position(self: Rc, child: &dyn Node, x: i32, y: i32) { let Some(parent) = self.toplevel_data.parent.get() else { return; diff --git a/src/tree/containing.rs b/src/tree/containing.rs index 8cbdee0d..c8b2d9ba 100644 --- a/src/tree/containing.rs +++ b/src/tree/containing.rs @@ -12,6 +12,7 @@ pub trait ContainingNode: Node { fn cnode_accepts_child(&self, node: &dyn Node) -> bool; fn cnode_child_attention_request_changed(self: Rc, child: &dyn Node, set: bool); fn cnode_workspace(self: Rc) -> Rc; + fn cnode_make_visible(self: Rc, child: &dyn Node); fn cnode_set_child_position(self: Rc, child: &dyn Node, x: i32, y: i32) { let _ = child; let _ = x; diff --git a/src/tree/float.rs b/src/tree/float.rs index db3beb73..57f35461 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -752,6 +752,13 @@ impl Node for FloatNode { renderer.render_floating(self, x, y) } + fn node_make_visible(self: Rc) { + if self.visible.get() { + return; + } + self.workspace.get().cnode_make_visible(&*self); + } + fn node_on_button( self: Rc, seat: &Rc, @@ -904,6 +911,10 @@ impl ContainingNode for FloatNode { self.workspace.get() } + fn cnode_make_visible(self: Rc, _child: &dyn Node) { + self.node_make_visible(); + } + fn cnode_set_child_position(self: Rc, _child: &dyn Node, x: i32, y: i32) { let theme = &self.state.theme; let th = theme.sizes.title_height.get(); diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index 7041d2a6..e20c78d9 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -211,6 +211,10 @@ impl Node for PlaceholderNode { Some(self) } + fn node_make_visible(self: Rc) { + self.toplevel.make_visible(&*self); + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, _x: Fixed, _y: Fixed) { seat.pointer_cursor().set_known(KnownCursor::Default); seat.enter_toplevel(self.clone()); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 54444324..33def51e 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -866,6 +866,15 @@ impl ToplevelData { self.property_changed(TL_CHANGED_CONTENT_TY); } } + + pub fn make_visible(&self, slf: &dyn Node) { + if self.visible.get() { + return; + } + if let Some(parent) = self.parent.get() { + parent.cnode_make_visible(slf); + } + } } impl Drop for ToplevelData { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 643b12eb..ee254dba 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -359,6 +359,13 @@ impl Node for WorkspaceNode { renderer.render_workspace(self, x, y); } + fn node_make_visible(self: Rc) { + if self.is_dummy { + return; + } + self.state.show_workspace2(None, &self); + } + fn node_on_pointer_focus(&self, seat: &Rc) { // log::info!("workspace focus"); seat.pointer_cursor().set_known(KnownCursor::Default); @@ -434,6 +441,10 @@ impl ContainingNode for WorkspaceNode { fn cnode_workspace(self: Rc) -> Rc { self } + + fn cnode_make_visible(self: Rc, _child: &dyn Node) { + self.node_make_visible(); + } } pub struct WsMoveConfig { From 9941263a8260ad572423f7d47183c3ac22b75f53 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 20:02:04 +0200 Subject: [PATCH 6/8] linked-list: add LinkedList::rotate_last --- src/utils/linkedlist.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/linkedlist.rs b/src/utils/linkedlist.rs index 251c43a3..e531b116 100644 --- a/src/utils/linkedlist.rs +++ b/src/utils/linkedlist.rs @@ -97,6 +97,19 @@ impl LinkedList { self.root.append_existing(t) } + #[expect(dead_code)] + pub fn rotate_last(&self, t: &NodeRef) { + unsafe { + let root = self.root.data.as_ref(); + root.prev.get().as_ref().next.set(root.next.get()); + root.next.get().as_ref().prev.set(root.prev.get()); + root.prev.set(t.data); + root.next.set(t.data.as_ref().next.get()); + t.data.as_ref().next.get().as_ref().prev.set(self.root.data); + t.data.as_ref().next.set(self.root.data); + } + } + pub fn iter(&self) -> LinkedListIter { unsafe { let root = self.root.data.as_ref(); From d12234b38b24ccd2326337da01d3f515d8c7ccda Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 20:09:34 +0200 Subject: [PATCH 7/8] seat: add focus history --- jay-config/src/_private/client.rs | 19 ++- jay-config/src/_private/ipc.rs | 16 +- jay-config/src/input.rs | 30 ++++ src/config/handler.rs | 44 ++++- src/ifs/wl_seat.rs | 156 +++++++++++++++++- src/ifs/wl_seat/event_handling.rs | 55 +++++- src/ifs/wl_seat/kb_owner.rs | 2 +- src/tree.rs | 7 +- src/tree/display.rs | 6 +- src/tree/workspace.rs | 4 + src/utils/linkedlist.rs | 1 - src/utils/rc_eq.rs | 9 +- toml-config/src/config.rs | 5 +- toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/action.rs | 3 + toml-config/src/config/parsers/config.rs | 16 ++ .../src/config/parsers/focus_history.rs | 53 ++++++ toml-config/src/lib.rs | 12 ++ toml-spec/spec/spec.generated.json | 23 ++- toml-spec/spec/spec.generated.md | 58 +++++++ toml-spec/spec/spec.yaml | 48 ++++++ 21 files changed, 546 insertions(+), 22 deletions(-) create mode 100644 toml-config/src/config/parsers/focus_history.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 218fe46f..4c14d30e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -15,8 +15,8 @@ use { client::{Client, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ - FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, - capability::Capability, clickmethod::ClickMethod, + FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, Timeline, + acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, }, keyboard::{ Keymap, @@ -364,6 +364,21 @@ impl ConfigClient { self.send(&ClientMessage::GrabKb { kb, grab }); } + pub fn seat_focus_history(&self, seat: Seat, timeline: Timeline) { + self.send(&ClientMessage::SeatFocusHistory { seat, timeline }); + } + + pub fn seat_focus_history_set_only_visible(&self, seat: Seat, only_visible: bool) { + self.send(&ClientMessage::SeatFocusHistorySetOnlyVisible { seat, only_visible }); + } + + pub fn seat_focus_history_set_same_workspace(&self, seat: Seat, same_workspace: bool) { + self.send(&ClientMessage::SeatFocusHistorySetSameWorkspace { + seat, + same_workspace, + }); + } + 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 9ca66f39..9ad25abc 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -4,8 +4,8 @@ use { Axis, Direction, PciId, Workspace, client::{Client, ClientMatcher}, input::{ - FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, - capability::Capability, clickmethod::ClickMethod, + FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, Timeline, + acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, }, keyboard::{Keymap, mods::Modifiers, syms::KeySym}, logging::LogLevel, @@ -725,6 +725,18 @@ pub enum ClientMessage<'a> { show: bool, }, GetShowBar, + SeatFocusHistory { + seat: Seat, + timeline: Timeline, + }, + SeatFocusHistorySetOnlyVisible { + seat: Seat, + only_visible: bool, + }, + SeatFocusHistorySetSameWorkspace { + seat: Seat, + same_workspace: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 593bb079..c63cb809 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -182,6 +182,13 @@ impl InputDevice { } } +/// A direction in a timeline. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum Timeline { + Older, + Newer, +} + /// A seat. #[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] pub struct Seat(pub u64); @@ -273,6 +280,29 @@ impl Seat { get!().unbind(self, mod_sym.into()) } + /// Moves the focus in the focus history. + pub fn focus_history(self, timeline: Timeline) { + get!().seat_focus_history(self, timeline) + } + + /// Configures whether the focus history only includes visible windows. + /// + /// If this is `false`, then hidden windows will be made visible before moving the + /// focus to them. + /// + /// The default is `false`. + pub fn focus_history_set_only_visible(self, only_visible: bool) { + get!().seat_focus_history_set_only_visible(self, only_visible) + } + + /// Configures whether the focus history only includes windows on the same workspace + /// as the currently focused window. + /// + /// The default is `false`. + pub fn focus_history_set_same_workspace(self, same_workspace: bool) { + get!().seat_focus_history_set_same_workspace(self, same_workspace) + } + /// 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 63bd8ce7..d26ed820 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, + FocusFollowsMouseMode, InputDevice, Seat, Timeline, acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile}, capability::{ CAP_GESTURE, CAP_KEYBOARD, CAP_POINTER, CAP_SWITCH, CAP_TABLET_PAD, @@ -2150,11 +2150,41 @@ impl ConfigProxyHandler { .set(matcher, (m, tile_state)); Ok(()) } + fn handle_set_pointer_revert_key(&self, seat: Seat, key: KeySym) -> Result<(), CphError> { self.get_seat(seat)?.set_pointer_revert_key(key); Ok(()) } + fn handle_seat_focus_history(&self, seat: Seat, timeline: Timeline) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + match timeline { + Timeline::Older => seat.focus_prev(), + Timeline::Newer => seat.focus_next(), + } + Ok(()) + } + + fn handle_seat_focus_history_set_only_visible( + &self, + seat: Seat, + visible: bool, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.focus_history_set_visible(visible); + Ok(()) + } + + fn handle_seat_focus_history_set_same_workspace( + &self, + seat: Seat, + same_workspace: bool, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.focus_history_set_same_workspace(same_workspace); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2997,6 +3027,18 @@ impl ConfigProxyHandler { .wrn("get_content_type")?, ClientMessage::SetShowBar { show } => self.handle_set_show_bar(show), ClientMessage::GetShowBar => self.handle_get_show_bar(), + ClientMessage::SeatFocusHistory { seat, timeline } => self + .handle_seat_focus_history(seat, timeline) + .wrn("seat_focus_history")?, + ClientMessage::SeatFocusHistorySetOnlyVisible { seat, only_visible } => self + .handle_seat_focus_history_set_only_visible(seat, only_visible) + .wrn("seat_focus_history_set_only_visible")?, + ClientMessage::SeatFocusHistorySetSameWorkspace { + seat, + same_workspace, + } => self + .handle_seat_focus_history_set_same_workspace(seat, same_workspace) + .wrn("seat_focus_history_set_same_workspace")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 791d5781..1f0a8f9b 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -46,6 +46,7 @@ use { }, wl_output::WlOutputGlobal, wl_seat::{ + event_handling::FocusHistoryData, gesture_owner::GestureOwnerHolder, kb_owner::KbOwnerHolder, pointer_owner::PointerOwnerHolder, @@ -84,8 +85,14 @@ use { toplevel_parent_container, toplevel_set_floating, toplevel_set_workspace, }, utils::{ - asyncevent::AsyncEvent, bindings::PerClientBindings, clonecell::CloneCell, - copyhashmap::CopyHashMap, linkedlist::LinkedNode, numcell::NumCell, rc_eq::rc_eq, + asyncevent::AsyncEvent, + bindings::PerClientBindings, + clonecell::CloneCell, + copyhashmap::CopyHashMap, + linkedlist::{LinkedList, LinkedNode, NodeRef}, + numcell::NumCell, + on_drop::OnDrop, + rc_eq::{rc_eq, rc_weak_eq}, smallmap::SmallMap, }, wire::{ @@ -218,6 +225,11 @@ pub struct WlSeatGlobal { keyboard_node_serial: Cell, tray_popups: CopyHashMap<(TrayItemId, XdgPopupId), Rc>, revert_key: Cell, + last_focus_location: Cell>, + focus_history: LinkedList, + focus_history_rotate: NumCell, + focus_history_visible_only: Cell, + focus_history_same_workspace: Cell, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -292,6 +304,11 @@ impl WlSeatGlobal { ui_drag_highlight: Default::default(), tray_popups: Default::default(), revert_key: Cell::new(SYM_Escape), + last_focus_location: Default::default(), + focus_history: Default::default(), + focus_history_rotate: Default::default(), + focus_history_visible_only: Cell::new(false), + focus_history_same_workspace: Cell::new(false), }); slf.pointer_cursor.set_owner(slf.clone()); let seat = slf.clone(); @@ -649,6 +666,141 @@ impl WlSeatGlobal { } } + pub fn get_last_focus_on_workspace(&self, ws: &WorkspaceNode) -> Option> { + let mut node = self.focus_history.last()?; + loop { + if let Some(node) = node.node.upgrade() + && let Some(NodeLocation::Workspace(_, new)) = node.node_location() + && new == ws.id + { + return Some(node); + } + node = node.prev()?; + } + } + + fn get_focus_history( + &self, + next: impl Fn(&NodeRef) -> Option>, + first: impl FnOnce(&LinkedList) -> Option>, + ) -> Option<(Rc, bool)> { + let original = self.keyboard_node.get(); + let mut output = None; + let mut workspace = None; + if let Some(old) = original.node_location() { + match old { + NodeLocation::Workspace(o, w) => { + workspace = Some(w); + output = Some(o); + } + NodeLocation::Output(o) => { + output = Some(o); + } + } + } + if (output.is_none() || workspace.is_none()) + && let Some(old) = self.last_focus_location.get() + { + match old { + NodeLocation::Workspace(o, w) => { + workspace = workspace.or(Some(w)); + output = output.or(Some(o)); + } + NodeLocation::Output(o) => { + output = output.or(Some(o)); + } + } + } + if workspace.is_none() + && let Some(output) = original.node_output() + && let Some(ws) = output.workspace.get() + { + workspace = Some(ws.id); + } + let matches = |node: &FocusHistoryData| { + let visible = node.visible.get(); + if self.focus_history_visible_only.get() && !visible { + return None; + } + let node = node.node.upgrade()?; + if self.focus_history_same_workspace.get() { + let new = node.node_location()?; + let o = match new { + NodeLocation::Workspace(o, w) => { + if workspace != Some(w) { + return None; + } + o + } + NodeLocation::Output(o) => o, + }; + if output != Some(o) { + return None; + } + } + Some((node, visible)) + }; + let node = original.node_seat_state().get_focus_history(self); + if let Some(mut node) = node { + loop { + node = match next(&node) { + Some(n) => n, + _ => break, + }; + if let Some(matches) = matches(&node) { + return Some(matches); + } + } + } + let mut node = first(&self.focus_history)?; + loop { + if rc_weak_eq(&original, &node.node) { + return None; + } + if let Some(matches) = matches(&node) { + return Some(matches); + } + node = next(&node)?; + } + } + + fn focus_history( + self: &Rc, + next: impl Fn(&NodeRef) -> Option>, + first: impl FnOnce(&LinkedList) -> Option>, + ) { + let Some((node, visible)) = self.get_focus_history(next, first) else { + return; + }; + self.focus_history_rotate.fetch_add(1); + let _reset = OnDrop(|| { + self.focus_history_rotate.fetch_sub(1); + }); + if !visible { + node.clone().node_make_visible(); + if !node.node_visible() { + return; + } + } + self.focus_node(node); + } + + pub fn focus_prev(self: &Rc) { + self.focus_history(|s| s.prev(), |l| l.last()); + } + + pub fn focus_next(self: &Rc) { + self.focus_history(|s| s.next(), |l| l.first()); + } + + pub fn focus_history_set_visible(&self, visible: bool) { + self.focus_history_visible_only.set(visible); + } + + pub fn focus_history_set_same_workspace(&self, same_workspace: bool) { + self.focus_history_same_workspace.set(same_workspace); + } + fn set_selection_( self: &Rc, field: &CloneCell>>, diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index f2623573..f0229097 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -40,7 +40,10 @@ use { state::DeviceHandlerData, tree::{Direction, Node, ToplevelNode}, utils::{ - bitflags::BitflagsExt, hash_map_ext::HashMapExt, smallmap::SmallMap, + bitflags::BitflagsExt, + hash_map_ext::HashMapExt, + linkedlist::{LinkedNode, NodeRef}, + smallmap::{SmallMap, SmallMapMut}, syncqueue::SyncQueue, }, wire::WlDataOfferId, @@ -56,13 +59,20 @@ use { kbvm::{ModifierMask, state_machine::Event}, linearize::LinearizeExt, smallvec::SmallVec, - std::{cell::RefCell, collections::hash_map::Entry, mem, rc::Rc}, + std::{ + cell::{Cell, RefCell}, + collections::hash_map::Entry, + mem, + rc::{Rc, Weak}, + }, }; #[derive(Default)] pub struct NodeSeatState { pointer_foci: SmallMap, 1>, kb_foci: SmallMap, 1>, + no_focus_history: Cell, + kb_focus_histories: RefCell, 1>>, gesture_foci: SmallMap, 1>, touch_foci: SmallMap, 1>, pointer_grabs: SmallMap, 1>, @@ -72,6 +82,11 @@ pub struct NodeSeatState { ui_drags: SmallMap, 1>, } +pub struct FocusHistoryData { + pub visible: Cell, + pub node: Weak, +} + impl NodeSeatState { pub(super) fn enter(&self, seat: &Rc) { self.pointer_foci.insert(seat.id, seat.clone()); @@ -81,7 +96,26 @@ impl NodeSeatState { self.pointer_foci.remove(&seat.id); } - pub(super) fn focus(&self, seat: &Rc) -> bool { + pub fn disable_focus_history(&self) { + self.no_focus_history.set(true); + } + + pub(super) fn focus(&self, node: &Rc, seat: &Rc) -> bool { + if !self.no_focus_history.get() { + let hist = &mut *self.kb_focus_histories.borrow_mut(); + let hist = hist.get_or_insert_with(seat.id, || { + seat.focus_history.add_last(FocusHistoryData { + visible: Cell::new(node.node_visible()), + node: Rc::downgrade(node), + }) + }); + if seat.focus_history_rotate.is_zero() { + seat.last_focus_location.set(node.node_location()); + seat.focus_history.add_last_existing(hist); + } else { + seat.focus_history.rotate_last(hist); + } + } self.kb_foci.insert(seat.id, seat.clone()); self.kb_foci.len() == 1 } @@ -179,6 +213,10 @@ impl NodeSeatState { } pub fn destroy_node(&self, node: &dyn Node) { + for (_, entry) in self.kb_focus_histories.borrow_mut().iter_mut() { + entry.visible.set(false); + entry.detach(); + } self.destroy_node2(node, true); } @@ -223,11 +261,22 @@ impl NodeSeatState { } pub fn set_visible(&self, node: &dyn Node, visible: bool) { + for (_, entry) in self.kb_focus_histories.borrow_mut().iter_mut() { + entry.visible.set(visible); + } if !visible { self.destroy_node2(node, false); } } + pub(super) fn get_focus_history( + &self, + seat: &WlSeatGlobal, + ) -> Option> { + let hist = &*self.kb_focus_histories.borrow(); + Some(hist.get(&seat.id)?.to_ref()) + } + pub fn on_seat_remove(&self, seat: &WlSeatGlobal) { self.kb_foci.remove(&seat.id); self.pointer_foci.remove(&seat.id); diff --git a/src/ifs/wl_seat/kb_owner.rs b/src/ifs/wl_seat/kb_owner.rs index c6950062..447c6e5e 100644 --- a/src/ifs/wl_seat/kb_owner.rs +++ b/src/ifs/wl_seat/kb_owner.rs @@ -87,7 +87,7 @@ impl KbOwner for DefaultKbOwner { old.node_active_changed(false); } - if node.node_seat_state().focus(seat) { + if node.node_seat_state().focus(&node, seat) { node.node_active_changed(true); } // log::info!("focus {}", node.node_id()); diff --git a/src/tree.rs b/src/tree.rs index ede90beb..47191ab9 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -117,11 +117,8 @@ pub enum FindTreeUsecase { #[derive(Copy, Clone)] pub enum NodeLocation { - Workspace( - #[expect(dead_code)] OutputNodeId, - #[expect(dead_code)] WorkspaceNodeId, - ), - Output(#[expect(dead_code)] OutputNodeId), + Workspace(OutputNodeId, WorkspaceNodeId), + Output(OutputNodeId), } pub trait Node: 'static { diff --git a/src/tree/display.rs b/src/tree/display.rs index eaa5fe2b..a7aebf23 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -28,14 +28,16 @@ pub struct DisplayNode { impl DisplayNode { pub fn new(id: NodeId) -> Self { - Self { + let slf = Self { id, extents: Default::default(), outputs: Default::default(), stacked: Default::default(), stacked_above_layers: Default::default(), seat_state: Default::default(), - } + }; + slf.seat_state.disable_focus_history(); + slf } pub fn clear(&self) { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index ee254dba..8e265450 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -324,6 +324,10 @@ impl Node for WorkspaceNode { fn node_do_focus(self: Rc, seat: &Rc, direction: Direction) { if let Some(fs) = self.fullscreen.get() { fs.node_do_focus(seat, direction); + } else if self.stacked.is_not_empty() + && let Some(last) = seat.get_last_focus_on_workspace(&self) + { + seat.focus_node(last); } else if let Some(container) = self.container.get() { container.node_do_focus(seat, direction); } else if let Some(float) = self diff --git a/src/utils/linkedlist.rs b/src/utils/linkedlist.rs index e531b116..cd976314 100644 --- a/src/utils/linkedlist.rs +++ b/src/utils/linkedlist.rs @@ -97,7 +97,6 @@ impl LinkedList { self.root.append_existing(t) } - #[expect(dead_code)] pub fn rotate_last(&self, t: &NodeRef) { unsafe { let root = self.root.data.as_ref(); diff --git a/src/utils/rc_eq.rs b/src/utils/rc_eq.rs index 9c7e855c..5f0e1bea 100644 --- a/src/utils/rc_eq.rs +++ b/src/utils/rc_eq.rs @@ -1,9 +1,16 @@ -use std::{ops::Deref, rc::Rc}; +use std::{ + ops::Deref, + rc::{Rc, Weak}, +}; pub fn rc_eq(a: &Rc, b: &Rc) -> bool { Rc::as_ptr(a) as *const u8 == Rc::as_ptr(b) as *const u8 } +pub fn rc_weak_eq(a: &Rc, b: &Weak) -> bool { + Rc::as_ptr(a) as *const u8 == b.as_ptr() as *const u8 +} + #[derive(Default)] pub struct RcEq(pub Rc); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index ac2bdccf..758f6070 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -15,6 +15,7 @@ use { color_management::ColorManagement, config::{ConfigParser, ConfigParserError}, float::Float, + focus_history::FocusHistory, }, }, toml::{self}, @@ -22,7 +23,7 @@ use { ahash::AHashMap, jay_config::{ Axis, Direction, Workspace, - input::{SwitchEvent, acceleration::AccelProfile, clickmethod::ClickMethod}, + input::{SwitchEvent, Timeline, acceleration::AccelProfile, clickmethod::ClickMethod}, keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym}, logging::LogLevel, status::MessageFormat, @@ -70,6 +71,7 @@ pub enum SimpleCommand { KillClient, ShowBar(bool), ToggleBar, + FocusHistory(Timeline), } #[derive(Debug, Clone)] @@ -486,6 +488,7 @@ pub struct Config { pub pointer_revert_key: Option, pub use_hardware_cursor: Option, pub show_bar: Option, + pub focus_history: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 70fbf39e..15952e3c 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -21,6 +21,7 @@ mod drm_device_match; mod env; pub mod exec; pub mod float; +pub mod focus_history; mod format; mod gfx_api; mod idle; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index c0a39075..9322a784 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -34,6 +34,7 @@ use { jay_config::{ Axis::{Horizontal, Vertical}, get_workspace, + input::Timeline, }, thiserror::Error, }; @@ -136,6 +137,8 @@ impl ActionParser<'_> { "show-bar" => ShowBar(true), "hide-bar" => ShowBar(false), "toggle-bar" => ToggleBar, + "focus-prev" => FocusHistory(Timeline::Older), + "focus-next" => FocusHistory(Timeline::Newer), _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 07d61fcd..41bd364b 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -16,6 +16,7 @@ use { drm_device_match::DrmDeviceMatchParser, env::EnvParser, float::FloatParser, + focus_history::FocusHistoryParser, gfx_api::GfxApiParser, idle::IdleParser, input::InputsParser, @@ -133,6 +134,7 @@ impl Parser for ConfigParser<'_> { pointer_revert_key_str, use_hardware_cursor, show_bar, + focus_history_val, ), ) = ext.extract(( ( @@ -181,6 +183,7 @@ impl Parser for ConfigParser<'_> { recover(opt(str("pointer-revert-key"))), recover(opt(bol("use-hardware-cursor"))), recover(opt(bol("show-bar"))), + opt(val("focus-history")), ), ))?; let mut keymap = None; @@ -458,6 +461,18 @@ impl Parser for ConfigParser<'_> { None => log::warn!("Unknown keysym: {}", self.0.error3(value.span)), } } + let mut focus_history = None; + if let Some(value) = focus_history_val { + match value.parse(&mut FocusHistoryParser(self.0)) { + Ok(v) => focus_history = Some(v), + Err(e) => { + log::warn!( + "Could not parse the focus-history settings: {}", + self.0.error(e) + ); + } + } + } Ok(Config { keymap, repeat_rate, @@ -497,6 +512,7 @@ impl Parser for ConfigParser<'_> { pointer_revert_key, use_hardware_cursor: use_hardware_cursor.despan(), show_bar: show_bar.despan(), + focus_history, }) } } diff --git a/toml-config/src/config/parsers/focus_history.rs b/toml-config/src/config/parsers/focus_history.rs new file mode 100644 index 00000000..8aef1ebd --- /dev/null +++ b/toml-config/src/config/parsers/focus_history.rs @@ -0,0 +1,53 @@ +use { + crate::{ + config::{ + context::Context, + extractor::{Extractor, ExtractorError, bol, opt, recover}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum FocusHistoryParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct FocusHistoryParser<'a>(pub &'a Context<'a>); + +#[derive(Debug, Clone)] +pub struct FocusHistory { + pub only_visible: Option, + pub same_workspace: Option, +} + +impl Parser for FocusHistoryParser<'_> { + type Value = FocusHistory; + type Error = FocusHistoryParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (only_visible, same_workspace) = ext.extract(( + recover(opt(bol("only-visible"))), + recover(opt(bol("same-workspace"))), + ))?; + Ok(FocusHistory { + only_visible: only_visible.despan(), + same_workspace: same_workspace.despan(), + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 95191f34..b13733ce 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -156,6 +156,10 @@ impl Action { SimpleCommand::KillClient => client_action!(c, c.kill()), SimpleCommand::ShowBar(show) => B::new(move || set_show_bar(show)), SimpleCommand::ToggleBar => B::new(toggle_show_bar), + SimpleCommand::FocusHistory(timeline) => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.focus_history(timeline)) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -1252,6 +1256,14 @@ fn load_config(initial_load: bool, persistent: &Rc) { if let Some(v) = config.show_bar { set_show_bar(v); } + if let Some(v) = config.focus_history { + if let Some(v) = v.only_visible { + persistent.seat.focus_history_set_only_visible(v); + } + if let Some(v) = v.same_workspace { + persistent.seat.focus_history_set_same_workspace(v); + } + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 369a8835..18acd81d 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -887,6 +887,10 @@ "show-bar": { "type": "boolean", "description": "Configures whether the built-in bar is shown.\n\nThe default is `true`.\n" + }, + "focus-history": { + "description": "Configures the focus-history settings.\n\n- Example:\n\n ```toml\n [focus-history]\n only-visible: true\n same-workspace: true\n ```\n", + "$ref": "#/$defs/FocusHistory" } }, "required": [] @@ -1094,6 +1098,21 @@ }, "required": [] }, + "FocusHistory": { + "description": "Describes settings of the focus history.\n\n- Example:\n\n ```toml\n [focus-history]\n only-visible: true\n same-workspace: true\n ```\n", + "type": "object", + "properties": { + "only-visible": { + "type": "boolean", + "description": "Sets whether the focus history only moves to windows that are already visible.\n\nIf this is false, then the window will be made visible before focusing it.\n\nThe default is `false`.\n" + }, + "same-workspace": { + "type": "boolean", + "description": "Sets whether the focus history only moves to windows on the same workspace.\n\nThe default is `false`.\n" + } + }, + "required": [] + }, "Format": { "type": "string", "description": "A graphics format.\n\nThese formats are documented in https://github.com/torvalds/linux/blob/master/include/uapi/drm/drm_fourcc.h\n\n- Example:\n\n ```toml\n [[outputs]]\n match.serial-number = \"33K03894SL0\"\n format = \"rgb565\"\n ```\n", @@ -1593,7 +1612,9 @@ "kill-client", "show-bar", "hide-bar", - "toggle-bar" + "toggle-bar", + "focus-prev", + "focus-next" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index c4f5f56a..3229d088 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1802,6 +1802,20 @@ The table has the following fields: The value of this field should be a boolean. +- `focus-history` (optional): + + Configures the focus-history settings. + + - Example: + + ```toml + [focus-history] + only-visible: true + same-workspace: true + ``` + + The value of this field should be a [FocusHistory](#types-FocusHistory). + ### `Connector` @@ -2232,6 +2246,42 @@ The table has the following fields: The value of this field should be a boolean. + +### `FocusHistory` + +Describes settings of the focus history. + +- Example: + + ```toml + [focus-history] + only-visible: true + same-workspace: true + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `only-visible` (optional): + + Sets whether the focus history only moves to windows that are already visible. + + If this is false, then the window will be made visible before focusing it. + + The default is `false`. + + The value of this field should be a boolean. + +- `same-workspace` (optional): + + Sets whether the focus history only moves to windows on the same workspace. + + The default is `false`. + + The value of this field should be a boolean. + + ### `Format` @@ -3617,6 +3667,14 @@ The string should have one of the following values: Toggles the built-in bar. +- `focus-prev`: + + Focuses the previous window in the focus history. + +- `focus-next`: + + Focuses the next window in the focus history. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 76a142d3..b69f1177 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -860,6 +860,10 @@ SimpleActionName: description: Hides the built-in bar. - value: toggle-bar description: Toggles the built-in bar. + - value: focus-prev + description: Focuses the previous window in the focus history. + - value: focus-next + description: Focuses the next window in the focus history. Color: @@ -2627,6 +2631,19 @@ Config: Configures whether the built-in bar is shown. The default is `true`. + focus-history: + ref: FocusHistory + required: false + description: | + Configures the focus-history settings. + + - Example: + + ```toml + [focus-history] + only-visible: true + same-workspace: true + ``` Idle: @@ -3716,3 +3733,34 @@ ContentTypeMask: description: An array of masks that are OR'd. items: ref: ContentTypeMask + + +FocusHistory: + kind: table + description: | + Describes settings of the focus history. + + - Example: + + ```toml + [focus-history] + only-visible: true + same-workspace: true + ``` + fields: + only-visible: + description: | + Sets whether the focus history only moves to windows that are already visible. + + If this is false, then the window will be made visible before focusing it. + + The default is `false`. + kind: boolean + required: false + same-workspace: + description: | + Sets whether the focus history only moves to windows on the same workspace. + + The default is `false`. + kind: boolean + required: false From e85accf8052d333178b7e64994d674598f36e8ee Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 18 Jul 2025 21:00:52 +0200 Subject: [PATCH 8/8] workspace: use State::show_workspace2 to make workspaces visible --- .../ext_workspace_manager_v1.rs | 8 +++---- src/state.rs | 20 ++++++++++-------- src/tree/output.rs | 21 +++++-------------- src/tree/workspace.rs | 7 +++---- 4 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/ifs/workspace_manager/ext_workspace_manager_v1.rs b/src/ifs/workspace_manager/ext_workspace_manager_v1.rs index 0893353e..c050f582 100644 --- a/src/ifs/workspace_manager/ext_workspace_manager_v1.rs +++ b/src/ifs/workspace_manager/ext_workspace_manager_v1.rs @@ -250,10 +250,10 @@ impl ExtWorkspaceManagerV1RequestHandler for ExtWorkspaceManagerV1 { continue; }; let output = ws.output.get(); - output.show_workspace(&ws); - ws.flush_jay_workspaces(); - output.schedule_update_render_data(); - self.client.state.tree_changed(); + let seat = self.client.state.seat_queue.last().as_deref().cloned(); + self.client + .state + .show_workspace2(seat.as_ref(), &output, &ws); } WorkspaceChange::AssignWorkspace(w, o) => { let Some(ws) = w.get() else { diff --git a/src/state.rs b/src/state.rs index 08356aec..28fa3f35 100644 --- a/src/state.rs +++ b/src/state.rs @@ -853,8 +853,12 @@ impl State { node.node_do_focus(&seat, Direction::Unspecified); } - pub fn show_workspace2(&self, seat: Option<&Rc>, ws: &Rc) { - let output = ws.output.get(); + pub fn show_workspace2( + &self, + seat: Option<&Rc>, + output: &Rc, + ws: &Rc, + ) { let mut pinned_is_focused = false; if let Some(seat) = seat { for pinned in output.pinned.iter() { @@ -876,12 +880,10 @@ impl State { return; } ws.flush_jay_workspaces(); - output.schedule_update_render_data(); - self.tree_changed(); - // let seats = self.globals.seats.lock(); - // for seat in seats.values() { - // seat.workspace_changed(&output); - // } + if !output.is_dummy { + output.schedule_update_render_data(); + self.tree_changed(); + } } pub fn show_workspace(&self, seat: &Rc, name: &str) { @@ -896,7 +898,7 @@ impl State { output.create_workspace(name) } }; - self.show_workspace2(Some(seat), &ws); + self.show_workspace2(Some(seat), &ws.output.get(), &ws); } pub fn float_map_ws(&self) -> Rc { diff --git a/src/tree/output.rs b/src/tree/output.rs index 8588fb56..f5db42c1 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -1081,7 +1081,7 @@ impl OutputNode { set_layer_visible!(self.layers[3], visible); } - fn button(self: Rc, id: PointerType) { + fn button(self: Rc, seat: &Rc, id: PointerType) { if !self.state.show_bar.get() { return; } @@ -1105,10 +1105,7 @@ impl OutputNode { } return; }; - self.show_workspace(&ws); - ws.flush_jay_workspaces(); - self.schedule_update_render_data(); - self.state.tree_changed(); + self.state.show_workspace2(Some(seat), &self, &ws); } pub fn update_presentation_type(&self) { @@ -1628,7 +1625,7 @@ impl Node for OutputNode { self.pointer_down.remove(&seat.id()); return; } - self.button(PointerType::Seat(seat.id())); + self.button(seat, PointerType::Seat(seat.id())); } fn node_on_axis_event(self: Rc, seat: &Rc, event: &PendingScroll) { @@ -1658,15 +1655,7 @@ impl Node for OutputNode { None => break, }; } - if !self.show_workspace(&ws) { - return; - } - ws.flush_jay_workspaces(); - ws.deref() - .clone() - .node_do_focus(seat, Direction::Unspecified); - self.schedule_update_render_data(); - self.state.tree_changed(); + self.state.show_workspace2(Some(seat), &self, &ws); } fn node_on_leave(&self, seat: &WlSeatGlobal) { @@ -1730,7 +1719,7 @@ impl Node for OutputNode { self.pointer_move(id, x, y); if let Some(changes) = changes { if changes.down == Some(true) { - self.button(id); + self.button(tool.seat(), id); } } } diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 8e265450..8b118bef 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -367,7 +367,7 @@ impl Node for WorkspaceNode { if self.is_dummy { return; } - self.state.show_workspace2(None, &self); + self.state.show_workspace2(None, &self.output.get(), &self); } fn node_on_pointer_focus(&self, seat: &Rc) { @@ -499,14 +499,13 @@ pub fn move_ws_to_output( && (config.make_visible_always || (config.make_visible_if_empty && target.workspace.is_none())); if make_visible { - target.show_workspace(&ws); + ws.state.show_workspace2(None, target, &ws); } else { ws.set_visible(false); } ws.flush_jay_workspaces(); if let Some(ws) = new_source_ws { - source.show_workspace(&ws); - ws.flush_jay_workspaces(); + ws.state.show_workspace2(None, &source, &ws); } if !target.is_dummy { target.schedule_update_render_data();