diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index fbed99a5..24642b4c 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -36,6 +36,7 @@ use { ContentType, MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType, }, + workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, }, bincode::Options, @@ -987,6 +988,10 @@ impl ConfigClient { self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); } + pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { + self.send(&ClientMessage::SetWorkspaceDisplayOrder { order }); + } + pub fn seat_create_mark(&self, seat: Seat, kc: Option) { self.send(&ClientMessage::SeatCreateMark { seat, kc }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index bde2aacb..bb2d56d3 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -16,6 +16,7 @@ use { Transform, VrrMode, connector_type::ConnectorType, }, window::{ContentType, TileState, Window, WindowMatcher, WindowType}, + workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -760,6 +761,9 @@ pub enum ClientMessage<'a> { src: u32, dst: u32, }, + SetWorkspaceDisplayOrder { + order: WorkspaceDisplayOrder, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index e7e9a586..854e9455 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -45,6 +45,7 @@ #[expect(unused_imports)] use crate::input::Seat; + use { crate::{ _private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector, window::Window, @@ -73,6 +74,7 @@ pub mod theme; pub mod timer; pub mod video; pub mod window; +pub mod workspace; pub mod xwayland; /// A planar direction. diff --git a/jay-config/src/workspace.rs b/jay-config/src/workspace.rs new file mode 100644 index 00000000..5a63fc98 --- /dev/null +++ b/jay-config/src/workspace.rs @@ -0,0 +1,19 @@ +//! Tools for configuring workspaces. + +use serde::{Deserialize, Serialize}; + +/// How workspaces should be ordered in the UI. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum WorkspaceDisplayOrder { + /// Workspaces are not sorted and can be manually dragged. + Manual, + /// Workspaces are sorted alphabetically and cannot be manually dragged. + Sorted, +} + +/// Sets how workspaces should be ordered in the UI. +/// +/// The default is `WorkspaceDisplayOrder::Manual`. +pub fn set_workspace_display_order(order: WorkspaceDisplayOrder) { + get!().set_workspace_display_order(order); +} diff --git a/src/compositor.rs b/src/compositor.rs index a337c9c2..39df57bd 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -83,6 +83,7 @@ use { jay_config::{ _private::DEFAULT_SEAT_NAME, video::{GfxApi, Transform}, + workspace::WorkspaceDisplayOrder, }, std::{cell::Cell, env, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}, thiserror::Error, @@ -357,6 +358,7 @@ fn start_compositor2( show_bar: Cell::new(true), enable_primary_selection: Cell::new(true), xdg_surface_configure_events: Default::default(), + workspace_display_order: Cell::new(WorkspaceDisplayOrder::Manual), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 2a6c0511..ba7e624e 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -74,6 +74,7 @@ use { Transform, VrrMode as ConfigVrrMode, }, window::{TileState, Window, WindowMatcher}, + workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, }, kbvm::Keycode, @@ -1353,6 +1354,13 @@ impl ConfigProxyHandler { } } + fn handle_set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { + self.state.workspace_display_order.set(order); + for output in self.state.root.outputs.lock().values() { + output.handle_workspace_display_order_update(); + } + } + fn handle_get_seat_float_pinned(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetFloatPinned { @@ -3098,6 +3106,9 @@ impl ConfigProxyHandler { ClientMessage::SetMiddleClickPasteEnabled { enabled } => { self.handle_set_middle_click_paste_enabled(enabled) } + ClientMessage::SetWorkspaceDisplayOrder { order } => { + self.handle_set_workspace_display_order(order) + } ClientMessage::SeatCreateMark { seat, kc } => self .handle_seat_create_mark(seat, kc) .wrn("seat_create_mark")?, diff --git a/src/state.rs b/src/state.rs index a05fd28d..e1e37dc7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -128,6 +128,7 @@ use { PciId, video::{GfxApi, Transform}, window::TileState, + workspace::WorkspaceDisplayOrder, }, std::{ cell::{Cell, RefCell}, @@ -275,6 +276,7 @@ pub struct State { pub show_bar: Cell, pub enable_primary_selection: Cell, pub xdg_surface_configure_events: AsyncQueue, + pub workspace_display_order: Cell, } // impl Drop for State { diff --git a/src/tree/output.rs b/src/tree/output.rs index 4f1dee00..c7842d64 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -48,17 +48,27 @@ use { WorkspaceDragDestination, WorkspaceNode, WorkspaceNodeId, walker::NodeVisitor, }, utils::{ - asyncevent::AsyncEvent, bitflags::BitflagsExt, clonecell::CloneCell, - copyhashmap::CopyHashMap, errorfmt::ErrorFmt, event_listener::EventSource, - hash_map_ext::HashMapExt, linkedlist::LinkedList, on_drop_event::OnDropEvent, - scroller::Scroller, transform_ext::TransformExt, + asyncevent::AsyncEvent, + bitflags::BitflagsExt, + clonecell::CloneCell, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + event_listener::EventSource, + hash_map_ext::HashMapExt, + linkedlist::{LinkedList, NodeRef}, + on_drop_event::OnDropEvent, + scroller::Scroller, + transform_ext::TransformExt, }, wire::{ ExtImageCopyCaptureSessionV1Id, JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id, }, }, ahash::AHashMap, - jay_config::video::{TearingMode as ConfigTearingMode, Transform, VrrMode as ConfigVrrMode}, + jay_config::{ + video::{TearingMode as ConfigTearingMode, Transform, VrrMode as ConfigVrrMode}, + workspace::WorkspaceDisplayOrder, + }, smallvec::SmallVec, std::{ cell::{Cell, RefCell}, @@ -712,6 +722,17 @@ impl OutputNode { true } + pub fn find_workspace_insertion_point(&self, name: &str) -> Option>> { + if self.state.workspace_display_order.get() == WorkspaceDisplayOrder::Sorted { + for existing_ws in self.workspaces.iter() { + if name < existing_ws.name.as_str() { + return Some(existing_ws); + } + } + } + None + } + pub fn create_workspace(self: &Rc, name: &str) -> Rc { let ws = Rc::new(WorkspaceNode { id: self.state.node_ids.next(), @@ -740,7 +761,12 @@ impl OutputNode { }); ws.opt.set(Some(ws.clone())); ws.update_has_captures(); - *ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone())); + let link = if let Some(before) = self.find_workspace_insertion_point(name) { + before.prepend(ws.clone()) + } else { + self.workspaces.add_last(ws.clone()) + }; + *ws.output_link.borrow_mut() = Some(link); self.state.workspaces.set(name.to_string(), ws.clone()); if self.workspace.is_none() { self.show_workspace(&ws); @@ -1048,6 +1074,18 @@ impl OutputNode { } } + pub fn handle_workspace_display_order_update(self: &Rc) { + if self.state.workspace_display_order.get() == WorkspaceDisplayOrder::Sorted { + let mut workspaces: Vec<_> = self.workspaces.iter().collect(); + workspaces.sort_by(|a, b| a.name.cmp(&b.name)); + for ws_ref in workspaces { + ws_ref.detach(); + self.workspaces.add_last_existing(&ws_ref); + } + } + self.schedule_update_render_data(); + } + pub fn update_visible(&self) { let mut visible = self.state.root_visible(); if self.state.lock.locked.get() { @@ -1285,6 +1323,16 @@ impl OutputNode { if y_abs - rect.y1() > th + 1 { return None; } + if self.state.workspace_display_order.get() == WorkspaceDisplayOrder::Sorted { + if self.workspaces.iter().any(|ws| ws.id == source) { + return None; + } + return Some(WorkspaceDragDestination { + highlight: Rect::new_sized(rect.x1(), rect.y1(), rect.width(), th)?, + output: self.clone(), + before: None, + }); + } let rd = &*self.render_data.borrow(); let (x, _) = rect.translate(x_abs, y_abs); let mut prev_is_source = false; diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index bcc99517..df6180db 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -35,6 +35,7 @@ use { }, wire::JayWorkspaceId, }, + jay_config::workspace::WorkspaceDisplayOrder, std::{ cell::{Cell, RefCell}, fmt::Debug, @@ -491,8 +492,15 @@ pub fn move_ws_to_output( } } ws.set_output(&target); + let before = if target.state.workspace_display_order.get() == WorkspaceDisplayOrder::Sorted { + target + .find_workspace_insertion_point(&ws.name) + .map(|nr| nr.deref().clone()) + } else { + config.before + }; 'link: { - if let Some(before) = config.before + if let Some(before) = before && let Some(link) = &*before.output_link.borrow() { link.prepend_existing(ws); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 4d08b3c2..bbdc25e0 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -35,6 +35,7 @@ use { theme::Color, video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, window::{ContentType, TileState, WindowType}, + workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, }, std::{ @@ -509,6 +510,7 @@ pub struct Config { pub focus_history: Option, pub middle_click_paste: Option, pub input_modes: AHashMap, + pub workspace_display_order: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index a56a8bea..466e4652 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -47,6 +47,7 @@ mod vrr; mod window_match; mod window_rule; mod window_type; +mod workspace_display_order; mod xwayland; #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index f748838c..125730b4 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -36,6 +36,7 @@ use { ui_drag::UiDragParser, vrr::VrrParser, window_rule::WindowRulesParser, + workspace_display_order::WorkspaceDisplayOrderParser, xwayland::XwaylandParser, }, spanned::SpannedErrorExt, @@ -138,7 +139,7 @@ impl Parser for ConfigParser<'_> { show_bar, focus_history_val, ), - (middle_click_paste, input_modes_val), + (middle_click_paste, input_modes_val, workspace_display_order_val), ) = ext.extract(( ( opt(val("keymap")), @@ -188,7 +189,11 @@ impl Parser for ConfigParser<'_> { recover(opt(bol("show-bar"))), opt(val("focus-history")), ), - (recover(opt(bol("middle-click-paste"))), opt(val("modes"))), + ( + recover(opt(bol("middle-click-paste"))), + opt(val("modes")), + opt(val("workspace-display-order")), + ), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -486,6 +491,18 @@ impl Parser for ConfigParser<'_> { } } } + let mut workspace_display_order = None; + if let Some(value) = workspace_display_order_val { + match value.parse(&mut WorkspaceDisplayOrderParser) { + Ok(v) => workspace_display_order = Some(v), + Err(e) => { + log::warn!( + "Could not parse the workspace display order: {}", + self.0.error(e) + ); + } + } + } Ok(Config { keymap, repeat_rate, @@ -528,6 +545,7 @@ impl Parser for ConfigParser<'_> { focus_history, middle_click_paste: middle_click_paste.despan(), input_modes, + workspace_display_order, }) } } diff --git a/toml-config/src/config/parsers/workspace_display_order.rs b/toml-config/src/config/parsers/workspace_display_order.rs new file mode 100644 index 00000000..749cff2d --- /dev/null +++ b/toml-config/src/config/parsers/workspace_display_order.rs @@ -0,0 +1,32 @@ +use { + crate::{ + config::parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + toml::toml_span::{Span, SpannedExt}, + }, + jay_config::workspace::WorkspaceDisplayOrder, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum WorkspaceDisplayOrderParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown workspace display order {0}")] + Unknown(String), +} + +pub struct WorkspaceDisplayOrderParser; + +impl Parser for WorkspaceDisplayOrderParser { + type Value = WorkspaceDisplayOrder; + type Error = WorkspaceDisplayOrderParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + match string { + "manual" => Ok(WorkspaceDisplayOrder::Manual), + "sorted" => Ok(WorkspaceDisplayOrder::Sorted), + _ => Err(WorkspaceDisplayOrderParserError::Unknown(string.to_string()).spanned(span)), + } + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index d9395777..8c4b6413 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -50,6 +50,7 @@ use { set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, }, window::Window, + workspace::set_workspace_display_order, xwayland::set_x_scaling_mode, }, run_on_drop::on_drop, @@ -1306,6 +1307,9 @@ fn load_config(initial_load: bool, persistent: &Rc) { if let Some(v) = config.middle_click_paste { set_middle_click_paste_enabled(v); } + if let Some(v) = config.workspace_display_order { + set_workspace_display_order(v); + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 215ce1c9..10f6704a 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -991,6 +991,10 @@ "description": "", "$ref": "#/$defs/InputMode" } + }, + "workspace-display-order": { + "description": "Configures the order of workspaces displayed.\n\nThe default is `manual`.\n\n- Example:\n\n ```toml\n workspace-display-order = \"sorted\"\n ```\n", + "$ref": "#/$defs/WorkspaceDisplayOrder" } }, "required": [] @@ -2179,6 +2183,14 @@ } ] }, + "WorkspaceDisplayOrder": { + "type": "string", + "description": "The order of workspaces displayed.\n", + "enum": [ + "manual", + "sorted" + ] + }, "XScalingMode": { "type": "string", "description": "The scaling mode of X windows.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index e16b71a0..92b162bf 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1976,6 +1976,20 @@ The table has the following fields: The value of this field should be a table whose values are [InputModes](#types-InputMode). +- `workspace-display-order` (optional): + + Configures the order of workspaces displayed. + + The default is `manual`. + + - Example: + + ```toml + workspace-display-order = "sorted" + ``` + + The value of this field should be a [WorkspaceDisplayOrder](#types-WorkspaceDisplayOrder). + ### `Connector` @@ -4761,6 +4775,25 @@ An array of masks that are OR'd. Each element of this array should be a [WindowTypeMask](#types-WindowTypeMask). + +### `WorkspaceDisplayOrder` + +The order of workspaces displayed. + +Values of this type should be strings. + +The string should have one of the following values: + +- `manual`: + + Workspaces are not sorted and can be manually dragged. + +- `sorted`: + + Workspaces are sorted alphabetically and cannot be manually dragged. + + + ### `XScalingMode` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index d8bba591..558badb9 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2122,7 +2122,6 @@ Theme: description: The name of the font to use. - Config: kind: table description: | @@ -2802,8 +2801,21 @@ Config: q = "focus-prev" e = "focus-next" ``` - + Modes can be activated with the `push-mode` and `latch-mode` actions. + workspace-display-order: + ref: WorkspaceDisplayOrder + required: false + description: | + Configures the order of workspaces displayed. + + The default is `manual`. + + - Example: + + ```toml + workspace-display-order = "sorted" + ``` Idle: @@ -4006,3 +4018,14 @@ InputMode: The complex shortcuts of this mode. See the same field in the top-level `Config` object for a description. + + +WorkspaceDisplayOrder: + kind: string + description: | + The order of workspaces displayed. + values: + - value: manual + description: Workspaces are not sorted and can be manually dragged. + - value: sorted + description: Workspaces are sorted alphabetically and cannot be manually dragged.