1
0
Fork 0
forked from wry/wry

config: add keyboard-focus window criteria

This commit is contained in:
Julian Orth 2025-05-01 18:43:54 +02:00
parent eb172e9d8c
commit 91c948b219
15 changed files with 95 additions and 4 deletions

View file

@ -6,6 +6,7 @@ pub(crate) mod string_error;
use { use {
crate::{ crate::{
client::ClientMatcher, client::ClientMatcher,
input::Seat,
video::Mode, video::Mode,
window::{WindowMatcher, WindowType}, window::{WindowMatcher, WindowType},
}, },
@ -113,6 +114,7 @@ pub enum WindowCriterionIpc {
Floating, Floating,
Visible, Visible,
Urgent, Urgent,
SeatFocus(Seat),
} }
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]

View file

@ -1660,6 +1660,7 @@ impl ConfigClient {
WindowCriterion::Floating => WindowCriterionIpc::Floating, WindowCriterion::Floating => WindowCriterionIpc::Floating,
WindowCriterion::Visible => WindowCriterionIpc::Visible, WindowCriterion::Visible => WindowCriterionIpc::Visible,
WindowCriterion::Urgent => WindowCriterionIpc::Urgent, WindowCriterion::Urgent => WindowCriterionIpc::Urgent,
WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat),
}; };
let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion });
get_response!( get_response!(

View file

@ -4,6 +4,7 @@ use {
crate::{ crate::{
Axis, Direction, Workspace, Axis, Direction, Workspace,
client::{Client, ClientCriterion}, client::{Client, ClientCriterion},
input::Seat,
}, },
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
std::ops::Deref, std::ops::Deref,
@ -250,6 +251,8 @@ pub enum WindowCriterion<'a> {
Visible, Visible,
/// Matches if the window has the urgency flag set. /// Matches if the window has the urgency flag set.
Urgent, Urgent,
/// Matches if the window has the keyboard focus of the seat.
Focus(Seat),
} }
impl WindowCriterion<'_> { impl WindowCriterion<'_> {

View file

@ -2000,6 +2000,7 @@ impl ConfigProxyHandler {
WindowCriterionIpc::Floating => mgr.floating(), WindowCriterionIpc::Floating => mgr.floating(),
WindowCriterionIpc::Visible => mgr.visible(), WindowCriterionIpc::Visible => mgr.visible(),
WindowCriterionIpc::Urgent => mgr.urgent(), WindowCriterionIpc::Urgent => mgr.urgent(),
WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?),
}; };
let cached = Rc::new(CachedCriterion { let cached = Rc::new(CachedCriterion {
crit: criterion.clone(), crit: criterion.clone(),

View file

@ -15,11 +15,13 @@ use {
tlmm_client::TlmMatchClient, tlmm_client::TlmMatchClient,
tlmm_floating::TlmMatchFloating, tlmm_floating::TlmMatchFloating,
tlmm_kind::TlmMatchKind, tlmm_kind::TlmMatchKind,
tlmm_seat_focus::TlmMatchSeatFocus,
tlmm_string::{TlmMatchAppId, TlmMatchTitle}, tlmm_string::{TlmMatchAppId, TlmMatchTitle},
tlmm_urgent::TlmMatchUrgent, tlmm_urgent::TlmMatchUrgent,
tlmm_visible::TlmMatchVisible, tlmm_visible::TlmMatchVisible,
}, },
}, },
ifs::wl_seat::WlSeatGlobal,
state::State, state::State,
tree::{NodeId, ToplevelData, ToplevelNode}, tree::{NodeId, ToplevelData, ToplevelNode},
utils::{ utils::{
@ -44,6 +46,7 @@ bitflags! {
TL_CHANGED_FLOATING = 1 << 4, TL_CHANGED_FLOATING = 1 << 4,
TL_CHANGED_VISIBLE = 1 << 5, TL_CHANGED_VISIBLE = 1 << 5,
TL_CHANGED_URGENT = 1 << 6, TL_CHANGED_URGENT = 1 << 6,
TL_CHANGED_SEAT_FOCI = 1 << 7,
} }
type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>; type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>;
@ -67,6 +70,7 @@ pub struct RootMatchers {
clients: CopyHashMap<CritMatcherId, Weak<TlmMatchClient>>, clients: CopyHashMap<CritMatcherId, Weak<TlmMatchClient>>,
title: TlmRootMatcherMap<TlmMatchTitle>, title: TlmRootMatcherMap<TlmMatchTitle>,
app_id: TlmRootMatcherMap<TlmMatchAppId>, app_id: TlmRootMatcherMap<TlmMatchAppId>,
seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>,
} }
pub async fn handle_tl_changes(state: Rc<State>) { pub async fn handle_tl_changes(state: Rc<State>) {
@ -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 { pub fn has_no_interest(&self, data: &ToplevelData, change: TlMatcherChange) -> bool {
!self.has_interest(data, change) !self.has_interest(data, change)
} }
@ -175,6 +183,7 @@ impl TlMatcherManager {
} }
conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_TITLE, title);
conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_APP_ID, app_id);
conditional!(TL_CHANGED_SEAT_FOCI, seat_foci);
fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_FLOATING, floating);
fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_VISIBLE, visible);
fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_URGENT, urgent);
@ -244,6 +253,7 @@ impl TlMatcherManager {
} }
conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_TITLE, title);
conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_APP_ID, app_id);
conditional!(TL_CHANGED_SEAT_FOCI, seat_foci);
fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_FLOATING, floating);
fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_VISIBLE, visible);
fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_URGENT, urgent);
@ -276,6 +286,10 @@ impl TlMatcherManager {
pub fn urgent(&self) -> Rc<TlmUpstreamNode> { pub fn urgent(&self) -> Rc<TlmUpstreamNode> {
self.urgent[true].clone() self.urgent[true].clone()
} }
pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchSeatFocus::new(seat.id()))
}
} }
impl CritTarget for ToplevelData { impl CritTarget for ToplevelData {

View file

@ -20,6 +20,7 @@ macro_rules! fixed_root_criterion {
pub mod tlmm_client; pub mod tlmm_client;
pub mod tlmm_floating; pub mod tlmm_floating;
pub mod tlmm_kind; pub mod tlmm_kind;
pub mod tlmm_seat_focus;
pub mod tlmm_string; pub mod tlmm_string;
pub mod tlmm_urgent; pub mod tlmm_urgent;
pub mod tlmm_visible; pub mod tlmm_visible;

View file

@ -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<ToplevelData> for TlmMatchSeatFocus {
fn matches(&self, data: &ToplevelData) -> bool {
data.seat_foci.contains(&self.id)
}
fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap<Self>> {
Some(&roots.seat_foci)
}
}

View file

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
ifs::wl_seat::WlSeatGlobal, tree::Node, utils::clonecell::CloneCell, criteria::tlm::TL_CHANGED_SEAT_FOCI, ifs::wl_seat::WlSeatGlobal, tree::Node,
xwayland::XWaylandEvent, utils::clonecell::CloneCell, xwayland::XWaylandEvent,
}, },
std::rc::Rc, std::rc::Rc,
}; };
@ -61,6 +61,18 @@ impl KbOwner for DefaultKbOwner {
} }
fn set_kb_node(&self, seat: &Rc<WlSeatGlobal>, node: Rc<dyn Node>, serial: u64) { fn set_kb_node(&self, seat: &Rc<WlSeatGlobal>, node: Rc<dyn Node>, 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(); let old = seat.keyboard_node.get();
if old.node_id() == node.node_id() { if old.node_id() == node.node_id() {
return; return;
@ -70,6 +82,7 @@ impl KbOwner for DefaultKbOwner {
seat.state.xwayland.queue.push(XWaylandEvent::ActivateRoot); seat.state.xwayland.queue.push(XWaylandEvent::ActivateRoot);
} }
old.node_on_unfocus(seat); old.node_on_unfocus(seat);
notify_matcher!(old, data, data.seat_foci.remove(&seat.id));
if old.node_seat_state().unfocus(seat) { if old.node_seat_state().unfocus(seat) {
old.node_active_changed(false); old.node_active_changed(false);
} }
@ -79,6 +92,7 @@ impl KbOwner for DefaultKbOwner {
} }
// log::info!("focus {}", node.node_id()); // log::info!("focus {}", node.node_id());
node.clone().node_on_focus(seat); 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_serial.set(serial);
seat.keyboard_node.set(node.clone()); seat.keyboard_node.set(node.clone());
seat.tablet_on_keyboard_node_change(); seat.tablet_on_keyboard_node_change();

View file

@ -14,7 +14,7 @@ use {
ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1,
jay_screencast::JayScreencast, jay_screencast::JayScreencast,
jay_toplevel::JayToplevel, 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, wl_surface::WlSurface,
}, },
rect::Rect, rect::Rect,
@ -324,6 +324,7 @@ pub struct ToplevelData {
pub slf: Weak<dyn ToplevelNode>, pub slf: Weak<dyn ToplevelNode>,
pub destroyed: CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<ToplevelData>>>, pub destroyed: CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<ToplevelData>>>,
pub changed_properties: Cell<TlMatcherChange>, pub changed_properties: Cell<TlMatcherChange>,
pub seat_foci: CopyHashMap<SeatId, ()>,
} }
impl ToplevelData { impl ToplevelData {
@ -370,6 +371,7 @@ impl ToplevelData {
slf: slf.clone(), slf: slf.clone(),
destroyed: Default::default(), destroyed: Default::default(),
changed_properties: Default::default(), changed_properties: Default::default(),
seat_foci: Default::default(),
} }
} }

View file

@ -262,6 +262,7 @@ pub struct WindowMatch {
pub floating: Option<bool>, pub floating: Option<bool>,
pub visible: Option<bool>, pub visible: Option<bool>,
pub urgent: Option<bool>, pub urgent: Option<bool>,
pub focused: Option<bool>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> {
title, title,
title_regex, title_regex,
), ),
(app_id, app_id_regex, floating, visible, urgent), (app_id, app_id_regex, floating, visible, urgent, focused),
) = ext.extract(( ) = ext.extract((
( (
opt(str("name")), opt(str("name")),
@ -75,6 +75,7 @@ impl Parser for WindowMatchParser<'_> {
opt(bol("floating")), opt(bol("floating")),
opt(bol("visible")), opt(bol("visible")),
opt(bol("urgent")), opt(bol("urgent")),
opt(bol("focused")),
), ),
))?; ))?;
let mut not = None; let mut not = None;
@ -123,6 +124,7 @@ impl Parser for WindowMatchParser<'_> {
floating: floating.despan(), floating: floating.despan(),
visible: visible.despan(), visible: visible.despan(),
urgent: urgent.despan(), urgent: urgent.despan(),
focused: focused.despan(),
types, types,
client, client,
}) })

View file

@ -261,6 +261,14 @@ impl Rule for WindowRule {
bool!(Floating, floating); bool!(Floating, floating);
bool!(Visible, visible); bool!(Visible, visible);
bool!(Urgent, urgent); 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(()) Some(())
} }

View file

@ -1807,6 +1807,10 @@
"urgent": { "urgent": {
"type": "boolean", "type": "boolean",
"description": "Matches if the window has/hasn't the urgency flag set." "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": [] "required": []

View file

@ -4052,6 +4052,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.
- `focused` (optional):
Matches if the window has/hasn't the keyboard focus.
The value of this field should be a boolean.
<a name="types-WindowMatchExactly"></a> <a name="types-WindowMatchExactly"></a>
### `WindowMatchExactly` ### `WindowMatchExactly`

View file

@ -3495,6 +3495,10 @@ WindowMatch:
kind: boolean kind: boolean
required: false required: false
description: Matches if the window has/hasn't the urgency flag set. 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: WindowMatchExactly: