1
0
Fork 0
forked from wry/wry

control-center: add in-process control center

This commit is contained in:
Julian Orth 2026-03-07 19:20:39 +01:00
parent 008e8a671a
commit 186d5b694b
28 changed files with 859 additions and 14 deletions

28
Cargo.lock generated
View file

@ -407,6 +407,24 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "emath" name = "emath"
version = "0.33.3" version = "0.33.3"
@ -681,6 +699,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0" checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.17"
@ -722,6 +749,7 @@ dependencies = [
"clap_complete", "clap_complete",
"dirs", "dirs",
"egui", "egui",
"egui_tiles",
"futures-util", "futures-util",
"gpu-alloc", "gpu-alloc",
"gpu-alloc-types", "gpu-alloc-types",

View file

@ -70,6 +70,7 @@ with_builtin_macros = "0.1.0"
blake3 = "1.8.2" blake3 = "1.8.2"
run-on-drop = "1.0.0" run-on-drop = "1.0.0"
egui = { version = "0.33.3", default-features = false } egui = { version = "0.33.3", default-features = false }
egui_tiles = { version = "0.14.1", default-features = false }
[build-dependencies] [build-dependencies]
repc = "0.1.1" repc = "0.1.1"

View file

@ -1046,6 +1046,10 @@ impl ConfigClient {
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
} }
pub fn open_control_center(&self) {
self.send(&ClientMessage::OpenControlCenter);
}
pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) {
self.send(&ClientMessage::SetWorkspaceDisplayOrder { order }); self.send(&ClientMessage::SetWorkspaceDisplayOrder { order });
} }

View file

@ -845,6 +845,7 @@ pub enum ClientMessage<'a> {
proportional: Option<Vec<&'a str>>, proportional: Option<Vec<&'a str>>,
monospace: Option<Vec<&'a str>>, monospace: Option<Vec<&'a str>>,
}, },
OpenControlCenter,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -380,3 +380,8 @@ pub fn on_unload(f: impl FnOnce() + 'static) {
pub fn set_middle_click_paste_enabled(enabled: bool) { pub fn set_middle_click_paste_enabled(enabled: bool) {
get!().set_middle_click_paste_enabled(enabled); get!().set_middle_click_paste_enabled(enabled);
} }
/// Opens the control center.
pub fn open_control_center() {
get!().open_control_center();
}

View file

@ -1,6 +1,7 @@
mod clients; mod clients;
mod color; mod color;
mod color_management; mod color_management;
mod control_center;
mod damage_tracking; mod damage_tracking;
mod duration; mod duration;
mod generate; mod generate;
@ -97,6 +98,8 @@ pub enum Cmd {
Clients(ClientsArgs), Clients(ClientsArgs),
/// Inspect the surface tree. /// Inspect the surface tree.
Tree(TreeArgs), Tree(TreeArgs),
/// Opens the control center.
ControlCenter,
/// Prints the Jay version and exits. /// Prints the Jay version and exits.
Version, Version,
/// Prints the Jay PID and exits. /// Prints the Jay PID and exits.
@ -243,5 +246,6 @@ pub fn main() {
#[cfg(feature = "it")] #[cfg(feature = "it")]
Cmd::RunTests => crate::it::run_tests(), Cmd::RunTests => crate::it::run_tests(),
Cmd::Reexec(a) => reexec::main(cli.global, a), Cmd::Reexec(a) => reexec::main(cli.global, a),
Cmd::ControlCenter => control_center::main(cli.global),
} }
} }

32
src/cli/control_center.rs Normal file
View file

@ -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<ToolClient>,
}
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;
}
}

View file

@ -14,6 +14,7 @@ use {
clientmem::{self, ClientMemError}, clientmem::{self, ClientMemError},
cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries},
config::ConfigProxy, config::ConfigProxy,
control_center::redraw_control_centers,
copy_device::CopyDeviceRegistry, copy_device::CopyDeviceRegistry,
cpu_worker::{CpuWorker, CpuWorkerError}, cpu_worker::{CpuWorker, CpuWorkerError},
criteria::{ criteria::{
@ -393,6 +394,7 @@ fn start_compositor2(
lazy_event_sources: Default::default(), lazy_event_sources: Default::default(),
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
egg_state: Default::default(), egg_state: Default::default(),
control_centers: Default::default(),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);
@ -417,6 +419,7 @@ fn start_compositor2(
let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future));
ring.run()?; ring.run()?;
state.clear(); state.clear();
engine.clear();
Ok(()) Ok(())
} }
@ -597,6 +600,10 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
"lazy event sources", "lazy event sources",
handle_lazy_event_sources(state.clone()), handle_lazy_event_sources(state.clone()),
), ),
eng.spawn(
"redraw control centers",
redraw_control_centers(state.clone()),
),
] ]
} }

View file

@ -1811,6 +1811,12 @@ impl ConfigProxyHandler {
self.state.set_egui_fonts(proportional, monospace); 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) { fn handle_set_log_level(&self, level: ConfigLogLevel) {
self.state.set_log_level(level.into()); self.state.set_log_level(level.into());
} }
@ -3319,6 +3325,7 @@ impl ConfigProxyHandler {
proportional, proportional,
monospace, monospace,
} => self.handle_set_egui_fonts(proportional, monospace), } => self.handle_set_egui_fonts(proportional, monospace),
ClientMessage::OpenControlCenter => self.handle_open_control_center(),
} }
Ok(()) Ok(())
} }

606
src/control_center.rs Normal file
View file

@ -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<State>) {
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<ControlCenterInterest>,
redraw: AsyncEvent,
control_centers: CopyHashMap<ControlCenterId, Rc<ControlCenter>>,
}
bitflags! {
ControlCenterInterest: u32;
_UNUSED,
}
pub struct ControlCenter {
inner: Rc<ControlCenterInner>,
}
linear_ids!(PaneIds, PaneId, u64);
struct ControlCenterInner {
id: ControlCenterId,
state: Rc<State>,
tree: RefCell<Settings>,
window: Rc<EggWindow>,
pane_ids: PaneIds,
interests: Rc<Interests>,
}
#[derive(Default)]
struct Interests {
interests: NumCell<ControlCenterInterest>,
interests_array: [NumCell<u64>; <ControlCenterInterest as Bitflag>::Type::BITS as usize],
}
struct Settings {
tree: Tree<Pane>,
}
struct Pane {
id: PaneId,
ps: PaneState,
own_interests: ControlCenterInterest,
cc_interests: Rc<Interests>,
ty: PaneType,
}
struct PaneState {
errors: Vec<String>,
}
enum PaneType {}
struct CcBehavior<'a> {
#[expect(dead_code)]
cc: &'a Rc<ControlCenterInner>,
close: Option<TileId>,
open: Option<PaneType>,
}
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<Pane> 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<Pane>, tile_id: TileId) -> WidgetText {
fn add_title(tiles: &Tiles<Pane>, 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<Pane>,
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<Pane>,
_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<Self>, 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<Self>) -> Result<Rc<ControlCenter>, 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<Pane>, 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<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
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<R>(
ui: &mut Ui,
id_salt: impl Hash,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let mut spacing = ui.spacing().item_spacing;
spacing.x *= 3.0;
Grid::new(id_salt).spacing(spacing).show(ui, add_contents)
}
fn row<R>(ui: &mut Ui, name: &str, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
row_ui(ui, name, |_| (), add_contents)
}
fn row_ui<R, S>(
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<R>(
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<R>(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<T>(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<R, T>(
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<N>(
ui: &mut Ui,
name: &str,
old: N,
range: RangeInclusive<N>,
speed: f64,
set: impl FnOnce(N),
) where
N: Numeric,
{
drag_value_ui(ui, name, |_| (), old, range, speed, set);
}
fn drag_value_ui<R, N>(
ui: &mut Ui,
name: &str,
label: impl FnOnce(&mut Ui) -> R,
mut v: N,
range: RangeInclusive<N>,
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<WidgetText>) {
row(ui, name, |ui| ui.label(text));
}
trait GridExt {
fn row(&mut self) -> impl DerefMut<Target = Ui>;
}
impl GridExt for Ui {
fn row(&mut self) -> impl DerefMut<Target = Ui> {
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();
}
}

View file

@ -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<Vec<PaneName>> = 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<Self>, tree: &mut Tree<Pane>, 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);
})
},
);
}
}

View file

@ -112,11 +112,8 @@ pub enum EggError {
pub mod icons { pub mod icons {
#[expect(dead_code)] #[expect(dead_code)]
pub const ICON_ADD: &str = "\u{e145}"; pub const ICON_ADD: &str = "\u{e145}";
#[expect(dead_code)]
pub const ICON_CLOSE: &str = "\u{e5cd}"; pub const ICON_CLOSE: &str = "\u{e5cd}";
#[expect(dead_code)]
pub const ICON_DRAG_INDICATOR: &str = "\u{e945}"; pub const ICON_DRAG_INDICATOR: &str = "\u{e945}";
#[expect(dead_code)]
pub const ICON_INFO: &str = "\u{e88e}"; pub const ICON_INFO: &str = "\u{e88e}";
#[expect(dead_code)] #[expect(dead_code)]
pub const ICON_OPEN_IN_NEW: &str = "\u{e89e}"; pub const ICON_OPEN_IN_NEW: &str = "\u{e89e}";
@ -361,7 +358,6 @@ impl EggState {
} }
impl State { impl State {
#[expect(dead_code)]
pub fn get_egg_context(self: &Rc<Self>) -> Result<Rc<EggContext>, EggError> { pub fn get_egg_context(self: &Rc<Self>) -> Result<Rc<EggContext>, EggError> {
if let Some(ctx) = self.egg_state.ctx.get() { if let Some(ctx) = self.egg_state.ctx.get() {
return Ok(ctx); return Ok(ctx);
@ -467,7 +463,6 @@ impl State {
} }
impl EggContext { impl EggContext {
#[expect(dead_code)]
pub fn create_window(self: &Rc<Self>, title: &str) -> Rc<EggWindow> { pub fn create_window(self: &Rc<Self>, title: &str) -> Rc<EggWindow> {
let i = &self.inner; let i = &self.inner;
let wl_surface = i.wl_compositor.create_surface(); let wl_surface = i.wl_compositor.create_surface();
@ -628,12 +623,10 @@ impl EggSeatInner {
} }
impl EggWindow { impl EggWindow {
#[expect(dead_code)]
pub fn request_redraw(&self) { pub fn request_redraw(&self) {
self.inner.want_frame(); self.inner.want_frame();
} }
#[expect(dead_code)]
pub fn set_owner(&self, owner: Option<Rc<dyn EggWindowOwner>>) { pub fn set_owner(&self, owner: Option<Rc<dyn EggWindowOwner>>) {
self.inner.owner.set(owner); self.inner.owner.set(owner);
} }

View file

@ -21,6 +21,7 @@ pub mod jay_ei_session_builder;
pub mod jay_idle; pub mod jay_idle;
pub mod jay_input; pub mod jay_input;
pub mod jay_log_file; pub mod jay_log_file;
pub mod jay_open_control_center_request;
pub mod jay_output; pub mod jay_output;
pub mod jay_pointer; pub mod jay_pointer;
pub mod jay_popup_ext_manager_v1; pub mod jay_popup_ext_manager_v1;

View file

@ -11,6 +11,7 @@ use {
jay_idle::JayIdle, jay_idle::JayIdle,
jay_input::JayInput, jay_input::JayInput,
jay_log_file::JayLogFile, jay_log_file::JayLogFile,
jay_open_control_center_request::JayOpenControlCenterRequest,
jay_output::JayOutput, jay_output::JayOutput,
jay_pointer::JayPointer, jay_pointer::JayPointer,
jay_randr::JayRandr, jay_randr::JayRandr,
@ -77,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal { impl Global for JayCompositorGlobal {
fn version(&self) -> u32 { fn version(&self) -> u32 {
27 28
} }
fn required_caps(&self) -> ClientCaps { fn required_caps(&self) -> ClientCaps {
@ -541,6 +542,25 @@ impl JayCompositorRequestHandler for JayCompositor {
}); });
Ok(()) Ok(())
} }
fn open_control_center(
&self,
req: OpenControlCenter,
_slf: &Rc<Self>,
) -> 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! { object_base! {

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
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<Self>) -> 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<ClientError>),
}
efrom!(JayOpenControlCenterRequestError, ClientError);

View file

@ -58,6 +58,7 @@ mod clientmem;
mod cmm; mod cmm;
mod compositor; mod compositor;
mod config; mod config;
mod control_center;
mod copy_device; mod copy_device;
mod cpu_worker; mod cpu_worker;
mod criteria; mod criteria;

View file

@ -16,6 +16,7 @@ use {
cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager}, cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager},
compositor::{LIBEI_SOCKET, LogLevel}, compositor::{LIBEI_SOCKET, LogLevel},
config::ConfigProxy, config::ConfigProxy,
control_center::ControlCenters,
copy_device::CopyDeviceRegistry, copy_device::CopyDeviceRegistry,
cpu_worker::CpuWorker, cpu_worker::CpuWorker,
criteria::{clm::ClMatcherManager, tlm::TlMatcherManager}, criteria::{clm::ClMatcherManager, tlm::TlMatcherManager},
@ -297,6 +298,7 @@ pub struct State {
pub lazy_event_sources: Rc<LazyEventSources>, pub lazy_event_sources: Rc<LazyEventSources>,
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>, pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
pub egg_state: EggState, pub egg_state: EggState,
pub control_centers: ControlCenters,
} }
// impl Drop for State { // impl Drop for State {
@ -1162,6 +1164,7 @@ impl State {
self.lazy_event_sources.clear(); self.lazy_event_sources.clear();
self.bo_drop_queue.kill(); self.bo_drop_queue.kill();
self.egg_state.clear(); self.egg_state.clear();
self.control_centers.clear();
} }
pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) {

View file

@ -334,7 +334,7 @@ impl ToolClient {
self_id: s.registry, self_id: s.registry,
name: s.jay_compositor.0, name: s.jay_compositor.0,
interface: JayCompositor.name(), interface: JayCompositor.name(),
version: s.jay_compositor.1.min(27), version: s.jay_compositor.1.min(28),
id: id.into(), id: id.into(),
}); });
self.jay_compositor.set(Some(id)); self.jay_compositor.set(Some(id));

View file

@ -100,6 +100,14 @@ impl<T> NumCell<T> {
{ {
!self.is_zero() !self.is_zero()
} }
#[inline(always)]
pub fn take(&self) -> T
where
T: Default,
{
self.t.replace(T::default())
}
} }
impl<T: BitOr<Output = T> + Copy> BitOr<T> for &'_ NumCell<T> { impl<T: BitOr<Output = T> + Copy> BitOr<T> for &'_ NumCell<T> {

View file

@ -90,6 +90,7 @@ pub enum SimpleCommand {
ToggleSimpleImEnabled, ToggleSimpleImEnabled,
ReloadSimpleIm, ReloadSimpleIm,
EnableUnicodeInput, EnableUnicodeInput,
OpenControlCenter,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -167,6 +167,7 @@ impl ActionParser<'_> {
"toggle-simple-im-enabled" => ToggleSimpleImEnabled, "toggle-simple-im-enabled" => ToggleSimpleImEnabled,
"reload-simple-im" => ReloadSimpleIm, "reload-simple-im" => ReloadSimpleIm,
"enable-unicode-input" => EnableUnicodeInput, "enable-unicode-input" => EnableUnicodeInput,
"open-control-center" => OpenControlCenter,
_ => { _ => {
return Err( return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -31,6 +31,7 @@ alt-m = "toggle-mono"
alt-u = "toggle-fullscreen" alt-u = "toggle-fullscreen"
alt-f = "focus-parent" alt-f = "focus-parent"
alt-c = "open-control-center"
alt-shift-c = "close" alt-shift-c = "close"
alt-shift-f = "toggle-floating" alt-shift-f = "toggle-floating"
Super_L = { type = "exec", exec = "alacritty" } Super_L = { type = "exec", exec = "alacritty" }

View file

@ -36,10 +36,11 @@ use {
is_reload, is_reload,
keyboard::Keymap, keyboard::Keymap,
logging::set_log_level, logging::set_log_level,
on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled, on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload,
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled,
set_idle, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, set_float_above_fullscreen, set_idle, set_idle_grace_period,
set_show_float_pin_icon, set_show_titles, set_ui_drag_enabled, set_ui_drag_threshold, 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}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
tasks::{self, JoinHandle}, tasks::{self, JoinHandle},
@ -245,6 +246,7 @@ impl Action {
let persistent = state.persistent.clone(); let persistent = state.persistent.clone();
b.new(move || persistent.seat.enable_unicode_input()) b.new(move || persistent.seat.enable_unicode_input())
} }
SimpleCommand::OpenControlCenter => b.new(open_control_center),
}, },
Action::Multi { actions } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();

View file

@ -1928,7 +1928,8 @@
"disable-simple-im", "disable-simple-im",
"toggle-simple-im-enabled", "toggle-simple-im-enabled",
"reload-simple-im", "reload-simple-im",
"enable-unicode-input" "enable-unicode-input",
"open-control-center"
] ]
}, },
"SimpleIm": { "SimpleIm": {

View file

@ -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. This has no effect if the simple IM is not currently active.
- `open-control-center`:
Opens the control center.
<a name="types-SimpleIm"></a> <a name="types-SimpleIm"></a>

View file

@ -1089,6 +1089,8 @@ SimpleActionName:
Enables Unicode input in the simple, XCompose based input method. Enables Unicode input in the simple, XCompose based input method.
This has no effect if the simple IM is not currently active. This has no effect if the simple IM is not currently active.
- value: open-control-center
description: Opens the control center.
Color: Color:

View file

@ -135,6 +135,10 @@ request get_pid (since = 27) {
} }
request open_control_center (since = 28) {
id: id(jay_open_control_center_request),
}
# events # events
event client_id { event client_id {

View file

@ -0,0 +1,7 @@
request destroy {
}
event failed {
msg: str,
}