From a975e3b25abf984201ddf9dfdeb0afabfdf50a5c Mon Sep 17 00:00:00 2001 From: khyperia <953151+khyperia@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:57:02 +0100 Subject: [PATCH 1/3] seat: rename get_output to get_cursor_output --- jay-config/src/_private/client.rs | 6 +++--- jay-config/src/_private/ipc.rs | 4 ++-- jay-config/src/input.rs | 2 +- src/config/handler.rs | 17 +++++++++-------- src/ifs/wl_seat.rs | 2 +- src/ifs/wl_seat/event_handling.rs | 2 +- src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs | 2 +- src/ifs/xdg_toplevel_drag_v1.rs | 2 +- src/ifs/zwlr_layer_shell_v1.rs | 2 +- src/state.rs | 6 +++--- 10 files changed, 23 insertions(+), 22 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 31172667..a604cf02 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -551,9 +551,9 @@ impl ConfigClient { connector } - pub fn get_seat_workspace(&self, seat: Seat) -> Workspace { - let res = self.send_with_response(&ClientMessage::GetSeatWorkspace { seat }); - get_response!(res, Workspace(0), GetSeatWorkspace { workspace }); + pub fn get_seat_cursor_workspace(&self, seat: Seat) -> Workspace { + let res = self.send_with_response(&ClientMessage::GetSeatCursorWorkspace { seat }); + get_response!(res, Workspace(0), GetSeatCursorWorkspace { workspace }); workspace } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 1b89c8b9..842ff141 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -372,7 +372,7 @@ pub enum ClientMessage<'a> { MakeRenderDevice { device: DrmDevice, }, - GetSeatWorkspace { + GetSeatCursorWorkspace { seat: Seat, }, SetDefaultWorkspaceCapture { @@ -925,7 +925,7 @@ pub enum Response { width: i32, height: i32, }, - GetSeatWorkspace { + GetSeatCursorWorkspace { workspace: Workspace, }, GetDefaultWorkspaceCapture { diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 4672663c..ab5de258 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -420,7 +420,7 @@ impl Seat { /// /// If no such workspace exists, `exists` returns `false` for the returned workspace. pub fn get_workspace(self) -> Workspace { - get!(Workspace(0)).get_seat_workspace(self) + get!(Workspace(0)).get_seat_cursor_workspace(self) } /// Returns the workspace that is currently active on the output that contains the seat's diff --git a/src/config/handler.rs b/src/config/handler.rs index cc5f1eb5..877e3f38 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1012,16 +1012,16 @@ impl ConfigProxyHandler { self.state.double_click_distance.set(dist); } - fn handle_get_seat_workspace(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_cursor_workspace(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; - let output = seat.get_output(); + let output = seat.get_cursor_output(); let mut workspace = Workspace(0); if !output.is_dummy && let Some(ws) = output.workspace.get() { workspace = self.get_workspace_by_name(&ws.name); } - self.respond(Response::GetSeatWorkspace { workspace }); + self.respond(Response::GetSeatCursorWorkspace { workspace }); Ok(()) } @@ -1056,7 +1056,7 @@ impl ConfigProxyHandler { let name = self.get_workspace(ws)?; let workspace = match self.state.workspaces.get(name.deref()) { Some(ws) => ws, - _ => seat.get_output().create_workspace(name.deref()), + _ => seat.get_cursor_output().create_workspace(name.deref()), }; seat.set_workspace(&workspace); Ok(()) @@ -1112,7 +1112,8 @@ impl ConfigProxyHandler { Some(ws) => ws, _ => return Ok(()), }, - WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_output().workspace.get() { + WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_cursor_output().workspace.get() + { Some(ws) => ws, _ => return Ok(()), }, @@ -2935,9 +2936,9 @@ impl ConfigProxyHandler { ClientMessage::MakeRenderDevice { device } => self .handle_make_render_device(device) .wrn("make_render_device")?, - ClientMessage::GetSeatWorkspace { seat } => self - .handle_get_seat_workspace(seat) - .wrn("get_seat_workspace")?, + ClientMessage::GetSeatCursorWorkspace { seat } => self + .handle_get_seat_cursor_workspace(seat) + .wrn("get_seat_cursor_workspace")?, ClientMessage::GetSeatKeyboardWorkspace { seat } => self .handle_get_seat_keyboard_workspace(seat) .wrn("get_seat_keyboard_workspace")?, diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b746e3dc..866754d9 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -457,7 +457,7 @@ impl WlSeatGlobal { self.data_control_devices.remove(&device.id()); } - pub fn get_output(&self) -> Rc { + pub fn get_cursor_output(&self) -> Rc { self.cursor_user_group.latest_output() } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index ddec3ad8..fd0aee29 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -217,7 +217,7 @@ impl NodeSeatState { .set_kb_node(&seat, seat.state.root.clone(), seat.state.next_serial(None)); // log::info!("keyboard_node = root"); if focus_last { - seat.get_output() + seat.get_cursor_output() .node_do_focus(&seat, Direction::Unspecified); } } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 8a2808e7..8a74a8c2 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -486,7 +486,7 @@ impl XdgToplevel { if should_be_mapped { if !self.is_mapped.replace(true) { if let Some(seat) = drag.source.data.seat.get() { - self.xdg.set_output(&seat.get_output()); + self.xdg.set_output(&seat.get_cursor_output()); } self.toplevel_data.broadcast(self.clone()); self.tl_set_visible(self.state.root_visible()); diff --git a/src/ifs/xdg_toplevel_drag_v1.rs b/src/ifs/xdg_toplevel_drag_v1.rs index 61d4590c..3234be47 100644 --- a/src/ifs/xdg_toplevel_drag_v1.rs +++ b/src/ifs/xdg_toplevel_drag_v1.rs @@ -136,7 +136,7 @@ impl XdgToplevelDragV1 { if self.source.data.was_used() && let Some(tl) = self.toplevel.get() { - let output = seat.get_output(); + let output = seat.get_cursor_output(); let (x, y) = seat.pointer_cursor().position(); tl.drag.take(); tl.after_toplevel_drag( diff --git a/src/ifs/zwlr_layer_shell_v1.rs b/src/ifs/zwlr_layer_shell_v1.rs index 53e63178..0c9f3fa7 100644 --- a/src/ifs/zwlr_layer_shell_v1.rs +++ b/src/ifs/zwlr_layer_shell_v1.rs @@ -60,7 +60,7 @@ impl ZwlrLayerShellV1RequestHandler for ZwlrLayerShellV1 { self.client.lookup(req.output)?.global.clone() } else { for seat in self.client.state.seat_queue.rev_iter() { - let output = seat.get_output(); + let output = seat.get_cursor_output(); if !output.is_dummy { break 'get_output output.global.opt.clone(); } diff --git a/src/state.rs b/src/state.rs index 94f5f8d7..cd3e0058 100644 --- a/src/state.rs +++ b/src/state.rs @@ -780,7 +780,7 @@ impl State { pub fn ensure_map_workspace(&self, seat: Option<&Rc>) -> Rc { seat.cloned() .or_else(|| self.seat_queue.last().map(|s| s.deref().clone())) - .map(|s| s.get_output()) + .map(|s| s.get_cursor_output()) .or_else(|| self.root.outputs.lock().values().next().cloned()) .or_else(|| self.dummy_output.get()) .unwrap() @@ -916,7 +916,7 @@ impl State { let ws = match self.workspaces.get(name) { Some(ws) => ws, _ => { - let output = output.unwrap_or_else(|| seat.get_output()); + let output = output.unwrap_or_else(|| seat.get_cursor_output()); if output.is_dummy { log::warn!("Not showing workspace because seat is on dummy output"); return; @@ -929,7 +929,7 @@ impl State { pub fn float_map_ws(&self) -> Rc { if let Some(seat) = self.seat_queue.last() { - let output = seat.get_output(); + let output = seat.get_cursor_output(); if !output.is_dummy { return output.ensure_workspace(); } From dd3f8bad40aae999aefcfbef1d63da92d8175655 Mon Sep 17 00:00:00 2001 From: khyperia <953151+khyperia@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:57:02 +0100 Subject: [PATCH 2/3] config: add fallback output mode --- jay-config/src/_private/client.rs | 9 ++++- jay-config/src/_private/ipc.rs | 9 ++++- jay-config/src/input.rs | 20 ++++++++++ src/config/handler.rs | 28 ++++++++++---- src/ifs/wl_seat.rs | 20 +++++++++- src/ifs/wl_seat/event_handling.rs | 4 +- src/ifs/zwlr_layer_shell_v1.rs | 2 +- src/state.rs | 6 +-- toml-config/src/config.rs | 3 +- toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/config.rs | 16 ++++++++ .../config/parsers/fallback_output_mode.rs | 38 +++++++++++++++++++ toml-config/src/lib.rs | 3 ++ toml-spec/spec/spec.generated.json | 12 ++++++ toml-spec/spec/spec.generated.md | 36 ++++++++++++++++++ toml-spec/spec/spec.yaml | 27 +++++++++++++ 16 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 toml-config/src/config/parsers/fallback_output_mode.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index a604cf02..a9196cf8 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -15,8 +15,9 @@ use { client::{Client, ClientCapabilities, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ - FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, - acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, + FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, + SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, + clickmethod::ClickMethod, }, keyboard::{ Group, Keymap, @@ -1364,6 +1365,10 @@ impl ConfigClient { self.send(&ClientMessage::SetFocusFollowsMouseMode { seat, mode }) } + pub fn set_fallback_output_mode(&self, seat: Seat, mode: FallbackOutputMode) { + self.send(&ClientMessage::SetFallbackOutputMode { seat, mode }) + } + pub fn set_window_management_enabled(&self, seat: Seat, enabled: bool) { self.send(&ClientMessage::SetWindowManagementEnabled { seat, enabled }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 842ff141..91d53955 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -4,8 +4,9 @@ use { Axis, Direction, PciId, Workspace, client::{Client, ClientCapabilities, ClientMatcher}, input::{ - FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, - acceleration::AccelProfile, capability::Capability, clickmethod::ClickMethod, + FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, + SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, + clickmethod::ClickMethod, }, keyboard::{Group, Keymap, mods::Modifiers, syms::KeySym}, logging::LogLevel, @@ -826,6 +827,10 @@ pub enum ClientMessage<'a> { groups: Option>>, options: Option>, }, + SetFallbackOutputMode { + seat: Seat, + mode: FallbackOutputMode, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index ab5de258..2e985766 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -503,6 +503,13 @@ impl Seat { get!().set_focus_follows_mouse_mode(self, mode); } + /// Sets the fallback output mode. + /// + /// The default is `Cursor`. + pub fn set_fallback_output_mode(self, mode: FallbackOutputMode) { + get!().set_fallback_output_mode(self, mode); + } + /// Enables or disable window management mode. /// /// In window management mode, floating windows can be moved by pressing the left @@ -650,6 +657,19 @@ pub enum FocusFollowsMouseMode { False, } +/// Defines which output is used when no particular output is specified. +/// +/// This configures where to place a newly opened window or workspace, what window to focus when a +/// window is closed, which workspace is moved with [`Seat::move_to_output`], and similar actions. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[non_exhaustive] +pub enum FallbackOutputMode { + /// Use the output the cursor is on. + Cursor, + /// Use the output the focus is on (highlighted window). + Focus, +} + /// Returns all seats. pub fn get_seats() -> Vec { get!().seats() diff --git a/src/config/handler.rs b/src/config/handler.rs index 877e3f38..d32f608b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -54,7 +54,7 @@ use { Axis, Direction, Workspace, client::{Client as ConfigClient, ClientCapabilities, ClientMatcher}, input::{ - FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, Timeline, + FallbackOutputMode, 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, @@ -518,6 +518,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_fallback_output_mode( + &self, + seat: Seat, + mode: FallbackOutputMode, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_fallback_output_mode(mode); + Ok(()) + } + fn handle_set_window_management_enabled( &self, seat: Seat, @@ -1056,7 +1066,7 @@ impl ConfigProxyHandler { let name = self.get_workspace(ws)?; let workspace = match self.state.workspaces.get(name.deref()) { Some(ws) => ws, - _ => seat.get_cursor_output().create_workspace(name.deref()), + _ => seat.get_fallback_output().create_workspace(name.deref()), }; seat.set_workspace(&workspace); Ok(()) @@ -1112,11 +1122,12 @@ impl ConfigProxyHandler { Some(ws) => ws, _ => return Ok(()), }, - WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_cursor_output().workspace.get() - { - Some(ws) => ws, - _ => return Ok(()), - }, + WorkspaceSource::Seat(s) => { + match self.get_seat(s)?.get_fallback_output().workspace.get() { + Some(ws) => ws, + _ => return Ok(()), + } + } }; self.state.move_ws_to_output(&ws, &output); Ok(()) @@ -3355,6 +3366,9 @@ impl ConfigProxyHandler { } => self .handle_keymap_from_names(rules, model, groups, options) .wrn("keymap_from_names")?, + ClientMessage::SetFallbackOutputMode { seat, mode } => self + .handle_set_fallback_output_mode(seat, mode) + .wrn("set_fallback_output_mode")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 866754d9..5a96ea29 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -106,7 +106,10 @@ use { wire_ei::EiSeatId, }, ahash::AHashMap, - jay_config::keyboard::syms::{KeySym, SYM_Escape}, + jay_config::{ + input::FallbackOutputMode, + keyboard::syms::{KeySym, SYM_Escape}, + }, kbvm::Keycode, smallvec::SmallVec, std::{ @@ -226,6 +229,7 @@ pub struct WlSeatGlobal { input_method_grab: CloneCell>>, forward: Cell, focus_follows_mouse: Cell, + fallback_output_mode: Cell, swipe_bindings: PerClientBindings, pinch_bindings: PerClientBindings, hold_bindings: PerClientBindings, @@ -325,6 +329,7 @@ impl WlSeatGlobal { input_method_grab: Default::default(), forward: Cell::new(false), focus_follows_mouse: Cell::new(true), + fallback_output_mode: Cell::new(FallbackOutputMode::Cursor), swipe_bindings: Default::default(), pinch_bindings: Default::default(), hold_bindings: Default::default(), @@ -469,6 +474,15 @@ impl WlSeatGlobal { self.keyboard_node.get().node_output() } + pub fn get_fallback_output(&self) -> Rc { + if self.fallback_output_mode.get() == FallbackOutputMode::Focus + && let Some(output) = self.get_keyboard_output() + { + return output; + } + self.get_cursor_output() + } + pub fn set_workspace(&self, ws: &Rc) { let tl = match self.keyboard_node.get().node_toplevel() { Some(tl) => tl, @@ -1393,6 +1407,10 @@ impl WlSeatGlobal { self.focus_follows_mouse.set(focus_follows_mouse); } + pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) { + self.fallback_output_mode.set(fallback_output_mode); + } + pub fn set_window_management_enabled(self: &Rc, enabled: bool) { self.pointer_owner .set_window_management_enabled(self, enabled); diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index fd0aee29..9764c496 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -213,12 +213,12 @@ impl NodeSeatState { fn release_kb_focus2(&self, focus_last: bool) { self.release_kb_grab(); while let Some((_, seat)) = self.kb_foci.pop() { + let output = seat.get_fallback_output(); seat.kb_owner .set_kb_node(&seat, seat.state.root.clone(), seat.state.next_serial(None)); // log::info!("keyboard_node = root"); if focus_last { - seat.get_cursor_output() - .node_do_focus(&seat, Direction::Unspecified); + output.node_do_focus(&seat, Direction::Unspecified); } } } diff --git a/src/ifs/zwlr_layer_shell_v1.rs b/src/ifs/zwlr_layer_shell_v1.rs index 0c9f3fa7..f1e59041 100644 --- a/src/ifs/zwlr_layer_shell_v1.rs +++ b/src/ifs/zwlr_layer_shell_v1.rs @@ -60,7 +60,7 @@ impl ZwlrLayerShellV1RequestHandler for ZwlrLayerShellV1 { self.client.lookup(req.output)?.global.clone() } else { for seat in self.client.state.seat_queue.rev_iter() { - let output = seat.get_cursor_output(); + let output = seat.get_fallback_output(); if !output.is_dummy { break 'get_output output.global.opt.clone(); } diff --git a/src/state.rs b/src/state.rs index cd3e0058..85cdba2b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -780,7 +780,7 @@ impl State { pub fn ensure_map_workspace(&self, seat: Option<&Rc>) -> Rc { seat.cloned() .or_else(|| self.seat_queue.last().map(|s| s.deref().clone())) - .map(|s| s.get_cursor_output()) + .map(|s| s.get_fallback_output()) .or_else(|| self.root.outputs.lock().values().next().cloned()) .or_else(|| self.dummy_output.get()) .unwrap() @@ -916,7 +916,7 @@ impl State { let ws = match self.workspaces.get(name) { Some(ws) => ws, _ => { - let output = output.unwrap_or_else(|| seat.get_cursor_output()); + let output = output.unwrap_or_else(|| seat.get_fallback_output()); if output.is_dummy { log::warn!("Not showing workspace because seat is on dummy output"); return; @@ -929,7 +929,7 @@ impl State { pub fn float_map_ws(&self) -> Rc { if let Some(seat) = self.seat_queue.last() { - let output = seat.get_cursor_output(); + let output = seat.get_fallback_output(); if !output.is_dummy { return output.ensure_workspace(); } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 182ebaf4..9dc6daa0 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -27,7 +27,7 @@ use { Axis, Direction, Workspace, client::ClientCapabilities, input::{ - LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile, + FallbackOutputMode, LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile, clickmethod::ClickMethod, }, keyboard::{Keymap, ModifiedKeySym, mods::Modifiers, syms::KeySym}, @@ -537,6 +537,7 @@ pub struct Config { pub input_modes: AHashMap, pub workspace_display_order: Option, pub simple_im: Option, + pub fallback_output_mode: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index c786c71d..1a4319c3 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -21,6 +21,7 @@ mod drm_device; mod drm_device_match; mod env; pub mod exec; +mod fallback_output_mode; pub mod float; pub mod focus_history; mod format; diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 92e2c067..3ab5f655 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -15,6 +15,7 @@ use { drm_device::DrmDevicesParser, drm_device_match::DrmDeviceMatchParser, env::EnvParser, + fallback_output_mode::FallbackOutputModeParser, float::FloatParser, focus_history::FocusHistoryParser, gfx_api::GfxApiParser, @@ -147,6 +148,7 @@ impl Parser for ConfigParser<'_> { auto_reload, simple_im_val, show_titles, + fallback_output_mode_val, ), ) = ext.extract(( ( @@ -204,6 +206,7 @@ impl Parser for ConfigParser<'_> { recover(opt(bol("auto-reload"))), opt(val("simple-im")), recover(opt(bol("show-titles"))), + opt(val("fallback-output-mode")), ), ))?; let mut keymap = None; @@ -524,6 +527,18 @@ impl Parser for ConfigParser<'_> { } } } + let mut fallback_output_mode = None; + if let Some(value) = fallback_output_mode_val { + match value.parse(&mut FallbackOutputModeParser) { + Ok(v) => fallback_output_mode = Some(v), + Err(e) => { + log::warn!( + "Could not parse the fallback output mode: {}", + self.0.error(e) + ); + } + } + } Ok(Config { keymap, repeat_rate, @@ -570,6 +585,7 @@ impl Parser for ConfigParser<'_> { input_modes, workspace_display_order, simple_im, + fallback_output_mode, }) } } diff --git a/toml-config/src/config/parsers/fallback_output_mode.rs b/toml-config/src/config/parsers/fallback_output_mode.rs new file mode 100644 index 00000000..3918a5db --- /dev/null +++ b/toml-config/src/config/parsers/fallback_output_mode.rs @@ -0,0 +1,38 @@ +use { + crate::{ + config::parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + toml::toml_span::{Span, SpannedExt}, + }, + jay_config::input::FallbackOutputMode, + thiserror::Error, +}; + +pub struct FallbackOutputModeParser; + +#[derive(Debug, Error)] +pub enum FallbackOutputModeParserError { + #[error(transparent)] + DataType(#[from] UnexpectedDataType), + #[error("Unknown mode {0}")] + Unknown(String), +} + +impl Parser for FallbackOutputModeParser { + type Value = FallbackOutputMode; + type Error = FallbackOutputModeParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + use FallbackOutputMode::*; + let api = match string.to_ascii_lowercase().as_str() { + "cursor" => Cursor, + "focus" => Focus, + _ => { + return Err( + FallbackOutputModeParserError::Unknown(string.to_string()).spanned(span) + ); + } + }; + Ok(api) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 5138c8c8..77420126 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -1627,6 +1627,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 60af4936..a2df7f6f 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1068,6 +1068,10 @@ "simple-im": { "description": "Configures the simple, XCompose based input method.\n\nBy default, the input method is enabled. \n\nEven if the input method is enabled, it will only be used if there is no\nrunning external IM.\n\n- Example:\n\n ```toml\n [simple-im]\n enabled = false\n ```\n", "$ref": "#/$defs/SimpleIm" + }, + "fallback-output-mode": { + "description": "Sets the fallback output mode.\n\nThe default is `cursor`.\n\n- Example:\n\n ```toml\n fallback-output-mode = \"focus\"\n ```\n", + "$ref": "#/$defs/FallbackOutputMode" } }, "required": [] @@ -1284,6 +1288,14 @@ } ] }, + "FallbackOutputMode": { + "type": "string", + "description": "Defines which output is used when no particular output is specified.\n\nThis configures where to place a newly opened window or workspace, what window to focus when a\nwindow is closed, which workspace is moved with move-to-output, and similar actions.\n", + "enum": [ + "cursor", + "focus" + ] + }, "Float": { "description": "Describes settings of floating windows.\n\n- Example:\n\n ```toml\n [float]\n show-pin-icon = true\n ```\n", "type": "object", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 48c01716..436b5e43 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -2218,6 +2218,20 @@ The table has the following fields: The value of this field should be a [SimpleIm](#types-SimpleIm). +- `fallback-output-mode` (optional): + + Sets the fallback output mode. + + The default is `cursor`. + + - Example: + + ```toml + fallback-output-mode = "focus" + ``` + + The value of this field should be a [FallbackOutputMode](#types-FallbackOutputMode). + ### `Connector` @@ -2694,6 +2708,28 @@ The table has the following fields: The value of this field should be a boolean. + +### `FallbackOutputMode` + +Defines which output is used when no particular output is specified. + +This configures where to place a newly opened window or workspace, what window to focus when a +window is closed, which workspace is moved with move-to-output, and similar actions. + +Values of this type should be strings. + +The string should have one of the following values: + +- `cursor`: + + Use the output the cursor is on. + +- `focus`: + + Use the output the focus is on (highlighted window). + + + ### `Float` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 20dbacb3..8490eb43 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2986,6 +2986,19 @@ Config: [simple-im] enabled = false ``` + fallback-output-mode: + ref: FallbackOutputMode + required: false + description: | + Sets the fallback output mode. + + The default is `cursor`. + + - Example: + + ```toml + fallback-output-mode = "focus" + ``` Idle: @@ -4351,3 +4364,17 @@ BarPosition: description: The bar is at the top of the output. - value: bottom description: The bar is at the bottom of the output. + + +FallbackOutputMode: + kind: string + description: | + Defines which output is used when no particular output is specified. + + This configures where to place a newly opened window or workspace, what window to focus when a + window is closed, which workspace is moved with move-to-output, and similar actions. + values: + - value: cursor + description: Use the output the cursor is on. + - value: focus + description: Use the output the focus is on (highlighted window). From 5bb19f3ca7e75136b4c2a4e647df4b686ced3ac2 Mon Sep 17 00:00:00 2001 From: khyperia <953151+khyperia@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:53:31 +0100 Subject: [PATCH 3/3] tree: allow focusing workspace nodes --- src/compositor.rs | 1 + src/ifs/wl_seat.rs | 28 +++++++++++- .../tests/t0007_subsurface/screenshot_1.qoi | Bin 8141 -> 8141 bytes .../tests/t0007_subsurface/screenshot_2.qoi | Bin 7832 -> 7832 bytes src/it/tests/t0029_double_click_float.rs | 1 + .../t0029_double_click_float/screenshot_1.qoi | Bin 10118 -> 7834 bytes .../t0029_double_click_float/screenshot_2.qoi | Bin 7834 -> 10118 bytes .../t0037_toplevel_drag/screenshot_1.qoi | Bin 9072 -> 9069 bytes src/renderer.rs | 10 ++++- src/tasks/connector.rs | 1 + src/tree/output.rs | 41 ++++++++++++------ src/tree/workspace.rs | 16 ++++--- 12 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 8cb3ae8b..0e333e56 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -728,6 +728,7 @@ fn create_dummy_output(state: &Rc) { bar_rect: Default::default(), bar_rect_rel: Default::default(), bar_rect_with_separator: Default::default(), + bar_separator_rect: Default::default(), bar_separator_rect_rel: Default::default(), non_exclusive_rect: Default::default(), render_data: Default::default(), diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 5a96ea29..27ced5da 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -740,7 +740,16 @@ impl WlSeatGlobal { pub fn move_focus(self: &Rc, direction: Direction) { let tl = match self.keyboard_node.get().node_toplevel() { Some(tl) => tl, - _ => return, + _ => { + if let Some(ws) = self.keyboard_node.get().node_into_workspace() + && let Some(target) = self + .state + .find_output_in_direction(&ws.output.get(), direction) + { + target.take_keyboard_navigation_focus(self, direction); + } + return; + } }; if direction == Direction::Down && tl.node_is_container() { tl.node_do_focus(self, direction); @@ -762,6 +771,13 @@ impl WlSeatGlobal { pub fn move_focused(self: &Rc, direction: Direction) { let kb_node = self.keyboard_node.get(); let Some(tl) = kb_node.node_toplevel() else { + if let Some(ws) = self.keyboard_node.get().node_into_workspace() + && let Some(target) = self + .state + .find_output_in_direction(&ws.output.get(), direction) + { + self.state.move_ws_to_output(&ws, &target); + } return; }; let data = tl.tl_data(); @@ -995,7 +1011,15 @@ impl WlSeatGlobal { NodeLayer::Layer0 => handle_layer_shell(&output.layers[0]), NodeLayer::Layer1 => handle_layer_shell(&output.layers[1]), NodeLayer::Output => None, - NodeLayer::Workspace => None, + NodeLayer::Workspace => { + if let Some(ws) = &ws + && ws.container_visible() + { + self.focus_node(ws.clone()); + return; + } + None + } NodeLayer::Tiled => ws .as_ref() .and_then(|w| w.container.get()) diff --git a/src/it/tests/t0007_subsurface/screenshot_1.qoi b/src/it/tests/t0007_subsurface/screenshot_1.qoi index eca5ddef82cd60dbbbd400fd5b1b711540368114..230c0408f7411f5ac77c386a080c686f2646c3c0 100644 GIT binary patch delta 39 mcmX?Wf7X733ZsztUnKDDpOTW&WJ9Lg%vUycY!+eKDGLDNi58sz delta 39 mcmX?Wf7X733Zw8pV`JmLNZ?z4$7DmM+e}xCHj6Orlm!6T2o}iz diff --git a/src/it/tests/t0007_subsurface/screenshot_2.qoi b/src/it/tests/t0007_subsurface/screenshot_2.qoi index ea65888db926c35557a53373bb773265383fa617..722271f61949f939e280e154ee9a514d0b14314c 100644 GIT binary patch delta 39 mcmbPXJHvK^3ZsztUnKDDpOTW&WJ9Lg%vUycY!+c^kpTeF&K3Cp delta 39 mcmbPXJHvK^3Zw8pV`JmLNZ?z4$7DmM+e}xCHj6N|$N&JxO%^2p diff --git a/src/it/tests/t0029_double_click_float.rs b/src/it/tests/t0029_double_click_float.rs index dbce7c08..a2c71b92 100644 --- a/src/it/tests/t0029_double_click_float.rs +++ b/src/it/tests/t0029_double_click_float.rs @@ -18,6 +18,7 @@ async fn test(run: Rc) -> TestResult { win1.set_color(255, 0, 0, 255); win1.map2().await?; run.cfg.set_floating(ds.seat.id(), true)?; + client.sync().await; for i in ["1", "2"] { let (x, y) = win1.tl.server.node_absolute_position().position(); diff --git a/src/it/tests/t0029_double_click_float/screenshot_1.qoi b/src/it/tests/t0029_double_click_float/screenshot_1.qoi index f49edd4d3627d804802ad5f24047b69f68aebc11..dd974ccffda932661558262ad0c65bd5f05f7bed 100644 GIT binary patch delta 68 zcmZqkpJltjl#x+$CVM$^G)Iv7m{ zpb!{M2cS?GEe}T1!Du=Fg}`V!0ENP6c`%v|M$-W(1V+;VC=^D^gOQjHKr#RKFY!|$ esOllSQF1f{MnhmU1V%$(Gz5lU2z+CJ07d}izNMo8 diff --git a/src/it/tests/t0029_double_click_float/screenshot_2.qoi b/src/it/tests/t0029_double_click_float/screenshot_2.qoi index dd974ccffda932661558262ad0c65bd5f05f7bed..f49edd4d3627d804802ad5f24047b69f68aebc11 100644 GIT binary patch literal 10118 zcmXTS&rD-rU{+vYV2WU7_@@zCe*PZ=1H)e=@KpS~DH8YZh~xh=Ha12MfN%d81SF9K zM%kkwFd71*Aut*OBPj%){j+!Xf_V6m3532R4WWtW9}$CVM$^G)Iv7m{ zpb!{M2cS?GEe}T1!Du=Fg}`V!0ENP6c`%v|M$-W(1V+;VC=^D^gOQjHKr#RKFY!|$ esOllSQF1f{MnhmU1V%$(Gz5lU2z+CJ07d}izNMo8 delta 68 zcmZqkpJltjl#x+*gI1f2i? delta 17 YcmaFs_Q7p~3M0!uV`JmZs*D~=07A+J9RL6T diff --git a/src/renderer.rs b/src/renderer.rs index 6f7efd46..2c94020f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -85,6 +85,7 @@ impl Renderer<'_> { } else { render_layer!(output.layers[0]); render_layer!(output.layers[1]); + let ws = output.workspace.get(); if self.state.show_bar.get() { let non_exclusive_rect_rel = output.non_exclusive_rect_rel.get(); let (mut x, mut y) = non_exclusive_rect_rel.translate_inv(x, y); @@ -109,7 +110,12 @@ impl Renderer<'_> { self.base .fill_boxes2(slice::from_ref(&aw.rect), &c, srgb, x, y); } - let c = theme.colors.separator.get(); + let mut c = theme.colors.separator.get(); + if let Some(ws) = &ws + && ws.seat_state.is_active() + { + c = theme.colors.focused_title_background.get(); + } self.base .fill_boxes2(slice::from_ref(&rd.bar_separator), &c, srgb, x, y); let c = theme.colors.unfocused_title_background.get(); @@ -172,7 +178,7 @@ impl Renderer<'_> { } } } - if let Some(ws) = output.workspace.get() { + if let Some(ws) = &ws { let ws_rect = output.workspace_rect_rel.get(); let (x, y) = ws_rect.translate_inv(x, y); self.render_workspace(&ws, x, y); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 4b24cba5..85753771 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -235,6 +235,7 @@ impl ConnectorHandler { bar_rect: Default::default(), bar_rect_rel: Default::default(), bar_rect_with_separator: Default::default(), + bar_separator_rect: Default::default(), bar_separator_rect_rel: Default::default(), render_data: Default::default(), state: self.state.clone(), diff --git a/src/tree/output.rs b/src/tree/output.rs index ba78da11..29027687 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -96,6 +96,7 @@ pub struct OutputNode { pub bar_rect: Cell, pub bar_rect_rel: Cell, pub bar_rect_with_separator: Cell, + pub bar_separator_rect: Cell, pub bar_separator_rect_rel: Cell, pub render_data: RefCell, pub state: Rc, @@ -774,11 +775,11 @@ impl OutputNode { let mut bar_rect = Rect::default(); let mut bar_rect_rel = Rect::default(); let mut bar_rect_with_separator = Rect::default(); + let mut bar_separator_rect = Rect::default(); let mut bar_separator_rect_rel = Rect::default(); let mut workspace_rect = non_exclusive_rect; let mut workspace_rect_rel = non_exclusive_rect_rel; if self.state.show_bar.get() { - let bar_separator_rect; match self.state.theme.bar_position.get() { BarPosition::Bottom => { workspace_rect = Rect::new_sized_saturating(x1, y1, width, height - bh - bsw); @@ -805,6 +806,7 @@ impl OutputNode { self.bar_rect.set(bar_rect); self.bar_rect_rel.set(bar_rect_rel); self.bar_rect_with_separator.set(bar_rect_with_separator); + self.bar_separator_rect.set(bar_separator_rect); self.bar_separator_rect_rel.set(bar_separator_rect_rel); self.workspace_rect.set(workspace_rect); self.workspace_rect_rel.set(workspace_rect_rel); @@ -1147,20 +1149,13 @@ impl OutputNode { set_layer_visible!(self.layers[3], visible); } - fn button(self: Rc, seat: &Rc, id: PointerType) { + fn bar_button(self: &Rc, seat: &Rc, x: i32, y: i32) -> bool { if !self.state.show_bar.get() { - return; - } - let (x, y) = match self.pointer_positions.get(&id) { - Some(p) => p, - _ => return, - }; - if let PointerType::Seat(s) = id { - self.pointer_down.set(s, (x, y)); + return false; } let bar_rect_rel = self.bar_rect_rel.get(); if bar_rect_rel.not_contains(x, y) { - return; + return false; } let (x, _) = bar_rect_rel.translate(x, y); let ws = 'ws: { @@ -1170,9 +1165,25 @@ impl OutputNode { break 'ws title.ws.clone(); } } - return; + return true; }; - self.state.show_workspace2(Some(seat), &self, &ws); + self.state.show_workspace2(Some(seat), self, &ws); + true + } + + fn button(self: Rc, seat: &Rc, id: PointerType) { + let (x, y) = match self.pointer_positions.get(&id) { + Some(p) => p, + _ => return, + }; + if let PointerType::Seat(s) = id { + self.pointer_down.set(s, (x, y)); + } + if self.bar_button(seat, x, y) { + return; + } + let ws = self.ensure_workspace(); + seat.focus_node(ws); } pub fn update_presentation_type(&self) { @@ -1489,6 +1500,10 @@ impl OutputNode { if c.node_visible() { c.node_do_focus(seat, direction); } + } else { + if ws.node_visible() { + seat.focus_node(ws); + } } } } diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index df0e43cf..eb308c08 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -366,17 +366,23 @@ impl Node for WorkspaceNode { seat.focus_node(last); } else if let Some(container) = self.container.get() { container.node_do_focus(seat, direction); - } else if let Some(float) = self + } else if let Some(child) = self .stacked .rev_iter() - .find_map(|node| (*node).clone().node_into_float()) + .filter_map(|node| (*node).clone().node_into_float()) + .find_map(|float| float.child.get()) { - if let Some(child) = float.child.get() { - child.node_do_focus(seat, direction); - } + child.node_do_focus(seat, direction); + } else { + seat.focus_node(self); } } + fn node_active_changed(&self, _active: bool) { + let output = self.output.get(); + self.state.damage(output.bar_separator_rect.get()); + } + fn node_find_tree_at( &self, x: i32,