1
0
Fork 0
forked from wry/wry

config: add initial-tile-state window rule

This commit is contained in:
Julian Orth 2025-05-07 15:59:42 +02:00
parent b1ca98b488
commit 5e3465d861
16 changed files with 258 additions and 26 deletions

View file

@ -32,7 +32,7 @@ use {
Transform, VrrMode, Transform, VrrMode,
connector_type::{CON_UNKNOWN, ConnectorType}, connector_type::{CON_UNKNOWN, ConnectorType},
}, },
window::{MatchedWindow, Window, WindowCriterion, WindowMatcher, WindowType}, window::{MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType},
xwayland::XScalingMode, xwayland::XScalingMode,
}, },
bincode::Options, 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( pub fn set_window_matcher_latch_handler(
&self, &self,
matcher: WindowMatcher, matcher: WindowMatcher,

View file

@ -15,7 +15,7 @@ use {
ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction,
Transform, VrrMode, connector_type::ConnectorType, Transform, VrrMode, connector_type::ConnectorType,
}, },
window::{Window, WindowMatcher, WindowType}, window::{TileState, Window, WindowMatcher, WindowType},
xwayland::XScalingMode, xwayland::XScalingMode,
}, },
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
@ -702,6 +702,10 @@ pub enum ClientMessage<'a> {
matcher: WindowMatcher, matcher: WindowMatcher,
auto_focus: bool, auto_focus: bool,
}, },
SetWindowMatcherInitialTileState {
matcher: WindowMatcher,
tile_state: TileState,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -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. /// A window created by a client.
/// ///
/// This is the same as `XDG_TOPLEVEL | X_WINDOW`. /// This is the same as `XDG_TOPLEVEL | X_WINDOW`.
@ -306,6 +316,17 @@ impl WindowCriterion<'_> {
pub fn set_auto_focus(self, auto_focus: bool) { pub fn set_auto_focus(self, auto_focus: bool) {
self.to_matcher().set_auto_focus(auto_focus); 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 { impl WindowMatcher {
@ -330,6 +351,15 @@ impl WindowMatcher {
pub fn set_auto_focus(self, auto_focus: bool) { pub fn set_auto_focus(self, auto_focus: bool) {
get!().set_window_matcher_auto_focus(self, auto_focus); 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 { impl MatchedWindow {

View file

@ -23,7 +23,7 @@ use {
input::{InputDevice, Seat, SwitchEvent}, input::{InputDevice, Seat, SwitchEvent},
keyboard::{mods::Modifiers, syms::KeySym}, keyboard::{mods::Modifiers, syms::KeySym},
video::{Connector, DrmDevice}, video::{Connector, DrmDevice},
window, window::{self, TileState},
}, },
libloading::Library, libloading::Library,
std::{cell::Cell, io, mem, ptr, rc::Rc}, std::{cell::Cell, io, mem, ptr, rc::Rc},
@ -169,6 +169,10 @@ impl ConfigProxy {
}; };
handler.auto_focus(data) handler.auto_focus(data)
} }
pub fn initial_tile_state(&self, data: &ToplevelData) -> Option<TileState> {
self.handler.get()?.initial_tile_state(data)
}
} }
impl Drop for ConfigProxy { impl Drop for ConfigProxy {
@ -233,6 +237,7 @@ impl ConfigProxy {
window_matcher_leafs: Default::default(), window_matcher_leafs: Default::default(),
window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW),
window_matcher_no_auto_focus: Default::default(), window_matcher_no_auto_focus: Default::default(),
window_matcher_initial_tile_state: Default::default(),
}); });
let init_msg = bincode_ops() let init_msg = bincode_ops()
.serialize(&InitMessage::V1(V1InitMessage {})) .serialize(&InitMessage::V1(V1InitMessage {}))

View file

@ -66,7 +66,7 @@ use {
TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction,
Transform, VrrMode as ConfigVrrMode, Transform, VrrMode as ConfigVrrMode,
}, },
window::{Window, WindowMatcher}, window::{TileState, Window, WindowMatcher},
xwayland::XScalingMode, xwayland::XScalingMode,
}, },
libloading::Library, libloading::Library,
@ -126,6 +126,13 @@ pub(super) struct ConfigProxyHandler {
pub window_matcher_std_kinds: Rc<TlmUpstreamNode>, pub window_matcher_std_kinds: Rc<TlmUpstreamNode>,
pub window_matcher_no_auto_focus: pub window_matcher_no_auto_focus:
CopyHashMap<WindowMatcher, Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>>, CopyHashMap<WindowMatcher, Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>>,
pub window_matcher_initial_tile_state: CopyHashMap<
WindowMatcher,
(
Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>,
TileState,
),
>,
} }
pub struct Pollable { pub struct Pollable {
@ -2030,6 +2037,7 @@ impl ConfigProxyHandler {
self.window_matchers.remove(&matcher); self.window_matchers.remove(&matcher);
self.window_matcher_leafs.remove(&matcher); self.window_matcher_leafs.remove(&matcher);
self.window_matcher_no_auto_focus.remove(&matcher); self.window_matcher_no_auto_focus.remove(&matcher);
self.window_matcher_initial_tile_state.remove(&matcher);
} }
fn handle_enable_window_matcher_events( fn handle_enable_window_matcher_events(
@ -2073,6 +2081,17 @@ impl ConfigProxyHandler {
Ok(()) 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) { fn spaces_change(&self) {
struct V; struct V;
impl NodeVisitorBase for V { impl NodeVisitorBase for V {
@ -2884,6 +2903,12 @@ impl ConfigProxyHandler {
} => self } => self
.handle_set_window_matcher_auto_focus(matcher, auto_focus) .handle_set_window_matcher_auto_focus(matcher, auto_focus)
.wrn("set_window_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(()) Ok(())
} }
@ -2896,6 +2921,15 @@ impl ConfigProxyHandler {
} }
true true
} }
pub fn initial_tile_state(&self, data: &ToplevelData) -> Option<TileState> {
for (matcher, state) in self.window_matcher_initial_tile_state.lock().values() {
if matcher.node.pull(data) {
return Some(*state);
}
}
None
}
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -21,6 +21,7 @@ use {
xwayland::XWaylandEvent, xwayland::XWaylandEvent,
}, },
bstr::BString, bstr::BString,
jay_config::window::TileState,
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
ops::{Deref, Not}, ops::{Deref, Not},
@ -266,6 +267,14 @@ impl Xwindow {
pub fn map_status_changed(self: &Rc<Self>) { pub fn map_status_changed(self: &Rc<Self>) {
let map_change = self.map_change(); let map_change = self.map_change();
let override_redirect = self.data.info.override_redirect.get(); 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 { match map_change {
Change::None => return, Change::None => return,
Change::Unmap => { Change::Unmap => {
@ -282,7 +291,7 @@ impl Xwindow {
Some(self.data.state.root.stacked.add_last(self.clone())); Some(self.data.state.root.stacked.add_last(self.clone()));
self.data.state.tree_changed(); 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 ws = self.data.state.float_map_ws();
let ext = self.data.info.pending_extents.get(); let ext = self.data.info.pending_extents.get();
self.data self.data

View file

@ -34,6 +34,7 @@ use {
wire::{XdgToplevelId, xdg_toplevel::*}, wire::{XdgToplevelId, xdg_toplevel::*},
}, },
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
jay_config::window::TileState,
num_derive::FromPrimitive, num_derive::FromPrimitive,
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
@ -381,6 +382,31 @@ impl XdgToplevelRequestHandler for XdgToplevel {
} }
impl XdgToplevel { impl XdgToplevel {
fn map(
self: &Rc<Self>,
parent: Option<&XdgToplevel>,
pos: Option<(&Rc<OutputNode>, 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<Self>, workspace: &Rc<WorkspaceNode>, abs_pos: Option<(i32, i32)>) { fn map_floating(self: &Rc<Self>, workspace: &Rc<WorkspaceNode>, abs_pos: Option<(i32, i32)>) {
let (width, height) = self.toplevel_data.float_size(workspace); let (width, height) = self.toplevel_data.float_size(workspace);
self.state self.state
@ -474,11 +500,7 @@ impl XdgToplevel {
} }
self.state.tree_changed(); self.state.tree_changed();
} else { } else {
if let Some(parent) = self.parent.get() { self.map(self.parent.get().as_deref(), pos);
self.map_child(&parent, pos);
} else {
self.map_tiled();
}
self.extents_changed(); self.extents_changed();
if let Some(workspace) = self.xdg.workspace.get() { if let Some(workspace) = self.xdg.workspace.get() {
let output = workspace.output.get(); let output = workspace.output.get();

View file

@ -82,8 +82,8 @@ use {
time::Time, time::Time,
tree::{ tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node, ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node,
NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelData,
ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, ToplevelNode, ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor,
}, },
utils::{ utils::{
activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings, activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings,
@ -112,6 +112,7 @@ use {
jay_config::{ jay_config::{
PciId, PciId,
video::{GfxApi, Transform}, video::{GfxApi, Transform},
window::TileState,
}, },
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
@ -662,6 +663,16 @@ impl State {
} }
} }
pub fn ensure_map_workspace(&self, seat: Option<&Rc<WlSeatGlobal>>) -> Rc<WorkspaceNode> {
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<Self>, node: Rc<dyn ToplevelNode>) { pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
let seat = self.seat_queue.last(); let seat = self.seat_queue.last();
self.do_map_tiled(seat.as_deref(), node.clone()); self.do_map_tiled(seat.as_deref(), node.clone());
@ -669,12 +680,7 @@ impl State {
} }
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) { fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
let output = seat let ws = self.ensure_map_workspace(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();
self.map_tiled_on(node, &ws); self.map_tiled_on(node, &ws);
} }
@ -1384,6 +1390,10 @@ impl State {
}; };
ctx.supports_color_management() ctx.supports_color_management()
} }
pub fn initial_tile_state(&self, data: &ToplevelData) -> Option<TileState> {
self.config.get()?.initial_tile_state(data)
}
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -28,7 +28,7 @@ use {
status::MessageFormat, status::MessageFormat,
theme::Color, theme::Color,
video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode},
window::WindowType, window::{TileState, WindowType},
xwayland::XScalingMode, xwayland::XScalingMode,
}, },
std::{ std::{
@ -249,6 +249,7 @@ pub struct WindowRule {
pub action: Option<Action>, pub action: Option<Action>,
pub latch: Option<Action>, pub latch: Option<Action>,
pub auto_focus: Option<bool>, pub auto_focus: Option<bool>,
pub initial_tile_state: Option<TileState>,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]

View file

@ -37,6 +37,7 @@ pub mod shortcuts;
mod status; mod status;
mod tearing; mod tearing;
mod theme; mod theme;
mod tile_state;
mod ui_drag; mod ui_drag;
mod vrr; mod vrr;
mod window_match; mod window_match;

View file

@ -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<Self> {
let ty = match string {
"tiled" => TileState::Tiled,
"floating" => TileState::Floating,
_ => {
return Err(TileStateParserError::UnknownTileState(string.to_owned()).spanned(span));
}
};
Ok(ty)
}
}

View file

@ -7,6 +7,7 @@ use {
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{ parsers::{
action::{ActionParser, ActionParserError}, action::{ActionParser, ActionParserError},
tile_state::TileStateParser,
window_match::{WindowMatchParser, WindowMatchParserError}, window_match::{WindowMatchParser, WindowMatchParserError},
}, },
spanned::SpannedErrorExt, spanned::SpannedErrorExt,
@ -47,13 +48,15 @@ impl Parser for WindowRuleParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table); let mut ext = Extractor::new(self.0, span, table);
let (name, match_val, action_val, latch_val, auto_focus) = ext.extract(( let (name, match_val, action_val, latch_val, auto_focus, initial_tile_state_val) = ext
opt(str("name")), .extract((
opt(val("match")), opt(str("name")),
opt(val("action")), opt(val("match")),
opt(val("latch")), opt(val("action")),
recover(opt(bol("auto-focus"))), opt(val("latch")),
))?; recover(opt(bol("auto-focus"))),
opt(val("initial-tile-state")),
))?;
let mut action = None; let mut action = None;
if let Some(value) = action_val { if let Some(value) = action_val {
action = Some( action = Some(
@ -70,6 +73,18 @@ impl Parser for WindowRuleParser<'_> {
.map_spanned_err(WindowRuleParserError::Latch)?, .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 { let match_ = match match_val {
None => WindowMatch::default(), None => WindowMatch::default(),
Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?, Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?,
@ -80,6 +95,7 @@ impl Parser for WindowRuleParser<'_> {
action, action,
latch, latch,
auto_focus: auto_focus.despan(), auto_focus: auto_focus.despan(),
initial_tile_state,
}) })
} }
} }

View file

@ -336,6 +336,9 @@ impl Rule for WindowRule {
if let Some(auto_focus) = self.auto_focus { if let Some(auto_focus) = self.auto_focus {
matcher.set_auto_focus(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> { fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> {

View file

@ -1664,6 +1664,14 @@
}, },
"required": [] "required": []
}, },
"TileState": {
"type": "string",
"description": "Whether a window is tiled or floating.",
"enum": [
"tiled",
"floating"
]
},
"TransferFunction": { "TransferFunction": {
"type": "string", "type": "string",
"description": "The transfer function of an output.\n", "description": "The transfer function of an output.\n",
@ -1908,6 +1916,10 @@
"auto-focus": { "auto-focus": {
"type": "boolean", "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" "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": [] "required": []

View file

@ -3710,6 +3710,25 @@ The table has the following fields:
The value of this field should be a string. The value of this field should be a string.
<a name="types-TileState"></a>
### `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.
<a name="types-TransferFunction"></a> <a name="types-TransferFunction"></a>
### `TransferFunction` ### `TransferFunction`
@ -4212,6 +4231,12 @@ The table has the following fields:
The value of this field should be a boolean. 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).
<a name="types-WindowTypeMask"></a> <a name="types-WindowTypeMask"></a>
### `WindowTypeMask` ### `WindowTypeMask`

View file

@ -3376,6 +3376,10 @@ WindowRule:
If a window matches any rule for which this is false, the window will not be If a window matches any rule for which this is false, the window will not be
automatically focused. automatically focused.
initial-tile-state:
ref: TileState
required: false
description: Specifies if the window is initially mapped tiled or floating.
WindowMatch: WindowMatch:
@ -3602,3 +3606,13 @@ WindowTypeMask:
description: An array of masks that are OR'd. description: An array of masks that are OR'd.
items: items:
ref: WindowTypeMask 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.