476 lines
14 KiB
Rust
476 lines
14 KiB
Rust
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::{
|
|
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 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
|
|
}
|