1
0
Fork 0
forked from wry/wry

control-center: add window pane

This commit is contained in:
Julian Orth 2026-03-05 18:59:43 +01:00
parent aefd1fbbdb
commit ca6fc54246
9 changed files with 611 additions and 11 deletions

View file

@ -9,6 +9,7 @@ use {
cc_input::InputPane,
cc_look_and_feel::LookAndFeelPane,
cc_outputs::OutputsPane,
cc_window::{WindowPane, WindowSearchPane},
cc_xwayland::XwaylandPane,
},
egui_adapter::egui_platform::{
@ -18,8 +19,8 @@ use {
macros::Bitflag,
state::State,
utils::{
asyncevent::AsyncEvent, copyhashmap::CopyHashMap, numcell::NumCell,
static_text::StaticText,
asyncevent::AsyncEvent, copyhashmap::CopyHashMap,
event_listener::LazyEventSourceListener, numcell::NumCell, static_text::StaticText,
},
},
egui::{
@ -50,6 +51,7 @@ mod cc_input;
mod cc_look_and_feel;
mod cc_outputs;
mod cc_sidebar;
mod cc_window;
mod cc_xwayland;
#[derive(Debug, Error)]
@ -141,6 +143,8 @@ enum PaneType {
LookAndFeel(LookAndFeelPane),
Clients(ClientsPane),
Client(ClientPane),
WindowSearch(WindowSearchPane),
Window(WindowPane),
}
struct CcBehavior<'a> {
@ -168,6 +172,8 @@ impl Pane {
PaneType::LookAndFeel(v) => v.title(res),
PaneType::Clients(v) => v.title(res),
PaneType::Client(v) => v.title(res),
PaneType::WindowSearch(v) => v.title(res),
PaneType::Window(v) => v.title(res),
}
}
@ -183,6 +189,8 @@ impl Pane {
PaneType::LookAndFeel(p) => p.show(ui),
PaneType::Clients(p) => p.show(behavior, ui),
PaneType::Client(p) => p.show(behavior, ui),
PaneType::WindowSearch(p) => p.show(behavior, ui),
PaneType::Window(p) => p.show(behavior, ui),
}
}
}
@ -200,6 +208,8 @@ impl PaneType {
PaneType::LookAndFeel(_) => CCI_LOOK_AND_FEEL,
PaneType::Clients(_) => ControlCenterInterest::none(),
PaneType::Client(_) => ControlCenterInterest::none(),
PaneType::WindowSearch(_) => ControlCenterInterest::none(),
PaneType::Window(_) => ControlCenterInterest::none(),
}
}
}
@ -663,3 +673,9 @@ impl Drop for GridRow<'_> {
self.end_row();
}
}
impl LazyEventSourceListener for ControlCenterInner {
fn triggered(self: Rc<Self>) {
self.window.request_redraw();
}
}

View file

@ -4,16 +4,28 @@ use {
control_center::{
CcBehavior, ControlCenterInner, PaneType,
cc_criterion::{CcCriterion, CritImpl, CritRegex},
cc_window::show_window_collapsible,
grid, icon_label, label, read_only_bool,
},
criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher},
egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW,
state::State,
utils::{copyhashmap::CopyHashMap, static_text::StaticText},
tree::ToplevelData,
utils::{
copyhashmap::CopyHashMap, static_text::StaticText,
toplevel_identifier::ToplevelIdentifier,
},
},
ahash::AHashMap,
egui::{
CollapsingHeader, DragValue, Sense, TextFormat, Ui, Widget, cache::CacheTrait,
text::LayoutJob,
},
egui::{CollapsingHeader, DragValue, Sense, TextFormat, Ui, Widget, text::LayoutJob},
linearize::Linearize,
std::rc::{Rc, Weak},
std::{
any::Any,
rc::{Rc, Weak},
},
};
pub enum ClientCrit {
@ -318,7 +330,7 @@ pub fn show_client_collapsible(behavior: &mut CcBehavior, ui: &mut Ui, client: &
});
}
pub fn show_client(_behavior: &mut CcBehavior<'_>, ui: &mut Ui, client: &Client) {
pub fn show_client(behavior: &mut CcBehavior<'_>, ui: &mut Ui, client: &Client) {
grid(ui, ("client", client.id), |ui| {
label(ui, "ID", client.id.to_string());
label(ui, "PID", client.pid_info.pid.to_string());
@ -359,4 +371,93 @@ pub fn show_client(_behavior: &mut CcBehavior<'_>, ui: &mut Ui, client: &Client)
}
});
});
ui.collapsing("Windows", |ui| {
let matcher = ui.memory_mut(|m| {
m.caches
.cache::<ClientWindowMatchersCache>()
.get(behavior.cc, client.id)
.clone()
});
let mut windows: Vec<_> = matcher.windows.lock().keys().copied().collect();
windows.sort();
for id in windows {
let Some(window) = client.state.toplevels.get(&id).and_then(|v| v.upgrade()) else {
continue;
};
show_window_collapsible(behavior, ui, &window);
}
});
}
#[derive(Default)]
struct ClientWindowMatchersCache {
generation: u64,
matchers: AHashMap<ClientId, CachedWindowMatcher>,
}
struct CachedWindowMatcher {
generation: u64,
_matcher: Rc<CritLeafMatcher<ToplevelData>>,
matchers: Rc<WindowMatchers>,
}
struct WindowMatchers {
cc: Weak<ControlCenterInner>,
windows: CopyHashMap<ToplevelIdentifier, ()>,
}
impl ClientWindowMatchersCache {
fn get(&mut self, cc: &Rc<ControlCenterInner>, id: ClientId) -> &Rc<WindowMatchers> {
let res = self.matchers.entry(id).or_insert_with(|| {
let state = &cc.state;
let node = state.cl_matcher_manager.id(id);
let node = state.tl_matcher_manager.client(state, &node);
let matchers = Rc::new(WindowMatchers {
cc: Rc::downgrade(&cc),
windows: Default::default(),
});
let matchers2 = matchers.clone();
let matcher = state.tl_matcher_manager.leaf(&node, move |id| {
matchers2.windows.set(id, ());
if let Some(cc) = matchers2.cc.upgrade() {
cc.window.request_redraw();
}
let matchers2 = matchers2.clone();
Box::new(move || {
matchers2.windows.remove(&id);
if let Some(cc) = matchers2.cc.upgrade() {
cc.window.request_redraw();
}
})
});
let res = CachedWindowMatcher {
generation: 0,
_matcher: matcher,
matchers,
};
state.cl_matcher_manager.rematch_all(state);
state.tl_matcher_manager.rematch_all(state);
res
});
res.generation = self.generation;
&res.matchers
}
}
unsafe impl Sync for ClientWindowMatchersCache {}
unsafe impl Send for ClientWindowMatchersCache {}
impl CacheTrait for ClientWindowMatchersCache {
fn update(&mut self) {
self.matchers.retain(|_, m| m.generation == self.generation);
self.generation += 1;
}
fn len(&self) -> usize {
self.matchers.len()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}

View file

@ -201,7 +201,6 @@ where
}
}
#[expect(dead_code)]
pub fn any(&self, mut any: impl FnMut(&T) -> bool) -> bool {
self.any_(&mut any)
}

View file

@ -17,6 +17,7 @@ enum PaneName {
Input,
LookAndFeel,
Clients,
WindowSearch,
}
impl PaneName {
@ -31,6 +32,7 @@ impl PaneName {
PaneName::Input => "Input",
PaneName::LookAndFeel => "Look and Feel",
PaneName::Clients => "Clients",
PaneName::WindowSearch => "Window Search",
}
}
}
@ -74,6 +76,9 @@ impl ControlCenterInner {
PaneType::LookAndFeel(self.create_look_and_feel_pane())
}
PaneName::Clients => PaneType::Clients(self.create_clients_pane()),
PaneName::WindowSearch => {
PaneType::WindowSearch(self.create_window_search_pane())
}
};
self.open(tree, ty);
ui.ctx().request_repaint();

View file

@ -0,0 +1,481 @@
use {
crate::{
control_center::{
CcBehavior, ControlCenterInner, PaneType,
cc_clients::{ClientCrit, show_client_collapsible},
cc_criterion::{CcCriterion, CritImpl, CritRegex},
grid, icon_label, label, read_only_bool,
},
criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher},
egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW,
state::State,
tree::{NodeId, ToplevelData, ToplevelNode, ToplevelType},
utils::{
copyhashmap::CopyHashMap,
event_listener::{EventListener, LazyEventSourceListener},
static_text::StaticText,
toplevel_identifier::ToplevelIdentifier,
},
},
ahash::AHashMap,
egui::{CollapsingHeader, Sense, TextFormat, Ui, Widget, cache::CacheTrait, text::LayoutJob},
isnt::std_1::primitive::IsntStrExt,
jay_config::window::{
ContentType, GAME_CONTENT, NO_CONTENT_TYPE, PHOTO_CONTENT, VIDEO_CONTENT,
},
linearize::Linearize,
std::{
any::Any,
mem,
rc::{Rc, Weak},
},
};
enum WindowClit {
Client(CcCriterion<ClientCrit>),
Title(CritRegex),
AppId(CritRegex),
Floating,
Visible,
Urgent,
Fullscreen,
Tag(CritRegex),
XClass(CritRegex),
XInstance(CritRegex),
XRole(CritRegex),
Workspace(CritRegex),
ContentTypes(ContentType),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Linearize)]
enum WindowCritTy {
Client,
Title,
AppId,
Floating,
Visible,
Urgent,
Fullscreen,
Tag,
XClass,
XInstance,
XRole,
Workspace,
ContentTypes,
}
impl Default for WindowClit {
fn default() -> Self {
WindowClit::Title(Default::default())
}
}
impl StaticText for WindowCritTy {
fn text(&self) -> &'static str {
match self {
WindowCritTy::Client => "Client",
WindowCritTy::Title => "Title",
WindowCritTy::AppId => "App ID",
WindowCritTy::Floating => "Floating",
WindowCritTy::Visible => "Visible",
WindowCritTy::Urgent => "Urgent",
WindowCritTy::Fullscreen => "Fullscreen",
WindowCritTy::Tag => "Tag",
WindowCritTy::XClass => "X Class",
WindowCritTy::XInstance => "X Instance",
WindowCritTy::XRole => "X Role",
WindowCritTy::Workspace => "Workspace",
WindowCritTy::ContentTypes => "Content Types",
}
}
}
impl CritImpl for WindowClit {
type Type = WindowCritTy;
type Target = ToplevelData;
fn ty(&self) -> Self::Type {
macro_rules! map {
($($n:ident,)*) => {
match self {
$(
Self::$n { .. } => WindowCritTy::$n,
)*
}
};
}
map! {
Client,
Title,
AppId,
Floating,
Visible,
Urgent,
Fullscreen,
Tag,
XClass,
XInstance,
XRole,
Workspace,
ContentTypes,
}
}
fn from_ty(ty: Self::Type) -> Self {
match ty {
WindowCritTy::Client => Self::Client(Default::default()),
WindowCritTy::Title => Self::Title(Default::default()),
WindowCritTy::AppId => Self::AppId(Default::default()),
WindowCritTy::Floating => Self::Floating,
WindowCritTy::Visible => Self::Visible,
WindowCritTy::Urgent => Self::Urgent,
WindowCritTy::Fullscreen => Self::Fullscreen,
WindowCritTy::Tag => Self::Tag(Default::default()),
WindowCritTy::XClass => Self::XClass(Default::default()),
WindowCritTy::XInstance => Self::XInstance(Default::default()),
WindowCritTy::XRole => Self::XRole(Default::default()),
WindowCritTy::Workspace => Self::Workspace(Default::default()),
WindowCritTy::ContentTypes => {
Self::ContentTypes(PHOTO_CONTENT | VIDEO_CONTENT | GAME_CONTENT)
}
}
}
fn show(&mut self, ui: &mut Ui) -> bool {
match self {
WindowClit::Client(v) => v.show(ui),
WindowClit::Title(v) => v.show(ui),
WindowClit::AppId(v) => v.show(ui),
WindowClit::Floating => false,
WindowClit::Visible => false,
WindowClit::Urgent => false,
WindowClit::Fullscreen => false,
WindowClit::Tag(v) => v.show(ui),
WindowClit::XClass(v) => v.show(ui),
WindowClit::XInstance(v) => v.show(ui),
WindowClit::XRole(v) => v.show(ui),
WindowClit::Workspace(v) => v.show(ui),
WindowClit::ContentTypes(v) => show_content_types(ui, v),
}
}
fn to_crit(&self, state: &Rc<State>) -> Option<Rc<dyn CritUpstreamNode<Self::Target>>> {
let m = &state.tl_matcher_manager;
let res = match self {
WindowClit::Client(v) => m.client(state, &v.to_crit(state)?),
WindowClit::Title(v) => m.title(v.to_crit()?),
WindowClit::AppId(v) => m.app_id(v.to_crit()?),
WindowClit::Floating => m.floating(),
WindowClit::Visible => m.visible(),
WindowClit::Urgent => m.urgent(),
WindowClit::Fullscreen => m.fullscreen(),
WindowClit::Tag(v) => m.tag(v.to_crit()?),
WindowClit::XClass(v) => m.class(v.to_crit()?),
WindowClit::XInstance(v) => m.instance(v.to_crit()?),
WindowClit::XRole(v) => m.role(v.to_crit()?),
WindowClit::Workspace(v) => m.workspace(v.to_crit()?),
WindowClit::ContentTypes(v) => m.content_type(*v),
};
Some(res)
}
fn not(
state: &State,
upstream: &Rc<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.not(upstream)
}
fn list(
state: &State,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.list(upstream, all)
}
fn exactly(
state: &State,
n: usize,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.exactly(upstream, n)
}
}
pub struct WindowSearchPane {
state: Rc<State>,
criterion: CcCriterion<WindowClit>,
matched: Rc<Matched>,
leaf: Option<Rc<CritLeafMatcher<ToplevelData>>>,
}
struct Matched {
slf: Weak<ControlCenterInner>,
windows: CopyHashMap<ToplevelIdentifier, ()>,
}
impl Matched {
fn request_frame(&self) {
if let Some(slf) = self.slf.upgrade() {
slf.window.request_redraw();
}
}
}
impl ControlCenterInner {
pub fn create_window_search_pane(self: &Rc<Self>) -> WindowSearchPane {
let mut pane = WindowSearchPane {
state: self.state.clone(),
criterion: Default::default(),
matched: Rc::new(Matched {
slf: Rc::downgrade(self),
windows: Default::default(),
}),
leaf: Default::default(),
};
pane.update_matcher();
pane
}
}
impl WindowSearchPane {
pub fn title(&self, res: &mut String) {
res.push_str("Window Search");
}
pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) {
let mut clear = false;
if self.criterion.show(ui) {
clear = self.update_matcher();
}
ui.separator();
let mut windows: Vec<_> = self.matched.windows.lock().keys().copied().collect();
windows.sort();
for id in windows {
let Some(window) = self.state.toplevels.get(&id).and_then(|v| v.upgrade()) else {
continue;
};
show_window_collapsible(behavior, ui, &window);
}
if clear {
self.matched.windows.clear();
}
}
fn update_matcher(&mut self) -> bool {
let mut clear = false;
let state = &self.state;
if let Some(new) = self.criterion.to_crit(state) {
clear = true;
let matched = self.matched.clone();
let leaf = state.tl_matcher_manager.leaf(&new, move |data| {
matched.windows.set(data, ());
matched.request_frame();
Box::new({
let matched = matched.clone();
move || {
matched.windows.remove(&data);
matched.request_frame();
}
})
});
state.tl_matcher_manager.rematch_all(state);
if self.criterion.any(|c| matches!(c, WindowClit::Client(_))) {
state.cl_matcher_manager.rematch_all(state);
}
self.leaf = Some(leaf);
}
clear
}
}
pub struct WindowPane {
window: Rc<dyn ToplevelNode>,
}
impl ControlCenterInner {
pub fn create_window_pane(self: &Rc<Self>, window: &Rc<dyn ToplevelNode>) -> WindowPane {
WindowPane {
window: window.clone(),
}
}
}
impl WindowPane {
pub fn title(&self, res: &mut String) {
res.push_str("Window");
}
pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) {
show_window(behavior, ui, &*self.window)
}
}
pub fn show_window_collapsible(
behavior: &mut CcBehavior,
ui: &mut Ui,
window: &Rc<dyn ToplevelNode>,
) {
let data = window.tl_data();
let mut layout_job = LayoutJob::default();
layout_job.append(
"Window",
0.0,
TextFormat {
color: ui.style().visuals.widgets.inactive.text_color(),
..Default::default()
},
);
layout_job.append(
&data.title.borrow(),
10.0,
TextFormat {
color: ui.style().visuals.widgets.active.text_color(),
..Default::default()
},
);
let closed = CollapsingHeader::new(layout_job)
.id_salt(("window", data.identifier.get()))
.show(ui, |ui| {
if icon_label(ICON_OPEN_IN_NEW)
.sense(Sense::CLICK)
.ui(ui)
.clicked()
{
behavior.open = Some(PaneType::Window(behavior.cc.create_window_pane(window)));
}
show_window(behavior, ui, &**window)
})
.fully_closed();
if closed {
ensure_listener(ui, behavior, data);
}
}
pub fn show_window(behavior: &mut CcBehavior<'_>, ui: &mut Ui, window: &dyn ToplevelNode) {
let data = window.tl_data();
ensure_listener(ui, behavior, data);
grid(ui, ("window", data.identifier.get()), |ui| {
label(ui, "ID", &*data.identifier.get().to_string());
label(ui, "Title", &*data.title.borrow());
if let Some(w) = data.workspace.get() {
label(ui, "Workspace", &w.name);
}
match &data.kind {
ToplevelType::Container => {
label(ui, "Type", "Container");
}
ToplevelType::Placeholder(_) => {
label(ui, "Type", "Placeholder");
}
ToplevelType::XdgToplevel(t) => {
label(ui, "Type", "xdg_toplevel");
let tag = &*t.tag.borrow();
if tag.is_not_empty() {
label(ui, "Tag", tag);
}
}
ToplevelType::XWindow(t) => {
label(ui, "Type", "X Window");
if let Some(class) = &*t.info.class.borrow() {
label(ui, "Class", class);
}
if let Some(instance) = &*t.info.instance.borrow() {
label(ui, "Instance", instance);
}
if let Some(role) = &*t.info.role.borrow() {
label(ui, "Role", role);
}
}
}
let app_id = &*data.app_id.borrow();
if app_id.is_not_empty() {
label(ui, "App ID", app_id);
}
read_only_bool(ui, "Floating", data.parent_is_float.get());
read_only_bool(ui, "Visible", data.visible.get());
read_only_bool(ui, "Urgent", data.wants_attention.get());
read_only_bool(ui, "Fullscreen", data.is_fullscreen.get());
if let Some(ct) = data.content_type.get() {
label(ui, "Content Type", ct.text());
}
});
if let Some(client) = &data.client {
show_client_collapsible(behavior, ui, client);
}
}
fn ensure_listener(ui: &mut Ui, behavior: &CcBehavior<'_>, data: &ToplevelData) {
ui.memory_mut(|m| {
m.caches
.cache::<WindowPropertyListeners>()
.ensure(behavior.cc, data);
});
}
#[derive(Default)]
struct WindowPropertyListeners {
generation: u64,
listeners: AHashMap<NodeId, WindowPropertyListener>,
}
struct WindowPropertyListener {
_listener: EventListener<dyn LazyEventSourceListener>,
generation: u64,
}
impl WindowPropertyListeners {
fn ensure(&mut self, cc: &Rc<ControlCenterInner>, data: &ToplevelData) {
let listener = self.listeners.entry(data.node_id).or_insert_with(|| {
let listener =
EventListener::new(Rc::downgrade(cc) as Weak<dyn LazyEventSourceListener>);
listener.attach(data.property_changed_source());
WindowPropertyListener {
_listener: listener,
generation: 0,
}
});
listener.generation = self.generation;
}
}
unsafe impl Sync for WindowPropertyListeners {}
unsafe impl Send for WindowPropertyListeners {}
impl CacheTrait for WindowPropertyListeners {
fn update(&mut self) {
self.listeners
.retain(|_, m| m.generation == self.generation);
self.generation += 1;
}
fn len(&self) -> usize {
self.listeners.len()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
fn show_content_types(ui: &mut Ui, ct: &mut ContentType) -> bool {
let mut v = *ct;
let mut photo = (v & PHOTO_CONTENT).0 != 0;
let mut video = (v & VIDEO_CONTENT).0 != 0;
let mut game = (v & GAME_CONTENT).0 != 0;
ui.checkbox(&mut photo, "Photo");
ui.checkbox(&mut video, "Video");
ui.checkbox(&mut game, "Game");
v = NO_CONTENT_TYPE;
if photo {
v |= PHOTO_CONTENT;
}
if video {
v |= VIDEO_CONTENT;
}
if game {
v |= GAME_CONTENT;
}
mem::replace(ct, v) != v
}

View file

@ -234,7 +234,6 @@ impl ClMatcherManager {
self.root(ClmMatchTag::new(string))
}
#[expect(dead_code)]
pub fn id(&self, id: ClientId) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchId(id))
}

View file

@ -936,7 +936,6 @@ impl ToplevelData {
parent.node_is_workspace()
}
#[expect(dead_code)]
pub fn property_changed_source(&self) -> &Rc<LazyEventSource> {
self.property_changed_source
.get_or_init(|| self.state.lazy_event_sources.create_source())

View file

@ -10,7 +10,7 @@ use {
thiserror::Error,
};
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct Opaque {
lo: u64,
hi: u64,

View file

@ -10,7 +10,7 @@ use {
},
};
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Ord, PartialOrd)]
pub struct ToplevelIdentifier(Opaque);
unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {}