diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index e5e7388..59ba0eb 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -83,6 +83,19 @@ pub struct CosmicCompConfig { /// If set to Global, autotile applies to all windows in all workspaces /// If set to PerWorkspace, autotile only applies to new windows, and new workspaces pub autotile_behavior: TileBehavior, + /// Keep a group's split direction when a new window opens into it + /// + /// Normally a new window *splits the focused window*: it is wrapped in a + /// fresh group whose direction is taken from that window's aspect ratio. + /// Two windows side by side are each taller than wide, so a third one + /// splits a column in half and you get quadrants -- three panes in a row + /// is not reachable by opening windows at all. + /// + /// With this set the new window joins the group the focused window is + /// already in, keeping that group's direction, so three windows land as + /// three panes in a row. Named after Hyprland's `dwindle:preserve_split`, + /// which does the same thing. + pub preserve_split: bool, /// Active hint enabled pub active_hint: bool, /// Enables changing keyboard focus to windows when the cursor passes into them @@ -129,6 +142,8 @@ impl Default for CosmicCompConfig { keyboard_config: Default::default(), autotile: Default::default(), autotile_behavior: Default::default(), + // Off, so the fork's default layout is still COSMIC's. + preserve_split: false, active_hint: true, focus_follows_cursor: false, cursor_follows_focus: false, diff --git a/src/config/mod.rs b/src/config/mod.rs index 1d9b9c9..86344b1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -883,6 +883,16 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut ); } } + "preserve_split" => { + let new = get_config::(&config, "preserve_split"); + if new != state.common.config.cosmic_conf.preserve_split { + state.common.config.cosmic_conf.preserve_split = new; + // Only affects where the *next* window is inserted, so + // there is nothing to retile -- `update_config` pushing the + // flag down to every `TilingLayout` is the whole job. + state.common.update_config(); + } + } "active_hint" => { let new = get_config::(&config, "active_hint"); if new != state.common.config.cosmic_conf.active_hint { diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 2d95cd2..0c17051 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -138,6 +138,8 @@ pub struct TilingLayout { last_overview_hover: Option<(Option, TargetZone)>, pub theme: cosmic::Theme, pub appearance: AppearanceConfig, + /// See `map_to_tree`, which is the only thing that reads it. + pub preserve_split: bool, } #[derive(Debug, Clone, PartialEq)] @@ -352,6 +354,7 @@ impl TilingLayout { pub fn new( theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, output: &Output, ) -> TilingLayout { TilingLayout { @@ -369,6 +372,7 @@ impl TilingLayout { last_overview_hover: None, theme, appearance, + preserve_split, } } @@ -430,6 +434,7 @@ impl TilingLayout { last_active, direction, minimize_rect, + self.preserve_split, ); let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); self.queue.push_tree(tree, duration, blocker); @@ -552,6 +557,7 @@ impl TilingLayout { node: Option, direction: Option, minimize_rect: Option>, + preserve_split: bool, ) { let window = window.into(); let new_window = Node::new(Data::Mapped { @@ -582,17 +588,61 @@ impl TilingLayout { tree.insert(new_window, InsertBehavior::AsRoot).unwrap() } } else if let Some(ref node_id) = node { - let orientation = { - let window_size = tree.get(node_id).unwrap().data().geometry().size; - if window_size.w > window_size.h { - Orientation::Vertical - } else { - Orientation::Horizontal - } - }; - let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); - TilingLayout::new_group(tree, node_id, &new_id, orientation).unwrap(); - new_id + // Join the focused window's group, or split the focused window. + // + // Splitting is the normal behaviour and the reason a group with + // three children is unreachable by opening windows: the new window + // is wrapped together with the focused one in a *fresh* group whose + // orientation comes from the focused window's aspect ratio. Two + // windows side by side are each taller than wide, so the third + // splits a column horizontally and you get quadrants, forever. + // + // Preserving the split appends into the group the focused window is + // already in and keeps that group's orientation, which the sizes + // vector has always been able to represent -- `add_window` divides + // by `len + 1`, not by two. Three windows then land as three panes + // in a row. + // + // The parent of a mapped node is always a group; `is_group` is + // checked anyway because getting it wrong would panic in + // `add_window` rather than misplace a window. + let join = preserve_split + .then(|| tree.get(node_id).unwrap().parent().cloned()) + .flatten() + .filter(|parent_id| tree.get(parent_id).unwrap().data().is_group()); + + if let Some(parent_id) = join { + let idx = tree + .children_ids(&parent_id) + .unwrap() + .position(|id| id == node_id) + .unwrap(); + let new_id = tree + .insert(new_window, InsertBehavior::UnderNode(&parent_id)) + .unwrap(); + // Directly after the focused window, and the size goes in at + // the same index -- children and `sizes` are positional, so the + // two must agree or every window in the group is drawn one + // slot over. + tree.make_nth_sibling(&new_id, idx + 1).unwrap(); + tree.get_mut(&parent_id) + .unwrap() + .data_mut() + .add_window(idx + 1); + new_id + } else { + let orientation = { + let window_size = tree.get(node_id).unwrap().data().geometry().size; + if window_size.w > window_size.h { + Orientation::Vertical + } else { + Orientation::Horizontal + } + }; + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, node_id, &new_id, orientation).unwrap(); + new_id + } } else { // nothing? then we add to the root if let Some(root_id) = tree.root_node_id().cloned() { @@ -2204,6 +2254,7 @@ impl TilingLayout { Some(current_node), None, None, + self.preserve_split, ); let node = window.tiling_node_id.lock().unwrap().clone().unwrap(); @@ -2794,6 +2845,7 @@ impl TilingLayout { None, None, None, + self.preserve_split, ); window } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 39d692e..64c7681 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -366,6 +366,7 @@ pub struct WorkspaceSet { output: Output, theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, pub sticky_layer: FloatingLayout, pub minimized_windows: Vec, pub workspaces: Vec, @@ -379,6 +380,7 @@ fn create_workspace( tiling: bool, theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, ) -> Workspace { let workspace_handle = state .create_workspace( @@ -408,6 +410,7 @@ fn create_workspace( tiling, theme.clone(), appearance, + preserve_split, ) } @@ -419,6 +422,7 @@ fn create_workspace_from_pinned( active: bool, theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, ) -> Workspace { let workspace_handle = state .create_workspace( @@ -453,6 +457,7 @@ fn create_workspace_from_pinned( output.clone(), theme.clone(), appearance, + preserve_split, ) } @@ -489,6 +494,7 @@ impl WorkspaceSet { tiling_enabled: bool, theme: &cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, ) -> WorkspaceSet { let group_handle = state.create_workspace_group(); let sticky_layer = FloatingLayout::new(theme.clone(), appearance, output); @@ -504,6 +510,7 @@ impl WorkspaceSet { workspaces: Vec::new(), output: output.clone(), appearance, + preserve_split, } } @@ -622,6 +629,7 @@ impl WorkspaceSet { self.tiling_enabled, self.theme.clone(), self.appearance, + self.preserve_split, ); workspace_set_idx( state, @@ -827,6 +835,7 @@ pub struct Workspaces { mode: WorkspaceMode, autotile: bool, autotile_behavior: TileBehavior, + preserve_split: bool, theme: cosmic::Theme, appearance: AppearanceConfig, // Persisted workspace to add on first `output_add` @@ -842,6 +851,7 @@ impl Workspaces { mode: config.cosmic_conf.workspaces.workspace_mode, autotile: config.cosmic_conf.autotile, autotile_behavior: config.cosmic_conf.autotile_behavior, + preserve_split: config.cosmic_conf.preserve_split, theme, appearance: config.cosmic_conf.appearance_settings, persisted_workspaces: config.cosmic_conf.pinned_workspaces.clone(), @@ -871,6 +881,7 @@ impl Workspaces { self.autotile, &self.theme, self.appearance, + self.preserve_split, ) }); workspace_state.add_group_output(&set.group, output); @@ -885,6 +896,7 @@ impl Workspaces { false, self.theme.clone(), self.appearance, + self.preserve_split, ); set.workspaces.push(workspace); } @@ -1163,13 +1175,16 @@ impl Workspaces { self.mode = config.cosmic_conf.workspaces.workspace_mode; self.layout = config.cosmic_conf.workspaces.workspace_layout; self.appearance = config.cosmic_conf.appearance_settings; + self.preserve_split = config.cosmic_conf.preserve_split; for set in self.sets.values_mut() { set.appearance = self.appearance; set.sticky_layer.appearance = self.appearance; + set.preserve_split = self.preserve_split; for workspace in set.workspaces.iter_mut() { workspace.floating_layer.appearance = self.appearance; workspace.tiling_layer.appearance = self.appearance; + workspace.tiling_layer.preserve_split = self.preserve_split; } } @@ -1219,6 +1234,7 @@ impl Workspaces { config.cosmic_conf.autotile, self.theme.clone(), self.appearance, + self.preserve_split, ), ); } diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index 84a020f..f3b34ce 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -387,8 +387,9 @@ impl Workspace { tiling_enabled: bool, theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, ) -> Workspace { - let tiling_layer = TilingLayout::new(theme.clone(), appearance, &output); + let tiling_layer = TilingLayout::new(theme.clone(), appearance, preserve_split, &output); let floating_layer = FloatingLayout::new(theme, appearance, &output); let output_match = output_match_for_output(&output); @@ -421,8 +422,9 @@ impl Workspace { output: Output, theme: cosmic::Theme, appearance: AppearanceConfig, + preserve_split: bool, ) -> Self { - let tiling_layer = TilingLayout::new(theme.clone(), appearance, &output); + let tiling_layer = TilingLayout::new(theme.clone(), appearance, preserve_split, &output); let floating_layer = FloatingLayout::new(theme, appearance, &output); let output_match = output_match_for_output(&output);