From 5e3465d8612f1c83c7f96a1d614a22f7ca239209 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 7 May 2025 15:59:42 +0200 Subject: [PATCH] config: add initial-tile-state window rule --- jay-config/src/_private/client.rs | 13 ++++++- jay-config/src/_private/ipc.rs | 6 +++- jay-config/src/window.rs | 30 ++++++++++++++++ src/config.rs | 7 +++- src/config/handler.rs | 36 ++++++++++++++++++- src/ifs/wl_surface/x_surface/xwindow.rs | 11 +++++- .../wl_surface/xdg_surface/xdg_toplevel.rs | 32 ++++++++++++++--- src/state.rs | 26 +++++++++----- toml-config/src/config.rs | 3 +- toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/tile_state.rs | 35 ++++++++++++++++++ toml-config/src/config/parsers/window_rule.rs | 30 ++++++++++++---- toml-config/src/rules.rs | 3 ++ toml-spec/spec/spec.generated.json | 12 +++++++ toml-spec/spec/spec.generated.md | 25 +++++++++++++ toml-spec/spec/spec.yaml | 14 ++++++++ 16 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 toml-config/src/config/parsers/tile_state.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2991b43d..e03ec7fb 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -32,7 +32,7 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, - window::{MatchedWindow, Window, WindowCriterion, WindowMatcher, WindowType}, + window::{MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType}, xwayland::XScalingMode, }, bincode::Options, @@ -1708,6 +1708,17 @@ impl ConfigClient { }); } + pub fn set_window_matcher_initial_tile_state( + &self, + matcher: WindowMatcher, + tile_state: TileState, + ) { + self.send(&ClientMessage::SetWindowMatcherInitialTileState { + matcher, + tile_state, + }); + } + pub fn set_window_matcher_latch_handler( &self, matcher: WindowMatcher, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 50d01c68..6aa10326 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -15,7 +15,7 @@ use { ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode, connector_type::ConnectorType, }, - window::{Window, WindowMatcher, WindowType}, + window::{TileState, Window, WindowMatcher, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -702,6 +702,10 @@ pub enum ClientMessage<'a> { matcher: WindowMatcher, auto_focus: bool, }, + SetWindowMatcherInitialTileState { + matcher: WindowMatcher, + tile_state: TileState, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 222e1ac2..df949770 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -41,6 +41,16 @@ bitflags! { } } +/// The tile state of a window. +#[non_exhaustive] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum TileState { + /// The window is tiled. + Tiled, + /// The window is floating. + Floating, +} + /// A window created by a client. /// /// This is the same as `XDG_TOPLEVEL | X_WINDOW`. @@ -306,6 +316,17 @@ impl WindowCriterion<'_> { pub fn set_auto_focus(self, auto_focus: bool) { self.to_matcher().set_auto_focus(auto_focus); } + + /// Sets whether newly mapped windows that match this matcher are mapped tiling or + /// floating. + /// + /// If multiple such window matchers match a window, the used tile state is + /// unspecified. + /// + /// This leaks the matcher. + pub fn set_initial_tile_state(self, tile_state: TileState) { + self.to_matcher().set_initial_tile_state(tile_state); + } } impl WindowMatcher { @@ -330,6 +351,15 @@ impl WindowMatcher { pub fn set_auto_focus(self, auto_focus: bool) { get!().set_window_matcher_auto_focus(self, auto_focus); } + + /// Sets whether newly mapped windows that match this matcher are mapped tiling or + /// floating. + /// + /// If multiple such window matchers match a window, the used tile state is + /// unspecified. + pub fn set_initial_tile_state(self, tile_state: TileState) { + get!().set_window_matcher_initial_tile_state(self, tile_state); + } } impl MatchedWindow { diff --git a/src/config.rs b/src/config.rs index 7655568b..ebc993cd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,7 @@ use { input::{InputDevice, Seat, SwitchEvent}, keyboard::{mods::Modifiers, syms::KeySym}, video::{Connector, DrmDevice}, - window, + window::{self, TileState}, }, libloading::Library, std::{cell::Cell, io, mem, ptr, rc::Rc}, @@ -169,6 +169,10 @@ impl ConfigProxy { }; handler.auto_focus(data) } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + self.handler.get()?.initial_tile_state(data) + } } impl Drop for ConfigProxy { @@ -233,6 +237,7 @@ impl ConfigProxy { window_matcher_leafs: Default::default(), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), window_matcher_no_auto_focus: Default::default(), + window_matcher_initial_tile_state: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index aa4996a1..9f693fe6 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -66,7 +66,7 @@ use { TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, - window::{Window, WindowMatcher}, + window::{TileState, Window, WindowMatcher}, xwayland::XScalingMode, }, libloading::Library, @@ -126,6 +126,13 @@ pub(super) struct ConfigProxyHandler { pub window_matcher_std_kinds: Rc, pub window_matcher_no_auto_focus: CopyHashMap>>, + pub window_matcher_initial_tile_state: CopyHashMap< + WindowMatcher, + ( + Rc>, + TileState, + ), + >, } pub struct Pollable { @@ -2030,6 +2037,7 @@ impl ConfigProxyHandler { self.window_matchers.remove(&matcher); self.window_matcher_leafs.remove(&matcher); self.window_matcher_no_auto_focus.remove(&matcher); + self.window_matcher_initial_tile_state.remove(&matcher); } fn handle_enable_window_matcher_events( @@ -2073,6 +2081,17 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_matcher_initial_tile_state( + &self, + matcher: WindowMatcher, + tile_state: TileState, + ) -> Result<(), CphError> { + let m = self.get_window_matcher(matcher)?; + self.window_matcher_initial_tile_state + .set(matcher, (m, tile_state)); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2884,6 +2903,12 @@ impl ConfigProxyHandler { } => self .handle_set_window_matcher_auto_focus(matcher, auto_focus) .wrn("set_window_matcher_auto_focus")?, + ClientMessage::SetWindowMatcherInitialTileState { + matcher, + tile_state, + } => self + .handle_set_window_matcher_initial_tile_state(matcher, tile_state) + .wrn("set_window_matcher_initial_tile_state")?, } Ok(()) } @@ -2896,6 +2921,15 @@ impl ConfigProxyHandler { } true } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + for (matcher, state) in self.window_matcher_initial_tile_state.lock().values() { + if matcher.node.pull(data) { + return Some(*state); + } + } + None + } } #[derive(Debug, Error)] diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 2bceaf68..46b950fb 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -21,6 +21,7 @@ use { xwayland::XWaylandEvent, }, bstr::BString, + jay_config::window::TileState, std::{ cell::{Cell, RefCell}, ops::{Deref, Not}, @@ -266,6 +267,14 @@ impl Xwindow { pub fn map_status_changed(self: &Rc) { let map_change = self.map_change(); let override_redirect = self.data.info.override_redirect.get(); + let map_floating = match self + .toplevel_data + .state + .initial_tile_state(&self.toplevel_data) + { + None => self.data.info.wants_floating.get(), + Some(m) => m == TileState::Floating, + }; match map_change { Change::None => return, Change::Unmap => { @@ -282,7 +291,7 @@ impl Xwindow { Some(self.data.state.root.stacked.add_last(self.clone())); self.data.state.tree_changed(); } - Change::Map if self.data.info.wants_floating.get() => { + Change::Map if map_floating => { let ws = self.data.state.float_map_ws(); let ext = self.data.info.pending_extents.get(); self.data diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 9bdc3d3c..9a824027 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -34,6 +34,7 @@ use { wire::{XdgToplevelId, xdg_toplevel::*}, }, ahash::{AHashMap, AHashSet}, + jay_config::window::TileState, num_derive::FromPrimitive, std::{ cell::{Cell, RefCell}, @@ -381,6 +382,31 @@ impl XdgToplevelRequestHandler for XdgToplevel { } impl XdgToplevel { + fn map( + self: &Rc, + parent: Option<&XdgToplevel>, + pos: Option<(&Rc, i32, i32)>, + ) { + if let Some(state) = self.state.initial_tile_state(&self.toplevel_data) { + match state { + TileState::Floating => { + let mut ws = None; + if let Some(parent) = parent { + ws = parent.xdg.workspace.get(); + } + let ws = ws.unwrap_or_else(|| self.state.ensure_map_workspace(None)); + self.map_floating(&ws, pos.map(|p| (p.1, p.2))); + } + _ => self.map_tiled(), + } + return; + } + match parent { + None => self.map_tiled(), + Some(p) => self.map_child(p, pos), + } + } + fn map_floating(self: &Rc, workspace: &Rc, abs_pos: Option<(i32, i32)>) { let (width, height) = self.toplevel_data.float_size(workspace); self.state @@ -474,11 +500,7 @@ impl XdgToplevel { } self.state.tree_changed(); } else { - if let Some(parent) = self.parent.get() { - self.map_child(&parent, pos); - } else { - self.map_tiled(); - } + self.map(self.parent.get().as_deref(), pos); self.extents_changed(); if let Some(workspace) = self.xdg.workspace.get() { let output = workspace.output.get(); diff --git a/src/state.rs b/src/state.rs index e93e67fe..b03cdddc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -82,8 +82,8 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node, - NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode, - ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, + NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelData, + ToplevelNode, ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, }, utils::{ activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings, @@ -112,6 +112,7 @@ use { jay_config::{ PciId, video::{GfxApi, Transform}, + window::TileState, }, std::{ cell::{Cell, RefCell}, @@ -662,6 +663,16 @@ 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()) + .or_else(|| self.root.outputs.lock().values().next().cloned()) + .or_else(|| self.dummy_output.get()) + .unwrap() + .ensure_workspace() + } + pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); self.do_map_tiled(seat.as_deref(), node.clone()); @@ -669,12 +680,7 @@ impl State { } fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { - let output = seat - .map(|s| s.get_output()) - .or_else(|| self.root.outputs.lock().values().next().cloned()) - .or_else(|| self.dummy_output.get()) - .unwrap(); - let ws = output.ensure_workspace(); + let ws = self.ensure_map_workspace(seat); self.map_tiled_on(node, &ws); } @@ -1384,6 +1390,10 @@ impl State { }; ctx.supports_color_management() } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + self.config.get()?.initial_tile_state(data) + } } #[derive(Debug, Error)] diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 52f18fde..06bf1873 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -28,7 +28,7 @@ use { status::MessageFormat, theme::Color, video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, - window::WindowType, + window::{TileState, WindowType}, xwayland::XScalingMode, }, std::{ @@ -249,6 +249,7 @@ pub struct WindowRule { pub action: Option, pub latch: Option, pub auto_focus: Option, + pub initial_tile_state: Option, } #[derive(Default, Debug, Clone)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index d49fabfc..ed064b44 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -37,6 +37,7 @@ pub mod shortcuts; mod status; mod tearing; mod theme; +mod tile_state; mod ui_drag; mod vrr; mod window_match; diff --git a/toml-config/src/config/parsers/tile_state.rs b/toml-config/src/config/parsers/tile_state.rs new file mode 100644 index 00000000..de9d30eb --- /dev/null +++ b/toml-config/src/config/parsers/tile_state.rs @@ -0,0 +1,35 @@ +use { + crate::{ + config::parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + toml::toml_span::{Span, SpannedExt}, + }, + jay_config::window::TileState, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum TileStateParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown tile state `{}`", .0)] + UnknownTileState(String), +} + +pub struct TileStateParser; + +impl Parser for TileStateParser { + type Value = TileState; + type Error = TileStateParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let ty = match string { + "tiled" => TileState::Tiled, + "floating" => TileState::Floating, + _ => { + return Err(TileStateParserError::UnknownTileState(string.to_owned()).spanned(span)); + } + }; + Ok(ty) + } +} diff --git a/toml-config/src/config/parsers/window_rule.rs b/toml-config/src/config/parsers/window_rule.rs index 21405e60..5311641c 100644 --- a/toml-config/src/config/parsers/window_rule.rs +++ b/toml-config/src/config/parsers/window_rule.rs @@ -7,6 +7,7 @@ use { parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::{ActionParser, ActionParserError}, + tile_state::TileStateParser, window_match::{WindowMatchParser, WindowMatchParserError}, }, spanned::SpannedErrorExt, @@ -47,13 +48,15 @@ impl Parser for WindowRuleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (name, match_val, action_val, latch_val, auto_focus) = ext.extract(( - opt(str("name")), - opt(val("match")), - opt(val("action")), - opt(val("latch")), - recover(opt(bol("auto-focus"))), - ))?; + let (name, match_val, action_val, latch_val, auto_focus, initial_tile_state_val) = ext + .extract(( + opt(str("name")), + opt(val("match")), + opt(val("action")), + opt(val("latch")), + recover(opt(bol("auto-focus"))), + opt(val("initial-tile-state")), + ))?; let mut action = None; if let Some(value) = action_val { action = Some( @@ -70,6 +73,18 @@ impl Parser for WindowRuleParser<'_> { .map_spanned_err(WindowRuleParserError::Latch)?, ); } + let mut initial_tile_state = None; + if let Some(value) = initial_tile_state_val { + match value.parse(&mut TileStateParser) { + Ok(v) => initial_tile_state = Some(v), + Err(e) => { + log::warn!( + "Could not parse the initial tile state: {}", + self.0.error(e) + ); + } + } + } let match_ = match match_val { None => WindowMatch::default(), Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?, @@ -80,6 +95,7 @@ impl Parser for WindowRuleParser<'_> { action, latch, auto_focus: auto_focus.despan(), + initial_tile_state, }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 93ee47e2..ffef34c3 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -336,6 +336,9 @@ impl Rule for WindowRule { if let Some(auto_focus) = self.auto_focus { matcher.set_auto_focus(auto_focus); } + if let Some(tile_state) = self.initial_tile_state { + matcher.set_initial_tile_state(tile_state); + } } fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 7a91ce7b..d2c1b491 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1664,6 +1664,14 @@ }, "required": [] }, + "TileState": { + "type": "string", + "description": "Whether a window is tiled or floating.", + "enum": [ + "tiled", + "floating" + ] + }, "TransferFunction": { "type": "string", "description": "The transfer function of an output.\n", @@ -1908,6 +1916,10 @@ "auto-focus": { "type": "boolean", "description": "Whether newly mapped windows that match this rule get the keyboard focus.\n\nIf a window matches any rule for which this is false, the window will not be\nautomatically focused.\n" + }, + "initial-tile-state": { + "description": "Specifies if the window is initially mapped tiled or floating.", + "$ref": "#/$defs/TileState" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index c86bc249..96e76355 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3710,6 +3710,25 @@ The table has the following fields: The value of this field should be a string. + +### `TileState` + +Whether a window is tiled or floating. + +Values of this type should be strings. + +The string should have one of the following values: + +- `tiled`: + + The window is tiled. + +- `floating`: + + The window is floating. + + + ### `TransferFunction` @@ -4212,6 +4231,12 @@ The table has the following fields: The value of this field should be a boolean. +- `initial-tile-state` (optional): + + Specifies if the window is initially mapped tiled or floating. + + The value of this field should be a [TileState](#types-TileState). + ### `WindowTypeMask` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 73914d9a..5e2055d6 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3376,6 +3376,10 @@ WindowRule: If a window matches any rule for which this is false, the window will not be automatically focused. + initial-tile-state: + ref: TileState + required: false + description: Specifies if the window is initially mapped tiled or floating. WindowMatch: @@ -3602,3 +3606,13 @@ WindowTypeMask: description: An array of masks that are OR'd. items: ref: WindowTypeMask + + +TileState: + description: Whether a window is tiled or floating. + kind: string + values: + - value: tiled + description: The window is tiled. + - value: floating + description: The window is floating.