From 91c948b219a132520ef839dd77e8156766d3313c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:43:54 +0200 Subject: [PATCH] config: add keyboard-focus window criteria --- jay-config/src/_private.rs | 2 ++ jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 3 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 14 ++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + .../tlm/tlm_matchers/tlmm_seat_focus.rs | 28 +++++++++++++++++++ src/ifs/wl_seat/kb_owner.rs | 18 ++++++++++-- src/tree/toplevel.rs | 4 ++- toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 4 ++- toml-config/src/rules.rs | 8 ++++++ toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 6 ++++ toml-spec/spec/spec.yaml | 4 +++ 15 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index bd7bc94a..59f34023 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -6,6 +6,7 @@ pub(crate) mod string_error; use { crate::{ client::ClientMatcher, + input::Seat, video::Mode, window::{WindowMatcher, WindowType}, }, @@ -113,6 +114,7 @@ pub enum WindowCriterionIpc { Floating, Visible, Urgent, + SeatFocus(Seat), } #[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 0881d6bb..cacd9e91 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1660,6 +1660,7 @@ impl ConfigClient { WindowCriterion::Floating => WindowCriterionIpc::Floating, WindowCriterion::Visible => WindowCriterionIpc::Visible, WindowCriterion::Urgent => WindowCriterionIpc::Urgent, + WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), }; 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 0d3e1127..ce7a6918 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -4,6 +4,7 @@ use { crate::{ Axis, Direction, Workspace, client::{Client, ClientCriterion}, + input::Seat, }, serde::{Deserialize, Serialize}, std::ops::Deref, @@ -250,6 +251,8 @@ pub enum WindowCriterion<'a> { Visible, /// Matches if the window has the urgency flag set. Urgent, + /// Matches if the window has the keyboard focus of the seat. + Focus(Seat), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index bdd5fb66..0f675b0d 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2000,6 +2000,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Floating => mgr.floating(), WindowCriterionIpc::Visible => mgr.visible(), WindowCriterionIpc::Urgent => mgr.urgent(), + WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index af9797db..7a2652e6 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -15,11 +15,13 @@ use { tlmm_client::TlmMatchClient, tlmm_floating::TlmMatchFloating, tlmm_kind::TlmMatchKind, + tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, }, + ifs::wl_seat::WlSeatGlobal, state::State, tree::{NodeId, ToplevelData, ToplevelNode}, utils::{ @@ -44,6 +46,7 @@ bitflags! { TL_CHANGED_FLOATING = 1 << 4, TL_CHANGED_VISIBLE = 1 << 5, TL_CHANGED_URGENT = 1 << 6, + TL_CHANGED_SEAT_FOCI = 1 << 7, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -67,6 +70,7 @@ pub struct RootMatchers { clients: CopyHashMap>, title: TlmRootMatcherMap, app_id: TlmRootMatcherMap, + seat_foci: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -129,6 +133,10 @@ impl TlMatcherManager { } } + pub fn has_seat_foci(&self) -> bool { + self.matchers.seat_foci.is_not_empty() + } + pub fn has_no_interest(&self, data: &ToplevelData, change: TlMatcherChange) -> bool { !self.has_interest(data, change) } @@ -175,6 +183,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -244,6 +253,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -276,6 +286,10 @@ impl TlMatcherManager { pub fn urgent(&self) -> Rc { self.urgent[true].clone() } + + pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { + self.root(TlmMatchSeatFocus::new(seat.id())) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index fcff929e..1d5bd6ec 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_kind; +pub mod tlmm_seat_focus; pub mod tlmm_string; pub mod tlmm_urgent; pub mod tlmm_visible; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs b/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs new file mode 100644 index 00000000..34e75562 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs @@ -0,0 +1,28 @@ +use crate::{ + criteria::{ + crit_graph::CritRootCriterion, + tlm::{RootMatchers, TlmRootMatcherMap}, + }, + ifs::wl_seat::SeatId, + tree::ToplevelData, +}; + +pub struct TlmMatchSeatFocus { + id: SeatId, +} + +impl TlmMatchSeatFocus { + pub fn new(id: SeatId) -> TlmMatchSeatFocus { + Self { id } + } +} + +impl CritRootCriterion for TlmMatchSeatFocus { + fn matches(&self, data: &ToplevelData) -> bool { + data.seat_foci.contains(&self.id) + } + + fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap> { + Some(&roots.seat_foci) + } +} diff --git a/src/ifs/wl_seat/kb_owner.rs b/src/ifs/wl_seat/kb_owner.rs index caf7048b..c6950062 100644 --- a/src/ifs/wl_seat/kb_owner.rs +++ b/src/ifs/wl_seat/kb_owner.rs @@ -1,7 +1,7 @@ use { crate::{ - ifs::wl_seat::WlSeatGlobal, tree::Node, utils::clonecell::CloneCell, - xwayland::XWaylandEvent, + criteria::tlm::TL_CHANGED_SEAT_FOCI, ifs::wl_seat::WlSeatGlobal, tree::Node, + utils::clonecell::CloneCell, xwayland::XWaylandEvent, }, std::rc::Rc, }; @@ -61,6 +61,18 @@ impl KbOwner for DefaultKbOwner { } fn set_kb_node(&self, seat: &Rc, node: Rc, serial: u64) { + macro_rules! notify_matcher { + ($node:expr, $data:ident, $block:expr) => { + if let Some(tl) = $node.clone().node_toplevel() { + let $data = tl.tl_data(); + $block; + if seat.state.tl_matcher_manager.has_seat_foci() { + $data.property_changed(TL_CHANGED_SEAT_FOCI); + } + } + }; + } + let old = seat.keyboard_node.get(); if old.node_id() == node.node_id() { return; @@ -70,6 +82,7 @@ impl KbOwner for DefaultKbOwner { seat.state.xwayland.queue.push(XWaylandEvent::ActivateRoot); } old.node_on_unfocus(seat); + notify_matcher!(old, data, data.seat_foci.remove(&seat.id)); if old.node_seat_state().unfocus(seat) { old.node_active_changed(false); } @@ -79,6 +92,7 @@ impl KbOwner for DefaultKbOwner { } // log::info!("focus {}", node.node_id()); node.clone().node_on_focus(seat); + notify_matcher!(node, data, data.seat_foci.set(seat.id, ())); seat.keyboard_node_serial.set(serial); seat.keyboard_node.set(node.clone()); seat.tablet_on_keyboard_node_change(); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 36f77f14..afcf0e02 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -14,7 +14,7 @@ use { ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, jay_screencast::JayScreencast, jay_toplevel::JayToplevel, - wl_seat::{NodeSeatState, collect_kb_foci, collect_kb_foci2}, + wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, wl_surface::WlSurface, }, rect::Rect, @@ -324,6 +324,7 @@ pub struct ToplevelData { pub slf: Weak, pub destroyed: CopyHashMap>>, pub changed_properties: Cell, + pub seat_foci: CopyHashMap, } impl ToplevelData { @@ -370,6 +371,7 @@ impl ToplevelData { slf: slf.clone(), destroyed: Default::default(), changed_properties: Default::default(), + seat_foci: Default::default(), } } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 5745898b..dedc5049 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -262,6 +262,7 @@ pub struct WindowMatch { pub floating: Option, pub visible: Option, pub urgent: Option, + pub focused: 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 69ca4eea..5f66362a 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), + (app_id, app_id_regex, floating, visible, urgent, focused), ) = ext.extract(( ( opt(str("name")), @@ -75,6 +75,7 @@ impl Parser for WindowMatchParser<'_> { opt(bol("floating")), opt(bol("visible")), opt(bol("urgent")), + opt(bol("focused")), ), ))?; let mut not = None; @@ -123,6 +124,7 @@ impl Parser for WindowMatchParser<'_> { floating: floating.despan(), visible: visible.despan(), urgent: urgent.despan(), + focused: focused.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 65062aaa..a37a0ab5 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -261,6 +261,14 @@ impl Rule for WindowRule { bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); + if let Some(value) = match_.focused { + let crit = WindowCriterion::Focus(state.persistent.seat); + let matcher = match value { + false => m(WindowCriterion::Not(&crit)), + true => m(crit), + }; + all.push(matcher); + } Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1a37fd8a..953900aa 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1807,6 +1807,10 @@ "urgent": { "type": "boolean", "description": "Matches if the window has/hasn't the urgency flag set." + }, + "focused": { + "type": "boolean", + "description": "Matches if the window has/hasn't the keyboard focus." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 89a1103b..058d2eda 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4052,6 +4052,12 @@ The table has the following fields: The value of this field should be a boolean. +- `focused` (optional): + + Matches if the window has/hasn't the keyboard focus. + + 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 bf817b84..3f88b423 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3495,6 +3495,10 @@ WindowMatch: kind: boolean required: false description: Matches if the window has/hasn't the urgency flag set. + focused: + kind: boolean + required: false + description: Matches if the window has/hasn't the keyboard focus. WindowMatchExactly: