From 5f1268cada395c4e9b7b429d9d83940a122c3fdf Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:52:55 +0200 Subject: [PATCH] config: add just-mapped window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 5 ++++ src/async_engine.rs | 4 +-- src/compositor.rs | 9 +++++- src/config/handler.rs | 1 + src/criteria/tlm.rs | 30 +++++++++++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + .../tlm/tlm_matchers/tlmm_just_mapped.rs | 11 +++++++ src/tree/toplevel.rs | 9 ++++++ toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 4 ++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 9 ++++++ toml-spec/spec/spec.yaml | 8 +++++ 16 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index e7303d20..62ff5b09 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -116,6 +116,7 @@ pub enum WindowCriterionIpc { Urgent, SeatFocus(Seat), Fullscreen, + JustMapped, } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 09660de4..6bdd2f1e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1662,6 +1662,7 @@ impl ConfigClient { WindowCriterion::Urgent => WindowCriterionIpc::Urgent, WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, + WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, }; let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); get_response!( diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index cb105c0b..a5ef9036 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -255,6 +255,11 @@ pub enum WindowCriterion<'a> { Focus(Seat), /// Matches if the window is fullscreen. Fullscreen, + /// Matches if the window has/hasn't just been mapped. + /// + /// This is true for one iteration of the compositor's main loop immediately after the + /// window has been mapped. + JustMapped, } impl WindowCriterion<'_> { diff --git a/src/async_engine.rs b/src/async_engine.rs index e936dd46..80e38c52 100644 --- a/src/async_engine.rs +++ b/src/async_engine.rs @@ -105,7 +105,6 @@ impl AsyncEngine { break; } self.now.take(); - self.iteration.fetch_add(1); let mut phase = 0; while phase < NUM_PHASES { self.queues[phase].swap(&mut *stash); @@ -121,6 +120,7 @@ impl AsyncEngine { } } } + self.iteration.fetch_add(1); self.yields.swap(&mut *yield_stash); while let Some(waker) = yield_stash.pop_front() { waker.wake(); @@ -153,7 +153,7 @@ impl AsyncEngine { self.yields.push(waker); } - fn iteration(&self) -> u64 { + pub fn iteration(&self) -> u64 { self.iteration.get() } diff --git a/src/compositor.rs b/src/compositor.rs index 2c3352ee..82967771 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -18,7 +18,9 @@ use { criteria::{ CritMatcherIds, clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events}, - tlm::{TlMatcherManager, handle_tl_changes, handle_tl_leaf_events}, + tlm::{ + TlMatcherManager, handle_tl_changes, handle_tl_just_mapped, handle_tl_leaf_events, + }, }, damage::{DamageVisualizer, visualize_damage}, dbus::Dbus, @@ -484,6 +486,11 @@ fn start_global_event_handlers( "tl matcher leaf events", handle_tl_leaf_events(state.clone()), ), + eng.spawn2( + "tl matcher just mapped", + Phase::Layout, + handle_tl_just_mapped(state.clone()), + ), ] } diff --git a/src/config/handler.rs b/src/config/handler.rs index 62c957a1..2241dc08 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2002,6 +2002,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Urgent => mgr.urgent(), WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), WindowCriterionIpc::Fullscreen => mgr.fullscreen(), + WindowCriterionIpc::JustMapped => mgr.just_mapped(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 658444b1..75b6fa08 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -15,6 +15,7 @@ use { tlmm_client::TlmMatchClient, tlmm_floating::TlmMatchFloating, tlmm_fullscreen::TlmMatchFullscreen, + tlmm_just_mapped::TlmMatchJustMapped, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, @@ -49,6 +50,7 @@ bitflags! { TL_CHANGED_URGENT = 1 << 6, TL_CHANGED_SEAT_FOCI = 1 << 7, TL_CHANGED_FULLSCREEN = 1 << 8, + TL_CHANGED_JUST_MAPPED = 1 << 9, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -57,11 +59,13 @@ pub struct TlMatcherManager { ids: Rc, changes: AsyncQueue>, leaf_events: Rc>>, + handle_just_mapped: AsyncQueue>, constant: TlmFixedRootMatcher>, floating: TlmFixedRootMatcher, visible: TlmFixedRootMatcher, urgent: TlmFixedRootMatcher, fullscreen: TlmFixedRootMatcher, + just_mapped: TlmFixedRootMatcher, matchers: Rc, } @@ -94,6 +98,16 @@ pub async fn handle_tl_leaf_events(state: Rc) { } } +pub async fn handle_tl_just_mapped(state: Rc) { + let mgr = &state.tl_matcher_manager; + loop { + let tl = mgr.handle_just_mapped.pop().await; + let data = tl.tl_data(); + data.just_mapped_scheduled.set(false); + data.property_changed(TL_CHANGED_JUST_MAPPED); + } +} + pub type TlmUpstreamNode = dyn CritUpstreamNode; pub type TlmLeafMatcher = CritLeafMatcher; @@ -117,16 +131,19 @@ impl TlMatcherManager { visible: bool!(TlmMatchVisible), urgent: bool!(TlmMatchUrgent), fullscreen: bool!(TlmMatchFullscreen), + just_mapped: bool!(TlmMatchJustMapped), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), matchers, + handle_just_mapped: Default::default(), } } pub fn clear(&self) { self.changes.clear(); self.leaf_events.clear(); + self.handle_just_mapped.clear(); } pub fn rematch_all(&self, state: &Rc) { @@ -192,6 +209,7 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); + fixed_conditional!(TL_CHANGED_JUST_MAPPED, just_mapped); false } @@ -263,6 +281,14 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); + fixed_conditional!(TL_CHANGED_JUST_MAPPED, just_mapped); + if changed.contains(TL_CHANGED_JUST_MAPPED) + && data.just_mapped() + && (self.just_mapped[false].has_downstream() || self.just_mapped[true].has_downstream()) + && !data.just_mapped_scheduled.replace(true) + { + self.handle_just_mapped.push(node); + } } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -297,6 +323,10 @@ impl TlMatcherManager { self.urgent[true].clone() } + pub fn just_mapped(&self) -> Rc { + self.just_mapped[true].clone() + } + pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { self.root(TlmMatchSeatFocus::new(seat.id())) } diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 550fc739..6d900774 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -20,6 +20,7 @@ macro_rules! fixed_root_criterion { pub mod tlmm_client; pub mod tlmm_floating; pub mod tlmm_fullscreen; +pub mod tlmm_just_mapped; pub mod tlmm_kind; pub mod tlmm_seat_focus; pub mod tlmm_string; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs b/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs new file mode 100644 index 00000000..e21b5675 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchJustMapped(pub bool); + +fixed_root_criterion!(TlmMatchJustMapped, just_mapped); + +impl CritFixedRootCriterion for TlmMatchJustMapped { + fn matches(&self, data: &ToplevelData) -> bool { + data.just_mapped() + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index bab92811..d328b2ee 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -103,6 +103,7 @@ impl ToplevelNode for T { let data = self.tl_data(); let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); if parent_was_none { + data.mapped_during_iteration.set(data.state.eng.iteration()); data.property_changed(TL_CHANGED_NEW); } let was_floating = data.is_floating.get(); @@ -308,6 +309,7 @@ pub struct ToplevelData { pub workspace: CloneCell>>, pub title: RefCell, pub parent: CloneCell>>, + pub mapped_during_iteration: Cell, pub pos: Cell, pub desired_extents: Cell, pub seat_state: NodeSeatState, @@ -325,6 +327,7 @@ pub struct ToplevelData { pub slf: Weak, pub destroyed: CopyHashMap>>, pub changed_properties: Cell, + pub just_mapped_scheduled: Cell, pub seat_foci: CopyHashMap, } @@ -357,6 +360,7 @@ impl ToplevelData { workspace: Default::default(), title: RefCell::new(title), parent: Default::default(), + mapped_during_iteration: Cell::new(0), pos: Default::default(), desired_extents: Default::default(), seat_state: Default::default(), @@ -372,6 +376,7 @@ impl ToplevelData { slf: slf.clone(), destroyed: Default::default(), changed_properties: Default::default(), + just_mapped_scheduled: Cell::new(false), seat_foci: Default::default(), } } @@ -696,6 +701,10 @@ impl ToplevelData { }; (0, 0) } + + pub fn just_mapped(&self) -> bool { + self.mapped_during_iteration.get() == self.state.eng.iteration() + } } impl Drop for ToplevelData { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 34e54f75..dc35cbce 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -264,6 +264,7 @@ pub struct WindowMatch { pub urgent: Option, pub focused: Option, pub fullscreen: Option, + pub just_mapped: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/window_match.rs b/toml-config/src/config/parsers/window_match.rs index f2a51571..864e897c 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen), + (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen, just_mapped), ) = ext.extract(( ( opt(str("name")), @@ -77,6 +77,7 @@ impl Parser for WindowMatchParser<'_> { opt(bol("urgent")), opt(bol("focused")), opt(bol("fullscreen")), + opt(bol("just-mapped")), ), ))?; let mut not = None; @@ -127,6 +128,7 @@ impl Parser for WindowMatchParser<'_> { urgent: urgent.despan(), focused: focused.despan(), fullscreen: fullscreen.despan(), + just_mapped: just_mapped.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 31f40a94..42c75b24 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -262,6 +262,7 @@ impl Rule for WindowRule { bool!(Visible, visible); bool!(Urgent, urgent); bool!(Fullscreen, fullscreen); + bool!(JustMapped, just_mapped); if let Some(value) = match_.focused { let crit = WindowCriterion::Focus(state.persistent.seat); let matcher = match value { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index fad46f11..7413ab70 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1815,6 +1815,10 @@ "fullscreen": { "type": "boolean", "description": "Matches if the window is/isn't fullscreen." + }, + "just-mapped": { + "type": "boolean", + "description": "Matches if the window has/hasn't just been mapped.\n\nThis is true for one iteration of the compositor's main loop immediately after the\nwindow has been mapped.\n" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 66a42572..b8c5192d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4064,6 +4064,15 @@ The table has the following fields: The value of this field should be a boolean. +- `just-mapped` (optional): + + Matches if the window has/hasn't just been mapped. + + This is true for one iteration of the compositor's main loop immediately after the + window has been mapped. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 98c8f37d..79b7294d 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3503,6 +3503,14 @@ WindowMatch: kind: boolean required: false description: Matches if the window is/isn't fullscreen. + just-mapped: + kind: boolean + required: false + description: | + Matches if the window has/hasn't just been mapped. + + This is true for one iteration of the compositor's main loop immediately after the + window has been mapped. WindowMatchExactly: