diff --git a/book/src/configuration/misc.md b/book/src/configuration/misc.md index 167d96962..dcf0f1491 100644 --- a/book/src/configuration/misc.md +++ b/book/src/configuration/misc.md @@ -186,6 +186,20 @@ The value should be a keysym name (see the [xkbcommon keysym list](https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h) with the `XKB_KEY_` prefix removed). +## Flatten Tree + +When a window is removed from a container (e.g. moved to another workspace or +floated), the container may end up with only one child. By default the +container is kept, resulting in redundant nesting. + +Setting `flatten-tree` to `true` automatically collapses +such containers by replacing them with their only remaining child, keeping the +window tree flat. + +```toml +flatten-tree = true +``` + ## Middle-Click Paste Controls whether middle-clicking pastes the primary selection. Changing this diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index aafe041c4..319323483 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1101,6 +1101,10 @@ impl ConfigClient { self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); } + pub fn set_flatten_tree(&self, enabled: bool) { + self.send(&ClientMessage::SetFlattenTree { enabled }); + } + pub fn open_control_center(&self) { self.send(&ClientMessage::OpenControlCenter); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 89fdf41b3..5f25b695b 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -756,6 +756,9 @@ pub enum ClientMessage<'a> { SetMiddleClickPasteEnabled { enabled: bool, }, + SetFlattenTree { + enabled: bool, + }, SeatCreateMark { seat: Seat, kc: Option, diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 21a66c5dd..0feea8d70 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -520,6 +520,16 @@ pub fn set_middle_click_paste_enabled(enabled: bool) { get!().set_middle_click_paste_enabled(enabled); } +/// Enables or disables tree flattening. +/// +/// When enabled, a container with only one child is automatically collapsed +/// by replacing it with its child, keeping the tree tidy. +/// +/// The default is `false`. +pub fn set_flatten_tree(enabled: bool) { + get!().set_flatten_tree(enabled); +} + /// Opens the control center. pub fn open_control_center() { get!().open_control_center(); diff --git a/src/compositor.rs b/src/compositor.rs index e446a6157..fde7f1448 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -375,6 +375,7 @@ fn start_compositor2( color_management_enabled: Cell::new(false), color_manager, float_above_fullscreen: Cell::new(false), + flatten_tree: Cell::new(false), icons: Default::default(), show_pin_icon: Cell::new(false), cl_matcher_manager: ClMatcherManager::new(&crit_ids), diff --git a/src/config/handler.rs b/src/config/handler.rs index 74c21da7a..10e02d435 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2595,6 +2595,10 @@ impl ConfigProxyHandler { self.state.set_primary_selection_enabled(enabled); } + fn handle_set_flatten_tree(&self, enabled: bool) { + self.state.set_flatten_tree(enabled); + } + fn handle_seat_create_mark(&self, seat: Seat, kc: Option) -> Result<(), CphError> { let seat = self.get_seat(seat)?; if let Some(kc) = kc { @@ -3625,6 +3629,7 @@ impl ConfigProxyHandler { ClientMessage::SetMiddleClickPasteEnabled { enabled } => { self.handle_set_middle_click_paste_enabled(enabled) } + ClientMessage::SetFlattenTree { enabled } => self.handle_set_flatten_tree(enabled), ClientMessage::SetWorkspaceDisplayOrder { order } => { self.handle_set_workspace_display_order(order) } diff --git a/src/state.rs b/src/state.rs index f2dba95a5..732e7597b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -284,6 +284,7 @@ pub struct State { pub color_management_enabled: Cell, pub color_manager: Rc, pub float_above_fullscreen: Cell, + pub flatten_tree: Cell, pub icons: Icons, pub show_pin_icon: Cell, pub cl_matcher_manager: ClMatcherManager, @@ -2096,6 +2097,10 @@ impl State { self.root.update_visible(self); } + pub fn set_flatten_tree(&self, v: bool) { + self.flatten_tree.set(v); + } + pub fn reset_sizes(&self) { self.theme.sizes.reset(); self.spaces_changed(); diff --git a/src/tree/container.rs b/src/tree/container.rs index ec62cec16..6312bb831 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -300,6 +300,10 @@ impl ContainerNode { slf } + pub fn num_children(&self) -> usize { + self.num_children.get() + } + pub fn prepend_child(self: &Rc, new: Rc) { if let Some(child) = self.children.first() { self.add_child_before_(&child, new); @@ -2114,6 +2118,24 @@ impl ContainingNode for ContainerNode { self.tl_destroy(); return; } + if num_children == 1 && self.state.flatten_tree.get() { + if let Some(parent) = self.toplevel_data.parent.get() + && !self.toplevel_data.is_fullscreen.get() + { + let remaining = { + let cn = self.child_nodes.borrow(); + let (_, v) = cn.iter().next().unwrap(); + v.node.clone() + }; + if parent.cnode_accepts_child(&*remaining) { + parent.cnode_replace_child(self.deref(), remaining); + self.toplevel_data.parent.take(); + self.child_nodes.borrow_mut().clear(); + self.tl_destroy(); + return; + } + } + } self.update_content_size(); let rem = 1.0 - node.factor.get(); let mut sum = 0.0; diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 1d6aa907f..e9c8c0d67 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1067,6 +1067,13 @@ pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: Some(pn) => pn, _ => return, }; + if let Some(container) = pn.clone().node_into_container() + && container.num_children() == 1 + && state.flatten_tree.get() + { + container.set_split(axis); + return; + } if let Some(pn) = pn.node_into_containing_node() { let cn = ContainerNode::new(state, &ws, tl.clone(), axis); pn.cnode_replace_child(&*tl, cn); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index f74ba7fd0..21f6c2359 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -573,6 +573,7 @@ pub struct Config { pub use_hardware_cursor: Option, pub show_bar: Option, pub show_titles: Option, + pub flatten_tree: Option, pub focus_history: Option, pub middle_click_paste: Option, pub input_modes: AHashMap, diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index af6b6ba47..e4683610e 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -158,7 +158,7 @@ impl Parser for ConfigParser<'_> { clean_logs_older_than_val, mouse_follows_focus, ), - (session_management_val, workspaces_val), + (session_management_val, workspaces_val, flatten_tree), ) = ext.extract(( ( opt(val("keymap")), @@ -220,7 +220,11 @@ impl Parser for ConfigParser<'_> { opt(val("clean-logs-older-than")), recover(opt(bol("unstable-mouse-follows-focus"))), ), - (opt(val("session-management")), opt(val("workspaces"))), + ( + opt(val("session-management")), + opt(val("workspaces")), + recover(opt(bol("flatten-tree"))), + ), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -633,6 +637,7 @@ impl Parser for ConfigParser<'_> { use_hardware_cursor: use_hardware_cursor.despan(), show_bar: show_bar.despan(), show_titles: show_titles.despan(), + flatten_tree: flatten_tree.despan(), focus_history, middle_click_paste: middle_click_paste.despan(), input_modes, diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index b93d15ed4..ef086725e 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -38,7 +38,7 @@ use { logging::{clean_logs_older_than, set_log_level}, 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_flatten_tree, set_float_above_fullscreen, set_idle, set_idle_grace_period, set_middle_click_paste_enabled, set_session_management_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}, @@ -1677,6 +1677,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc