Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions book/src/control-center.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ LIBEI_SOCKET
Workspace Display Order
: Dropdown to select how workspaces are ordered in the bar

Workspace Empty Behavior
: Dropdown to select what happens to empty workspaces when they are left or become inactive

Log Level
: Dropdown to change the active log level at runtime (shown when the logger is available)

Expand Down
44 changes: 44 additions & 0 deletions book/src/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,50 @@ workspace-display-order = "sorted"

You can also change this at runtime in the control center.

## Empty Workspace Behavior

Jay creates workspaces on demand. When a workspace becomes empty, Jay can
optionally hide or destroy it automatically so your workspace list does not
accumulate unused entries.

Configure this with the `workspace-empty-behavior` top-level setting (or at
runtime in the control center, in the Compositor pane):

```toml
workspace-empty-behavior = "hide-on-leave"
```

> [!NOTE]
> This behavior is evaluated per output.
>
> - "leave" means the workspace stops being the active workspace on its output
> because you showed another workspace on that same output.
> - "inactive" means the workspace is currently not the active workspace on its
> output.

Supported values:

`preserve`
: Never destroy or hide empty workspaces automatically.

`destroy-on-leave`
: Destroy an empty workspace when you leave it (default).

`hide-on-leave`
: Hide an empty workspace when you leave it.

`destroy`
: Destroy an empty workspace whenever it is empty and inactive.

`hide`
: Hide an empty workspace whenever it is empty and inactive.

> [!TIP]
> Hidden workspaces are not listed in the bar or workspace lists, but you can
> restore them by showing the workspace by name (for example via the
> `show-workspace` action). When restoring a hidden workspace, Jay prefers the
> output it was last shown on if that output is still connected.

## Hot-Plug and Hot-Unplug

Jay handles monitor connections gracefully:
Expand Down
6 changes: 5 additions & 1 deletion jay-config/src/_private/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use {
ContentType, MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher,
WindowType,
},
workspace::WorkspaceDisplayOrder,
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
xwayland::XScalingMode,
},
bincode::Options,
Expand Down Expand Up @@ -1078,6 +1078,10 @@ impl ConfigClient {
self.send(&ClientMessage::SetWorkspaceDisplayOrder { order });
}

pub fn set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) {
self.send(&ClientMessage::SetWorkspaceEmptyBehavior { behavior });
}

pub fn seat_create_mark(&self, seat: Seat, kc: Option<u32>) {
self.send(&ClientMessage::SeatCreateMark { seat, kc });
}
Expand Down
5 changes: 4 additions & 1 deletion jay-config/src/_private/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use {
Transform, VrrMode, connector_type::ConnectorType,
},
window::{ContentType, TileState, Window, WindowMatcher, WindowType},
workspace::WorkspaceDisplayOrder,
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
xwayland::XScalingMode,
},
serde::{Deserialize, Serialize},
Expand Down Expand Up @@ -875,6 +875,9 @@ pub enum ClientMessage<'a> {
seat: Seat,
enabled: bool,
},
SetWorkspaceEmptyBehavior {
behavior: WorkspaceEmptyBehavior,
},
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down
22 changes: 22 additions & 0 deletions jay-config/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,25 @@ pub enum WorkspaceDisplayOrder {
pub fn set_workspace_display_order(order: WorkspaceDisplayOrder) {
get!().set_workspace_display_order(order);
}

/// Configures what happens to empty workspaces when they are left or become inactive.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum WorkspaceEmptyBehavior {
/// Never destroy or hide empty workspaces automatically.
Preserve,
/// Destroy an empty workspace when switching away from it.
DestroyOnLeave,
/// Hide an empty workspace when switching away from it.
HideOnLeave,
/// Destroy an empty workspace whenever it is empty and inactive.
Destroy,
/// Hide an empty workspace whenever it is empty and inactive.
Hide,
}

/// Sets what should happen to empty workspaces.
///
/// The default is `WorkspaceEmptyBehavior::DestroyOnLeave`.
pub fn set_workspace_empty_behavior(behavior: WorkspaceEmptyBehavior) {
get!().set_workspace_empty_behavior(behavior);
}
7 changes: 4 additions & 3 deletions src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ use {
tracy::enable_profiler,
tree::{
DisplayNode, NodeIds, OutputNode, TearingMode, Transform, VrrMode,
WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions,
container_render_titles, float_layout, float_titles, output_render_data,
placeholder_render_textures,
WorkspaceDisplayOrder, WorkspaceEmptyBehavior, WorkspaceNode, container_layout,
container_render_positions, container_render_titles, float_layout, float_titles,
output_render_data, placeholder_render_textures,
},
user_session::import_environment,
utils::{
Expand Down Expand Up @@ -398,6 +398,7 @@ fn start_compositor2(
control_centers: Default::default(),
virtual_outputs: Default::default(),
clean_logs_older_than: Default::default(),
workspace_empty_behavior: Cell::new(WorkspaceEmptyBehavior::DestroyOnLeave),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
Expand Down
12 changes: 11 additions & 1 deletion src/config/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ use {
VrrMode as ConfigVrrMode,
},
window::{TileState as ConfigTileState, Window, WindowMatcher},
workspace::WorkspaceDisplayOrder,
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
xwayland::XScalingMode,
},
kbvm::Keycode,
Expand Down Expand Up @@ -579,6 +579,9 @@ impl ConfigProxyHandler {
fn handle_get_workspaces(&self) {
let mut workspaces = vec![];
for ws in self.state.workspaces.lock().values() {
if ws.hidden.get() {
continue;
}
workspaces.push(self.get_workspace_by_name(&ws.name));
}
self.respond(Response::GetWorkspaces { workspaces });
Expand Down Expand Up @@ -1443,6 +1446,10 @@ impl ConfigProxyHandler {
self.state.set_workspace_display_order(order.into());
}

fn handle_set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) {
self.state.set_workspace_empty_behavior(behavior.into());
}

fn handle_get_seat_float_pinned(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
self.respond(Response::GetFloatPinned {
Expand Down Expand Up @@ -3412,6 +3419,9 @@ impl ConfigProxyHandler {
} => self
.handle_window_resize(window, dx1, dy1, dx2, dy2)
.wrn("window_resize")?,
ClientMessage::SetWorkspaceEmptyBehavior { behavior } => {
self.handle_set_workspace_empty_behavior(behavior)
}
}
Ok(())
}
Expand Down
6 changes: 6 additions & 0 deletions src/control_center/cc_compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ impl CompositorPane {
s.workspace_display_order.get(),
|o| s.set_workspace_display_order(o),
);
combo_box(
ui,
"Workspace Empty Behavior",
s.workspace_empty_behavior.get(),
|b| s.set_workspace_empty_behavior(b),
);
if let Some(logger) = &s.logger {
combo_box(ui, "Log Level", logger.level(), |l| s.set_log_level(l));
row(ui, "Log File", |ui| {
Expand Down
4 changes: 3 additions & 1 deletion src/ifs/workspace_manager/ext_workspace_handle_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use {

const STATE_ACTIVE: u32 = 1;
const STATE_URGENT: u32 = 2;
#[expect(dead_code)]
const STATE_HIDDEN: u32 = 4;

const CAP_ACTIVATE: u32 = 1;
Expand Down Expand Up @@ -85,6 +84,9 @@ impl ExtWorkspaceHandleV1 {
if ws.attention_requests.active() {
state |= STATE_URGENT;
}
if ws.hidden.get() {
state |= STATE_HIDDEN;
}
self.send_state(state);
}

Expand Down
11 changes: 11 additions & 0 deletions src/ifs/workspace_manager/ext_workspace_manager_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ impl ExtWorkspaceManagerV1Global {
obj.announce_workspace(&dummy_output, &ws);
}
}
let hidden_workspaces: Vec<_> = client
.state
.workspaces
.lock()
.values()
.filter(|ws| ws.hidden.get() && ws.ext_workspaces.get(&obj.manager_id).is_none())
.cloned()
.collect();
for ws in hidden_workspaces {
obj.announce_workspace(&dummy_output, &ws);
}
for output in client.state.root.outputs.lock().values() {
obj.announce_output(output);
}
Expand Down
5 changes: 5 additions & 0 deletions src/it/test_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use {
sized::{BAR_SEPARATOR_WIDTH, Resizable},
},
video::{Connector, Transform},
workspace::WorkspaceEmptyBehavior,
},
std::{cell::Cell, ops::Deref, ptr, rc::Rc, time::Duration},
};
Expand Down Expand Up @@ -331,6 +332,10 @@ impl TestConfig {
pub fn set_show_titles(&self, show: bool) -> TestResult {
self.send(ClientMessage::SetShowTitles { show })
}

pub fn set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) -> TestResult {
self.send(ClientMessage::SetWorkspaceEmptyBehavior { behavior })
}
}

impl Drop for TestConfig {
Expand Down
2 changes: 2 additions & 0 deletions src/it/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ mod t0051_pointer_warp;
mod t0052_bar;
mod t0053_theme;
mod t0054_subsurface_already_attached;
mod t0055_workspace_empty_behavior;

pub trait TestCase: Sync {
fn name(&self) -> &'static str;
Expand Down Expand Up @@ -158,5 +159,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
t0052_bar,
t0053_theme,
t0054_subsurface_already_attached,
t0055_workspace_empty_behavior,
}
}
Loading
Loading