From 186d5b694bf7c10f04ac18ce21659c02174ef0ef Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 7 Mar 2026 19:20:39 +0100 Subject: [PATCH] control-center: add in-process control center --- Cargo.lock | 28 + Cargo.toml | 1 + jay-config/src/_private/client.rs | 4 + jay-config/src/_private/ipc.rs | 1 + jay-config/src/lib.rs | 5 + src/cli.rs | 4 + src/cli/control_center.rs | 32 ++ src/compositor.rs | 7 + src/config/handler.rs | 7 + src/control_center.rs | 606 +++++++++++++++++++++ src/control_center/cc_sidebar.rs | 48 ++ src/egui_adapter/egui_platform.rs | 7 - src/ifs.rs | 1 + src/ifs/jay_compositor.rs | 22 +- src/ifs/jay_open_control_center_request.rs | 53 ++ src/main.rs | 1 + src/state.rs | 3 + src/tools/tool_client.rs | 2 +- src/utils/numcell.rs | 8 + toml-config/src/config.rs | 1 + toml-config/src/config/parsers/action.rs | 1 + toml-config/src/default-config.toml | 1 + toml-config/src/lib.rs | 10 +- toml-spec/spec/spec.generated.json | 3 +- toml-spec/spec/spec.generated.md | 4 + toml-spec/spec/spec.yaml | 2 + wire/jay_compositor.txt | 4 + wire/jay_open_control_center_request.txt | 7 + 28 files changed, 859 insertions(+), 14 deletions(-) create mode 100644 src/cli/control_center.rs create mode 100644 src/control_center.rs create mode 100644 src/control_center/cc_sidebar.rs create mode 100644 src/ifs/jay_open_control_center_request.rs create mode 100644 wire/jay_open_control_center_request.txt diff --git a/Cargo.lock b/Cargo.lock index 2d3aefc2..02207e85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "egui_tiles" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef184e589f0a80560bd3b63017634642d1ba112a8a8d9b29341f7cafd04601f" +dependencies = [ + "ahash", + "egui", + "itertools", + "log", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.33.3" @@ -681,6 +699,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -722,6 +749,7 @@ dependencies = [ "clap_complete", "dirs", "egui", + "egui_tiles", "futures-util", "gpu-alloc", "gpu-alloc-types", diff --git a/Cargo.toml b/Cargo.toml index 74602f7d..be0de383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ with_builtin_macros = "0.1.0" blake3 = "1.8.2" run-on-drop = "1.0.0" egui = { version = "0.33.3", default-features = false } +egui_tiles = { version = "0.14.1", default-features = false } [build-dependencies] repc = "0.1.1" diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 9e24c86f..9c3be1e6 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1046,6 +1046,10 @@ impl ConfigClient { self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); } + pub fn open_control_center(&self) { + self.send(&ClientMessage::OpenControlCenter); + } + pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { self.send(&ClientMessage::SetWorkspaceDisplayOrder { order }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index fc61dd85..53e3662d 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -845,6 +845,7 @@ pub enum ClientMessage<'a> { proportional: Option>, monospace: Option>, }, + OpenControlCenter, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index aa975250..0b980479 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -380,3 +380,8 @@ pub fn on_unload(f: impl FnOnce() + 'static) { pub fn set_middle_click_paste_enabled(enabled: bool) { get!().set_middle_click_paste_enabled(enabled); } + +/// Opens the control center. +pub fn open_control_center() { + get!().open_control_center(); +} diff --git a/src/cli.rs b/src/cli.rs index 303fe29c..a650e8b3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ mod clients; mod color; mod color_management; +mod control_center; mod damage_tracking; mod duration; mod generate; @@ -97,6 +98,8 @@ pub enum Cmd { Clients(ClientsArgs), /// Inspect the surface tree. Tree(TreeArgs), + /// Opens the control center. + ControlCenter, /// Prints the Jay version and exits. Version, /// Prints the Jay PID and exits. @@ -243,5 +246,6 @@ pub fn main() { #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), + Cmd::ControlCenter => control_center::main(cli.global), } } diff --git a/src/cli/control_center.rs b/src/cli/control_center.rs new file mode 100644 index 00000000..3a3f2b93 --- /dev/null +++ b/src/cli/control_center.rs @@ -0,0 +1,32 @@ +use { + crate::{ + cli::GlobalArgs, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{jay_compositor, jay_open_control_center_request}, + }, + std::rc::Rc, +}; + +pub fn main(global: GlobalArgs) { + with_tool_client(global.log_level, |tc| async move { + let cc = ControlCenter { tc: tc.clone() }; + cc.run().await; + }); +} + +struct ControlCenter { + tc: Rc, +} + +impl ControlCenter { + async fn run(self) { + let tc = &self.tc; + let comp = tc.jay_compositor().await; + let id = tc.id(); + tc.send(jay_compositor::OpenControlCenter { self_id: comp, id }); + jay_open_control_center_request::Failed::handle(&tc, id, (), |_, ev| { + fatal!("Could not open the control center: {}", ev.msg); + }); + tc.round_trip().await; + } +} diff --git a/src/compositor.rs b/src/compositor.rs index 0482f6d3..30909c7e 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -14,6 +14,7 @@ use { clientmem::{self, ClientMemError}, cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, config::ConfigProxy, + control_center::redraw_control_centers, copy_device::CopyDeviceRegistry, cpu_worker::{CpuWorker, CpuWorkerError}, criteria::{ @@ -393,6 +394,7 @@ fn start_compositor2( lazy_event_sources: Default::default(), bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), egg_state: Default::default(), + control_centers: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -417,6 +419,7 @@ fn start_compositor2( let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); ring.run()?; state.clear(); + engine.clear(); Ok(()) } @@ -597,6 +600,10 @@ fn start_global_event_handlers(state: &Rc) -> Vec> { "lazy event sources", handle_lazy_event_sources(state.clone()), ), + eng.spawn( + "redraw control centers", + redraw_control_centers(state.clone()), + ), ] } diff --git a/src/config/handler.rs b/src/config/handler.rs index 86f8bbb3..54196937 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1811,6 +1811,12 @@ impl ConfigProxyHandler { self.state.set_egui_fonts(proportional, monospace); } + fn handle_open_control_center(&self) { + if let Err(e) = self.state.open_control_center() { + log::error!("Could not open control center: {}", ErrorFmt(e)); + } + } + fn handle_set_log_level(&self, level: ConfigLogLevel) { self.state.set_log_level(level.into()); } @@ -3319,6 +3325,7 @@ impl ConfigProxyHandler { proportional, monospace, } => self.handle_set_egui_fonts(proportional, monospace), + ClientMessage::OpenControlCenter => self.handle_open_control_center(), } Ok(()) } diff --git a/src/control_center.rs b/src/control_center.rs new file mode 100644 index 00000000..c49dfbad --- /dev/null +++ b/src/control_center.rs @@ -0,0 +1,606 @@ +use { + crate::{ + egui_adapter::egui_platform::{ + EggError, EggWindow, EggWindowOwner, + icons::{ICON_CLOSE, ICON_DRAG_INDICATOR, ICON_INFO}, + }, + macros::Bitflag, + state::State, + utils::{ + asyncevent::AsyncEvent, copyhashmap::CopyHashMap, numcell::NumCell, + static_text::StaticText, + }, + }, + egui::{ + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, CursorIcon, DragValue, Frame, + Grid, InnerResponse, Label, Layout, Response, Rgba, RichText, ScrollArea, Sense, SidePanel, + Stroke, TextBuffer, TextEdit, Ui, UiBuilder, Visuals, Widget, WidgetText, emath::Numeric, + vec2, + }, + egui_tiles::{ResizeState, TabState, Tile, TileId, Tiles, Tree}, + linearize::{Linearize, LinearizeExt}, + std::{ + cell::RefCell, + hash::Hash, + mem, + ops::{Deref, DerefMut, RangeInclusive}, + rc::Rc, + }, + thiserror::Error, +}; + +mod cc_sidebar; + +#[derive(Debug, Error)] +pub enum ControlCenterError { + #[error("Could not get the egg context")] + GetEggContext(#[source] EggError), +} + +linear_ids!(ControlCenterIds, ControlCenterId, u64); + +pub async fn redraw_control_centers(state: Rc) { + let cc = &state.control_centers; + loop { + cc.redraw.triggered().await; + let interests = cc.change.take(); + for cc in cc.control_centers.lock().values() { + if cc.inner.interests.interests.get().intersects(interests) { + cc.inner.window.request_redraw(); + } + } + } +} + +#[derive(Default)] +pub struct ControlCenters { + ids: ControlCenterIds, + change: NumCell, + redraw: AsyncEvent, + control_centers: CopyHashMap>, +} + +bitflags! { + ControlCenterInterest: u32; + _UNUSED, +} + +pub struct ControlCenter { + inner: Rc, +} + +linear_ids!(PaneIds, PaneId, u64); + +struct ControlCenterInner { + id: ControlCenterId, + state: Rc, + tree: RefCell, + window: Rc, + pane_ids: PaneIds, + interests: Rc, +} + +#[derive(Default)] +struct Interests { + interests: NumCell, + interests_array: [NumCell; ::Type::BITS as usize], +} + +struct Settings { + tree: Tree, +} + +struct Pane { + id: PaneId, + ps: PaneState, + own_interests: ControlCenterInterest, + cc_interests: Rc, + ty: PaneType, +} + +struct PaneState { + errors: Vec, +} + +enum PaneType {} + +struct CcBehavior<'a> { + #[expect(dead_code)] + cc: &'a Rc, + close: Option, + open: Option, +} + +impl ControlCenters { + pub fn clear(&self) { + self.control_centers.clear(); + } +} + +impl Pane { + fn title(&self, _res: &mut String) { + match self.ty {} + } + + fn show(&mut self, _behavior: &mut CcBehavior<'_>, _ui: &mut Ui) { + match self.ty {} + } +} + +impl PaneType { + fn interest(&self) -> ControlCenterInterest { + match *self {} + } +} + +impl egui_tiles::Behavior for CcBehavior<'_> { + fn pane_ui(&mut self, ui: &mut Ui, tile_id: TileId, pane: &mut Pane) -> egui_tiles::UiResponse { + let mut drag = false; + Frame::central_panel(ui.style()).show(ui, |ui| { + ui.horizontal(|ui| { + drag = ui + .add(icon_label(ICON_DRAG_INDICATOR).sense(Sense::drag())) + .total_drag_delta() + .map(|d| d.length() >= 5.0) + .unwrap_or(false); + let mut title = String::new(); + pane.title(&mut title); + if ui + .add(icon_label(&title).sense(Sense::click())) + .middle_clicked() + { + self.close = Some(tile_id); + } + if ui + .add(icon_label(ICON_CLOSE).sense(Sense::click())) + .clicked() + { + self.close = Some(tile_id); + } + }); + ui.separator(); + show_errors(ui, &mut pane.ps); + ui.scope_builder(UiBuilder::new().id(("pane", pane.id)), |ui| { + ScrollArea::vertical().show(ui, |ui| { + ui.allocate_space(vec2(ui.available_width(), 0.0)); + pane.show(self, ui); + }); + }); + }); + if drag { + egui_tiles::UiResponse::DragStarted + } else { + egui_tiles::UiResponse::None + } + } + + fn tab_title_for_pane(&mut self, _pane: &Pane) -> WidgetText { + "".into() + } + + fn tab_hover_cursor_icon(&self) -> CursorIcon { + CursorIcon::Default + } + + fn tab_title_for_tile(&mut self, tiles: &Tiles, tile_id: TileId) -> WidgetText { + fn add_title(tiles: &Tiles, res: &mut String, first: &mut bool, tile_id: TileId) { + if !mem::take(first) { + res.push_str("/"); + } + let Some(tile) = tiles.get(tile_id) else { + res.push_str("MISSING TILE"); + return; + }; + match tile { + Tile::Pane(p) => p.title(res), + Tile::Container(c) => { + let mut first = true; + for &tile_id in c.children() { + add_title(tiles, res, &mut first, tile_id); + } + } + } + } + let mut res = String::new(); + let mut first = true; + add_title(tiles, &mut res, &mut first, tile_id); + res.into() + } + + fn on_tab_button( + &mut self, + _tiles: &Tiles, + tile_id: TileId, + button_response: Response, + ) -> Response { + if button_response.middle_clicked() { + self.close = Some(tile_id); + } + button_response + } + + fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> Stroke { + match resize_state { + ResizeState::Idle => style.visuals.widgets.noninteractive.bg_stroke, + ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, + ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, + } + } + + fn tab_bar_color(&self, visuals: &Visuals) -> Color32 { + (Rgba::from(visuals.panel_fill) * Rgba::from_gray(0.8)).into() + } + + fn tab_bg_color( + &self, + visuals: &Visuals, + _tiles: &Tiles, + _tile_id: TileId, + state: &TabState, + ) -> Color32 { + match state.active { + true => visuals.panel_fill, + false => self.tab_bar_color(visuals), + } + } +} + +impl EggWindowOwner for ControlCenterInner { + fn close(&self) { + self.close(); + } + + fn render(self: Rc, ctx: &Context) { + let settings = &mut *self.tree.borrow_mut(); + SidePanel::left("sidebar").show(ctx, |ui| self.show_sidebar(&mut settings.tree, ui)); + CentralPanel::default() + .frame( + Frame::central_panel(&ctx.style()) + .outer_margin(0.0) + .inner_margin(0.0), + ) + .show(ctx, |ui| { + let tree = &mut settings.tree; + let mut behavior = CcBehavior { + cc: &self, + close: Default::default(), + open: Default::default(), + }; + tree.ui(&mut behavior, ui); + if let Some(close) = behavior.close { + tree.set_visible(close, false); + tree.remove_recursively(close); + ui.ctx().request_repaint(); + } + if let Some(ty) = behavior.open { + self.open(tree, ty); + ui.ctx().request_repaint(); + } + }); + } +} + +impl State { + pub fn open_control_center(self: &Rc) -> Result, ControlCenterError> { + let ctx = self + .get_egg_context() + .map_err(ControlCenterError::GetEggContext)?; + let window = ctx.create_window("Control Center"); + let cc = Rc::new(ControlCenter { + inner: Rc::new(ControlCenterInner { + id: self.control_centers.ids.next(), + window, + state: self.clone(), + tree: RefCell::new(Settings { + tree: Tree::new_tabs("abcd", vec![]), + }), + pane_ids: Default::default(), + interests: Default::default(), + }), + }); + cc.inner.window.set_owner(Some(cc.inner.clone())); + self.control_centers + .control_centers + .set(cc.inner.id, cc.clone()); + Ok(cc) + } + + #[expect(dead_code)] + pub fn trigger_cci(&self, cci: ControlCenterInterest) { + self.control_centers.change.or_assign(cci); + self.control_centers.redraw.trigger(); + } +} + +impl ControlCenterInner { + fn close(&self) { + self.window.set_owner(None); + self.tree.borrow_mut().tree = Tree::empty(""); + self.state.control_centers.control_centers.remove(&self.id); + } +} + +impl Drop for ControlCenter { + fn drop(&mut self) { + self.inner.close(); + } +} + +impl ControlCenterInner { + fn create_pane(&self, ty: PaneType) -> Pane { + let pane = Pane { + id: self.pane_ids.next(), + ps: PaneState { + errors: Default::default(), + }, + own_interests: ty.interest(), + cc_interests: self.interests.clone(), + ty, + }; + let own = pane.own_interests; + for (idx, v) in pane.cc_interests.interests_array.iter().enumerate() { + let interest = ControlCenterInterest(1 << idx); + if own.intersects(interest) && v.fetch_add(1) == 0 { + pane.cc_interests.interests.or_assign(interest); + } + } + pane + } + + fn open(&self, tree: &mut Tree, ty: PaneType) { + let _ = tree; + #[expect(unused_variables, unreachable_code)] + let pane = self.create_pane(ty); + let id = tree.tiles.insert_pane(pane); + if let Some(root) = tree.root + && let Some(tile) = tree.tiles.get_mut(root) + { + match tile { + Tile::Container(c) => { + c.add_child(id); + } + Tile::Pane(_) => { + let root = tree.tiles.insert_tab_tile(vec![root, id]); + tree.root = Some(root); + } + } + } else { + tree.root = Some(id); + } + tree.make_active(|t, _| t == id); + } +} + +impl Drop for Pane { + fn drop(&mut self) { + let own = self.own_interests; + for (idx, v) in self.cc_interests.interests_array.iter().enumerate() { + let interest = ControlCenterInterest(1 << idx); + if own.intersects(interest) && v.fetch_sub(1) == 1 { + self.cc_interests.interests.and_assign(!interest); + } + } + } +} + +fn icon_label(icon: &str) -> Label { + Label::new(icon).selectable(false) +} + +#[expect(dead_code)] +fn grid_label(ui: &mut Ui, label: &str) { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label(label); + }); +} + +fn grid_label_ui(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { + ui.with_layout(Layout::right_to_left(Align::Center), add_contents) +} + +#[expect(dead_code)] +fn tip(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { + icon_label(ICON_INFO).ui(ui).on_hover_ui(add_contents); +} + +#[expect(dead_code)] +fn text_edit(ui: &mut Ui, v: &mut dyn TextBuffer) -> Response { + TextEdit::singleline(v) + .clip_text(false) + .min_size(vec2(200.0, 0.0)) + .ui(ui) +} + +fn show_errors(ui: &mut Ui, pane: &mut PaneState) { + if pane.errors.is_empty() { + return; + } + let mut to_remove = None; + for (idx, e) in pane.errors.iter().enumerate() { + ui.horizontal(|ui| { + Frame::new().inner_margin(5.0).show(ui, |ui| { + if ui.button(ICON_CLOSE).clicked() { + to_remove = Some(idx); + } + ui.label( + RichText::new("Error:") + .strong() + .color(ui.style().visuals.error_fg_color), + ); + ui.add(Label::new(e).wrap()); + }); + }); + } + if let Some(idx) = to_remove { + pane.errors.remove(idx); + ui.ctx().request_repaint(); + } + ui.separator(); +} + +#[expect(dead_code)] +fn grid( + ui: &mut Ui, + id_salt: impl Hash, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> InnerResponse { + let mut spacing = ui.spacing().item_spacing; + spacing.x *= 3.0; + Grid::new(id_salt).spacing(spacing).show(ui, add_contents) +} + +fn row(ui: &mut Ui, name: &str, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + row_ui(ui, name, |_| (), add_contents) +} + +fn row_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> S, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> R { + let ui = &mut *ui.row(); + grid_label_ui(ui, |ui| { + ui.label(name); + label(ui); + }); + add_contents(ui) +} + +#[expect(dead_code)] +fn bool(ui: &mut Ui, name: &str, old: bool, set: impl FnOnce(bool)) { + bool_ui(ui, name, |_| (), old, set); +} + +fn bool_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: bool, + set: impl FnOnce(bool), +) { + row_ui(ui, name, label, |ui| { + if Checkbox::without_text(&mut v).ui(ui).changed() { + set(v); + } + }); +} + +#[expect(dead_code)] +fn read_only_bool(ui: &mut Ui, name: &str, old: bool) { + read_only_bool_ui(ui, name, |_| (), old); +} + +fn read_only_bool_ui(ui: &mut Ui, name: &str, label: impl FnOnce(&mut Ui) -> R, mut v: bool) { + row_ui(ui, name, label, |ui| { + ui.add_enabled_ui(false, |ui| Checkbox::without_text(&mut v).ui(ui)); + }); +} + +#[expect(dead_code)] +fn combo_box(ui: &mut Ui, name: &str, old: T, set: impl FnOnce(T)) +where + T: StaticText + Linearize + PartialEq + Copy, +{ + combo_box_ui(ui, name, |_| (), old, set); +} + +fn combo_box_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: T, + set: impl FnOnce(T), +) where + T: StaticText + Linearize + PartialEq + Copy, +{ + row_ui(ui, name, label, |ui| { + let old = v; + ComboBox::from_id_salt(name) + .selected_text(v.text()) + .show_ui(ui, |ui| { + for s in T::variants() { + ui.selectable_value(&mut v, s, s.text()); + } + }); + if old != v { + set(v); + } + }); +} + +#[expect(dead_code)] +fn drag_value( + ui: &mut Ui, + name: &str, + old: N, + range: RangeInclusive, + speed: f64, + set: impl FnOnce(N), +) where + N: Numeric, +{ + drag_value_ui(ui, name, |_| (), old, range, speed, set); +} + +fn drag_value_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: N, + range: RangeInclusive, + speed: f64, + set: impl FnOnce(N), +) where + N: Numeric, +{ + row_ui(ui, name, label, |ui| { + if DragValue::new(&mut v) + .range(range) + .speed(speed) + .ui(ui) + .changed() + { + set(v); + } + }); +} + +#[expect(dead_code)] +fn label(ui: &mut Ui, name: &str, text: impl Into) { + row(ui, name, |ui| ui.label(text)); +} + +trait GridExt { + fn row(&mut self) -> impl DerefMut; +} + +impl GridExt for Ui { + fn row(&mut self) -> impl DerefMut { + GridRow { ui: self } + } +} + +struct GridRow<'a> { + ui: &'a mut Ui, +} + +impl Deref for GridRow<'_> { + type Target = Ui; + + fn deref(&self) -> &Self::Target { + self.ui + } +} + +impl DerefMut for GridRow<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.ui + } +} + +impl Drop for GridRow<'_> { + fn drop(&mut self) { + self.end_row(); + } +} diff --git a/src/control_center/cc_sidebar.rs b/src/control_center/cc_sidebar.rs new file mode 100644 index 00000000..2b76420b --- /dev/null +++ b/src/control_center/cc_sidebar.rs @@ -0,0 +1,48 @@ +use { + crate::control_center::{ControlCenterInner, Pane}, + egui::{Align, Layout, ScrollArea, Ui, ViewportCommand}, + egui_tiles::Tree, + linearize::{Linearize, LinearizeExt}, + std::{rc::Rc, sync::LazyLock}, +}; + +#[derive(Copy, Clone, Linearize)] +enum PaneName {} + +impl PaneName { + fn name(self) -> &'static str { + match self {} + } +} + +static TYPES: LazyLock> = LazyLock::new(|| { + let mut res: Vec<_> = PaneName::variants().collect(); + res.sort_by_key(|t| t.name()); + res +}); + +impl ControlCenterInner { + pub fn show_sidebar(self: &Rc, tree: &mut Tree, ui: &mut Ui) { + ui.with_layout( + Layout::top_down(Align::Center).with_cross_justify(true), + |ui| { + ui.add_space(6.0); + if ui.button("Close").clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Close); + } + ui.separator(); + ScrollArea::vertical().show(ui, |ui| { + for &ty in &*TYPES { + if ui.button(ty.name()).clicked() { + let _ty = match ty {}; + #[expect(unreachable_code)] + self.open(tree, _ty); + ui.ctx().request_repaint(); + } + } + ui.add_space(3.0); + }) + }, + ); + } +} diff --git a/src/egui_adapter/egui_platform.rs b/src/egui_adapter/egui_platform.rs index 6c063855..1d1afd77 100644 --- a/src/egui_adapter/egui_platform.rs +++ b/src/egui_adapter/egui_platform.rs @@ -112,11 +112,8 @@ pub enum EggError { pub mod icons { #[expect(dead_code)] pub const ICON_ADD: &str = "\u{e145}"; - #[expect(dead_code)] pub const ICON_CLOSE: &str = "\u{e5cd}"; - #[expect(dead_code)] pub const ICON_DRAG_INDICATOR: &str = "\u{e945}"; - #[expect(dead_code)] pub const ICON_INFO: &str = "\u{e88e}"; #[expect(dead_code)] pub const ICON_OPEN_IN_NEW: &str = "\u{e89e}"; @@ -361,7 +358,6 @@ impl EggState { } impl State { - #[expect(dead_code)] pub fn get_egg_context(self: &Rc) -> Result, EggError> { if let Some(ctx) = self.egg_state.ctx.get() { return Ok(ctx); @@ -467,7 +463,6 @@ impl State { } impl EggContext { - #[expect(dead_code)] pub fn create_window(self: &Rc, title: &str) -> Rc { let i = &self.inner; let wl_surface = i.wl_compositor.create_surface(); @@ -628,12 +623,10 @@ impl EggSeatInner { } impl EggWindow { - #[expect(dead_code)] pub fn request_redraw(&self) { self.inner.want_frame(); } - #[expect(dead_code)] pub fn set_owner(&self, owner: Option>) { self.inner.owner.set(owner); } diff --git a/src/ifs.rs b/src/ifs.rs index 2814a144..f1168908 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -21,6 +21,7 @@ pub mod jay_ei_session_builder; pub mod jay_idle; pub mod jay_input; pub mod jay_log_file; +pub mod jay_open_control_center_request; pub mod jay_output; pub mod jay_pointer; pub mod jay_popup_ext_manager_v1; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index d0f8c3d4..e37c11a3 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -11,6 +11,7 @@ use { jay_idle::JayIdle, jay_input::JayInput, jay_log_file::JayLogFile, + jay_open_control_center_request::JayOpenControlCenterRequest, jay_output::JayOutput, jay_pointer::JayPointer, jay_randr::JayRandr, @@ -77,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 27 + 28 } fn required_caps(&self) -> ClientCaps { @@ -541,6 +542,25 @@ impl JayCompositorRequestHandler for JayCompositor { }); Ok(()) } + + fn open_control_center( + &self, + req: OpenControlCenter, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let obj = Rc::new(JayOpenControlCenterRequest { + id: req.id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + if let Err(e) = self.client.state.open_control_center() { + obj.send_failed(e); + } + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_open_control_center_request.rs b/src/ifs/jay_open_control_center_request.rs new file mode 100644 index 00000000..a5c0ef5d --- /dev/null +++ b/src/ifs/jay_open_control_center_request.rs @@ -0,0 +1,53 @@ +use { + crate::{ + client::{Client, ClientError}, + leaks::Tracker, + object::{Object, Version}, + utils::errorfmt::ErrorFmt, + wire::{JayOpenControlCenterRequestId, jay_open_control_center_request::*}, + }, + std::{error::Error, rc::Rc}, + thiserror::Error, +}; + +pub struct JayOpenControlCenterRequest { + pub id: JayOpenControlCenterRequestId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl JayOpenControlCenterRequest { + pub fn send_failed(&self, err: impl Error) { + let msg = &ErrorFmt(err).to_string(); + self.client.event(Failed { + self_id: self.id, + msg, + }); + } +} + +impl JayOpenControlCenterRequestRequestHandler for JayOpenControlCenterRequest { + type Error = JayOpenControlCenterRequestError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = JayOpenControlCenterRequest; + version = self.version; +} + +impl Object for JayOpenControlCenterRequest {} + +simple_add_obj!(JayOpenControlCenterRequest); + +#[derive(Debug, Error)] +pub enum JayOpenControlCenterRequestError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayOpenControlCenterRequestError, ClientError); diff --git a/src/main.rs b/src/main.rs index 73440c61..17b1662b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ mod clientmem; mod cmm; mod compositor; mod config; +mod control_center; mod copy_device; mod cpu_worker; mod criteria; diff --git a/src/state.rs b/src/state.rs index e1b22f7b..92e4daa6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -16,6 +16,7 @@ use { cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager}, compositor::{LIBEI_SOCKET, LogLevel}, config::ConfigProxy, + control_center::ControlCenters, copy_device::CopyDeviceRegistry, cpu_worker::CpuWorker, criteria::{clm::ClMatcherManager, tlm::TlMatcherManager}, @@ -297,6 +298,7 @@ pub struct State { pub lazy_event_sources: Rc, pub bo_drop_queue: Rc>>, pub egg_state: EggState, + pub control_centers: ControlCenters, } // impl Drop for State { @@ -1162,6 +1164,7 @@ impl State { self.lazy_event_sources.clear(); self.bo_drop_queue.kill(); self.egg_state.clear(); + self.control_centers.clear(); } pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 3464fed8..dc680d88 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -334,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(27), + version: s.jay_compositor.1.min(28), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/utils/numcell.rs b/src/utils/numcell.rs index a8fc11c1..9f9a3ab5 100644 --- a/src/utils/numcell.rs +++ b/src/utils/numcell.rs @@ -100,6 +100,14 @@ impl NumCell { { !self.is_zero() } + + #[inline(always)] + pub fn take(&self) -> T + where + T: Default, + { + self.t.replace(T::default()) + } } impl + Copy> BitOr for &'_ NumCell { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 8f51e717..915c686b 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -90,6 +90,7 @@ pub enum SimpleCommand { ToggleSimpleImEnabled, ReloadSimpleIm, EnableUnicodeInput, + OpenControlCenter, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 7ea8fec4..3e0e4702 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -167,6 +167,7 @@ impl ActionParser<'_> { "toggle-simple-im-enabled" => ToggleSimpleImEnabled, "reload-simple-im" => ReloadSimpleIm, "enable-unicode-input" => EnableUnicodeInput, + "open-control-center" => OpenControlCenter, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/default-config.toml b/toml-config/src/default-config.toml index fd572891..f8c3026f 100644 --- a/toml-config/src/default-config.toml +++ b/toml-config/src/default-config.toml @@ -31,6 +31,7 @@ alt-m = "toggle-mono" alt-u = "toggle-fullscreen" alt-f = "focus-parent" +alt-c = "open-control-center" alt-shift-c = "close" alt-shift-f = "toggle-floating" Super_L = { type = "exec", exec = "alacritty" } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 9d2b5dfb..04222433 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -36,10 +36,11 @@ use { is_reload, keyboard::Keymap, logging::set_log_level, - on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled, - set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, - set_idle, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, - set_show_float_pin_icon, set_show_titles, set_ui_drag_enabled, set_ui_drag_threshold, + on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload, + set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, + set_float_above_fullscreen, set_idle, set_idle_grace_period, + set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, set_show_titles, + set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, tasks::{self, JoinHandle}, @@ -245,6 +246,7 @@ impl Action { let persistent = state.persistent.clone(); b.new(move || persistent.seat.enable_unicode_input()) } + SimpleCommand::OpenControlCenter => b.new(open_control_center), }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index ad6aeb6c..c1b93a0a 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1928,7 +1928,8 @@ "disable-simple-im", "toggle-simple-im-enabled", "reload-simple-im", - "enable-unicode-input" + "enable-unicode-input", + "open-control-center" ] }, "SimpleIm": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index fb1ef758..2bf15c1e 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4427,6 +4427,10 @@ The string should have one of the following values: This has no effect if the simple IM is not currently active. +- `open-control-center`: + + Opens the control center. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 81442597..d4a1d12d 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1089,6 +1089,8 @@ SimpleActionName: Enables Unicode input in the simple, XCompose based input method. This has no effect if the simple IM is not currently active. + - value: open-control-center + description: Opens the control center. Color: diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 019f9ea3..45f917ca 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -135,6 +135,10 @@ request get_pid (since = 27) { } +request open_control_center (since = 28) { + id: id(jay_open_control_center_request), +} + # events event client_id { diff --git a/wire/jay_open_control_center_request.txt b/wire/jay_open_control_center_request.txt new file mode 100644 index 00000000..7f1cfb3a --- /dev/null +++ b/wire/jay_open_control_center_request.txt @@ -0,0 +1,7 @@ +request destroy { + +} + +event failed { + msg: str, +}