diff options
| -rw-r--r-- | niri-config/src/lib.rs | 18 | ||||
| -rw-r--r-- | niri-ipc/src/lib.rs | 2 | ||||
| -rw-r--r-- | resources/default-config.kdl | 5 | ||||
| -rw-r--r-- | src/input/mod.rs | 6 | ||||
| -rw-r--r-- | src/layout/mod.rs | 12 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 4 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 244 | ||||
| -rw-r--r-- | src/layout/tests.rs | 4 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 12 | ||||
| -rw-r--r-- | wiki/Configuration:-Layout.md | 15 |
10 files changed, 266 insertions, 56 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 8238e37d..fe43f814 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -458,6 +458,8 @@ pub struct Layout { pub always_center_single_column: bool, #[knuffel(child)] pub empty_workspace_above_first: bool, + #[knuffel(child, unwrap(argument), default)] + pub default_column_display: ColumnDisplay, #[knuffel(child, unwrap(argument), default = Self::default().gaps)] pub gaps: FloatOrInt<0, 65535>, #[knuffel(child, default)] @@ -476,6 +478,7 @@ impl Default for Layout { center_focused_column: Default::default(), always_center_single_column: false, empty_workspace_above_first: false, + default_column_display: Default::default(), gaps: FloatOrInt(16.), struts: Default::default(), preset_window_heights: Default::default(), @@ -794,6 +797,16 @@ impl From<PresetSize> for SizeChange { #[derive(Debug, Clone, Copy, PartialEq)] pub struct DefaultPresetSize(pub Option<PresetSize>); +/// How windows display in a column. +#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum ColumnDisplay { + /// Windows arranged vertically, spread across the working area height. + #[default] + Normal, + /// Windows are in tabs. + Tabbed, +} + #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct Struts { #[knuffel(child, unwrap(argument), default)] @@ -1367,6 +1380,7 @@ pub enum Action { ExpelWindowFromColumn, SwapWindowLeft, SwapWindowRight, + ToggleColumnTabbedDisplay, CenterColumn, CenterWindow, #[knuffel(skip)] @@ -1563,6 +1577,7 @@ impl From<niri_ipc::Action> for Action { niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn, niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight, niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft, + niri_ipc::Action::ToggleColumnTabbedDisplay {} => Self::ToggleColumnTabbedDisplay, niri_ipc::Action::CenterColumn {} => Self::CenterColumn, niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow, niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id), @@ -3490,6 +3505,8 @@ mod tests { center-focused-column "on-overflow" + default-column-display "tabbed" + insert-hint { color "rgb(255, 200, 127)" gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view" @@ -3754,6 +3771,7 @@ mod tests { bottom: FloatOrInt(0.), }, center_focused_column: CenterFocusedColumn::OnOverflow, + default_column_display: ColumnDisplay::Tabbed, always_center_single_column: false, empty_workspace_above_first: false, }, diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index c5551cc3..c2bd2c3c 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -321,6 +321,8 @@ pub enum Action { SwapWindowRight {}, /// Swap focused window with one to the left SwapWindowLeft {}, + /// Toggle the focused column between normal and tabbed display. + ToggleColumnTabbedDisplay {}, /// Center the focused column on the screen. CenterColumn {}, /// Center a window on the screen. diff --git a/resources/default-config.kdl b/resources/default-config.kdl index ad3e1b2b..133a542f 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -525,6 +525,11 @@ binds { Mod+V { toggle-window-floating; } Mod+Shift+V { switch-focus-between-floating-and-tiling; } + // Toggle tabbed column display mode. + // Windows in this column will appear as vertical tabs, + // rather than stacked on top of each other. + Mod+W { toggle-column-tabbed-display; } + // Actions to switch layouts. // Note: if you uncomment these, make sure you do NOT have // a matching layout switch hotkey configured in xkb options above. diff --git a/src/input/mod.rs b/src/input/mod.rs index 5c3df990..54b8a827 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1259,6 +1259,12 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } + Action::ToggleColumnTabbedDisplay => { + self.niri.layout.toggle_column_tabbed_display(); + self.maybe_warp_cursor_to_focus(); + // FIXME: granular + self.niri.queue_redraw_all(); + } Action::SwitchPresetColumnWidth => { self.niri.layout.toggle_width(); } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 4e0dbf93..54485617 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -39,7 +39,7 @@ use std::time::Duration; use monitor::MonitorAddWindowTarget; use niri_config::{ - CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts, + CenterFocusedColumn, ColumnDisplay, Config, CornerRadius, FloatOrInt, PresetSize, Struts, Workspace as WorkspaceConfig, WorkspaceReference, }; use niri_ipc::{PositionChange, SizeChange}; @@ -316,6 +316,7 @@ pub struct Options { pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, pub empty_workspace_above_first: bool, + pub default_column_display: ColumnDisplay, /// Column or window widths that `toggle_width()` switches between. pub preset_column_widths: Vec<PresetSize>, /// Initial width for new columns. @@ -340,6 +341,7 @@ impl Default for Options { center_focused_column: Default::default(), always_center_single_column: false, empty_workspace_above_first: false, + default_column_display: Default::default(), preset_column_widths: vec![ PresetSize::Proportion(1. / 3.), PresetSize::Proportion(0.5), @@ -552,6 +554,7 @@ impl Options { center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, empty_workspace_above_first: layout.empty_workspace_above_first, + default_column_display: layout.default_column_display, preset_column_widths, default_column_width, animations: config.animations.clone(), @@ -2141,6 +2144,13 @@ impl<W: LayoutElement> Layout<W> { monitor.swap_window_in_direction(direction); } + pub fn toggle_column_tabbed_display(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.toggle_column_tabbed_display(); + } + pub fn center_column(&mut self) { let Some(monitor) = self.active_monitor() else { return; diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 53bf5377..a9e76d5e 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -732,6 +732,10 @@ impl<W: LayoutElement> Monitor<W> { self.active_workspace().swap_window_in_direction(direction); } + pub fn toggle_column_tabbed_display(&mut self) { + self.active_workspace().toggle_column_tabbed_display(); + } + pub fn center_column(&mut self) { self.active_workspace().center_column(); } diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index a57d6109..ffa111ba 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -3,7 +3,7 @@ use std::iter::{self, zip}; use std::rc::Rc; use std::time::Duration; -use niri_config::{CenterFocusedColumn, CornerRadius, PresetSize, Struts}; +use niri_config::{CenterFocusedColumn, ColumnDisplay, CornerRadius, PresetSize, Struts}; use niri_ipc::SizeChange; use ordered_float::NotNan; use smithay::backend::renderer::gles::GlesRenderer; @@ -171,6 +171,9 @@ pub struct Column<W: LayoutElement> { /// Whether this column contains a single full-screened window. is_fullscreen: bool, + /// How this column displays and arranges windows. + display_mode: ColumnDisplay, + /// Animation of the render offset during window swapping. move_animation: Option<Animation>, @@ -744,15 +747,28 @@ impl<W: LayoutElement> ScrollingSpace<W> { // Find the closest gap between tiles. let col = &self.columns[col_idx]; - let (closest_tile_idx, tile_off) = col - .tile_offsets() - .enumerate() - .min_by_key(|(_, tile_off)| NotNan::new((tile_off.y - y).abs()).unwrap()) - .unwrap(); + + let (closest_tile_idx, tile_y) = if col.display_mode == ColumnDisplay::Tabbed { + // In tabbed mode, there's only one tile visible, and we want to check its top and + // bottom. + let top = col.tile_offsets().nth(col.active_tile_idx).unwrap().y; + let bottom = top + col.data[col.active_tile_idx].size.h; + if (top - y).abs() <= (bottom - y).abs() { + (col.active_tile_idx, top) + } else { + (col.active_tile_idx + 1, bottom) + } + } else { + col.tile_offsets() + .map(|tile_off| tile_off.y) + .enumerate() + .min_by_key(|(_, tile_y)| NotNan::new((tile_y - y).abs()).unwrap()) + .unwrap() + }; // Return the closest among the vertical and the horizontal gap. let vert_dist = (col_x - x).abs(); - let hor_dist = (tile_off.y - y).abs(); + let hor_dist = (tile_y - y).abs(); if vert_dist <= hor_dist { InsertPosition::NewColumn(closest_col_idx) } else { @@ -1288,6 +1304,11 @@ impl<W: LayoutElement> ScrollingSpace<W> { let col = &self.columns[col_idx]; let removing_last = col.tiles.len() == 1; + // Skip closing animation for invisible tiles in a tabbed column. + if col.display_mode == ColumnDisplay::Tabbed && tile_idx != col.active_tile_idx { + return; + } + tile_pos.x += self.view_pos(); if col_idx < self.active_column_idx { @@ -1936,6 +1957,16 @@ impl<W: LayoutElement> ScrollingSpace<W> { self.activate_column(target_column_idx); } + pub fn toggle_column_tabbed_display(&mut self) { + if self.columns.is_empty() { + return; + } + + let col = &mut self.columns[self.active_column_idx]; + cancel_resize_for_column(&mut self.interactive_resize, col); + col.toggle_tabbed_display(); + } + pub fn center_column(&mut self) { if self.columns.is_empty() { return; @@ -2054,19 +2085,21 @@ impl<W: LayoutElement> ScrollingSpace<W> { pub fn tiles_with_render_positions( &self, - ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> { + ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> { let scale = self.scale; let view_off = Point::from((-self.view_pos(), 0.)); self.columns_in_render_order() .flat_map(move |(col, col_x)| { let col_off = Point::from((col_x, 0.)); let col_render_off = col.render_offset(); - col.tiles_in_render_order().map(move |(tile, tile_off)| { - let pos = view_off + col_off + col_render_off + tile_off + tile.render_offset(); - // Round to physical pixels. - let pos = pos.to_physical_precise_round(scale).to_logical(scale); - (tile, pos) - }) + col.tiles_in_render_order() + .map(move |(tile, tile_off, visible)| { + let pos = + view_off + col_off + col_render_off + tile_off + tile.render_offset(); + // Round to physical pixels. + let pos = pos.to_physical_precise_round(scale).to_logical(scale); + (tile, pos, visible) + }) }) } @@ -2132,18 +2165,29 @@ impl<W: LayoutElement> ScrollingSpace<W> { return None; } - let (height, y) = if tile_index == 0 { - (150., col.tile_offset(tile_index).y) - } else if tile_index == col.tiles.len() { - ( - 150., - col.tile_offset(tile_index).y - self.options.gaps - 150., - ) + let is_tabbed = col.display_mode == ColumnDisplay::Tabbed; + + let (height, y) = if is_tabbed { + // In tabbed mode, there's only one tile visible, and we want to draw the hint + // at its top or bottom. + let top = col.tile_offset(col.active_tile_idx).y; + let bottom = top + col.data[col.active_tile_idx].size.h; + + if tile_index <= col.active_tile_idx { + (150., top) + } else { + (150., bottom - 150.) + } } else { - ( - 300., - col.tile_offset(tile_index).y - self.options.gaps / 2. - 150., - ) + let top = col.tile_offset(tile_index).y; + + if tile_index == 0 { + (150., top) + } else if tile_index == col.tiles.len() { + (150., top - self.options.gaps - 150.) + } else { + (300., top - self.options.gaps / 2. - 150.) + } }; let size = Size::from((self.data[column_index].width, height)); @@ -2455,11 +2499,15 @@ impl<W: LayoutElement> ScrollingSpace<W> { } let mut first = true; - for (tile, tile_pos) in self.tiles_with_render_positions() { + for (tile, tile_pos, visible) in self.tiles_with_render_positions() { // For the active tile (which comes first), draw the focus ring. let focus_ring = focus_ring && first; first = false; + if !visible { + continue; + } + rv.extend( tile.render(renderer, tile_pos, scale, focus_ring, target) .map(Into::into), @@ -2909,11 +2957,18 @@ impl<W: LayoutElement> ScrollingSpace<W> { } } + let is_tabbed = col.display_mode == ColumnDisplay::Tabbed; + + // If transactions are disabled, also disable combined throttling, for more intuitive + // behavior. In tabbed display mode, only one window is visible, so individual + // throttling makes more sense. + let individual_throttling = self.options.disable_transactions || is_tabbed; + let intent = if self.options.disable_resize_throttling { ConfigureIntent::CanSend - } else if self.options.disable_transactions { - // When transactions are disabled, we don't use combined throttling, but rather - // compute throttling individually below. + } else if individual_throttling { + // In this case, we don't use combined throttling, but rather compute throttling + // individually below. ConfigureIntent::CanSend } else { col.tiles @@ -2937,7 +2992,11 @@ impl<W: LayoutElement> ScrollingSpace<W> { win.set_active_in_column(active_in_column); win.set_floating(false); - let active = is_active && self.active_column_idx == col_idx && active_in_column; + let active = is_active + && self.active_column_idx == col_idx + // In tabbed mode, all tabs have activated state to reduce unnecessary + // animations when switching tabs. + && (active_in_column || is_tabbed); win.set_activated(active); win.set_interactive_resize(col_resize_data); @@ -2950,9 +3009,7 @@ impl<W: LayoutElement> ScrollingSpace<W> { ); win.set_bounds(bounds); - // If transactions are disabled, also disable combined throttling, for more - // intuitive behavior. - let intent = if self.options.disable_transactions { + let intent = if individual_throttling { win.configure_intent() } else { intent @@ -3166,6 +3223,8 @@ impl<W: LayoutElement> Column<W> { is_full_width: bool, animate_resize: bool, ) -> Self { + let options = tile.options.clone(); + let mut rv = Self { tiles: vec![], data: vec![], @@ -3174,12 +3233,13 @@ impl<W: LayoutElement> Column<W> { preset_width_idx: None, is_full_width, is_fullscreen: false, + display_mode: options.default_column_display, move_animation: None, view_size, working_area, scale, clock: tile.clock.clone(), - options: tile.options.clone(), + options, }; let is_pending_fullscreen = tile.window().is_pending_fullscreen(); @@ -3373,13 +3433,15 @@ impl<W: LayoutElement> Column<W> { tile.update_window(); self.data[tile_idx].update(tile); + let is_tabbed = self.display_mode == ColumnDisplay::Tabbed; + // Move windows below in tandem with resizing. // // FIXME: in always-centering mode, window resizing will affect the offsets of all other // windows in the column, so they should all be animated. How should this interact with // animated vs. non-animated resizes? For example, an animated +20 resize followed by two // non-animated -10 resizes. - if tile.resize_animation().is_some() && offset != 0. { + if !is_tabbed && tile.resize_animation().is_some() && offset != 0. { for tile in &mut self.tiles[tile_idx + 1..] { tile.animate_move_y_from_with_config( offset, @@ -3417,6 +3479,8 @@ impl<W: LayoutElement> Column<W> { return; } + let is_tabbed = self.display_mode == ColumnDisplay::Tabbed; + let min_size: Vec<_> = self .tiles .iter() @@ -3466,7 +3530,7 @@ impl<W: LayoutElement> Column<W> { // If there are multiple windows in a column, clamp the non-auto window's height according // to other windows' min sizes. let mut max_non_auto_window_height = None; - if self.tiles.len() > 1 { + if self.tiles.len() > 1 && !is_tabbed { if let Some(non_auto_idx) = self .data .iter() @@ -3522,6 +3586,39 @@ impl<W: LayoutElement> Column<W> { }) .collect::<Vec<_>>(); + // In tabbed display mode, fill fixed heights right away. + if is_tabbed { + // All tiles have the same height, equal to the height of the only fixed tile (if any). + let tabbed_height = heights + .iter() + .find_map(|h| { + if let WindowHeight::Fixed(h) = h { + Some(*h) + } else { + None + } + }) + .unwrap_or(max_tile_height); + + // We also take min height of all tabs into account. + let min_height = min_size + .iter() + .map(|size| NotNan::new(size.h).unwrap()) + .max() + .map(NotNan::into_inner) + .unwrap(); + // But, if there's a larger-than-workspace tab, we don't want to force all tabs to that + // size. + let min_height = f64::min(max_tile_height, min_height); + let tabbed_height = f64::max(tabbed_height, min_height); + + for h in &mut heights { + *h = WindowHeight::Fixed(tabbed_height); + } + + // The following logic will apply individual min/max height, etc. + } + let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64; let mut height_left = working_size.h - gaps_left; let mut auto_tiles_left = self.tiles.len(); @@ -3633,13 +3730,22 @@ impl<W: LayoutElement> Column<W> { assert_eq!(auto_tiles_left, 0); } - for (tile, h) in zip(&mut self.tiles, heights) { + for (tile_idx, (tile, h)) in zip(&mut self.tiles, heights).enumerate() { let WindowHeight::Fixed(height) = h else { unreachable!() }; let size = Size::from((width, height)); - tile.request_tile_size(size, animate, Some(transaction.clone())); + + // In tabbed mode, only the visible window participates in the transaction. + let is_active = tile_idx == self.active_tile_idx; + let transaction = if self.display_mode == ColumnDisplay::Tabbed && !is_active { + None + } else { + Some(transaction.clone()) + }; + + tile.request_tile_size(size, animate, transaction); } } @@ -3860,13 +3966,16 @@ impl<W: LayoutElement> Column<W> { }; // Clamp the height according to other windows' min sizes, or simply to working area height. - let min_height_taken = self - .tiles - .iter() - .enumerate() - .filter(|(idx, _)| *idx != tile_idx) - .map(|(_, tile)| f64::max(1., tile.min_size().h) + self.options.gaps) - .sum::<f64>(); + let min_height_taken = if self.display_mode == ColumnDisplay::Tabbed { + 0. + } else { + self.tiles + .iter() + .enumerate() + .filter(|(idx, _)| *idx != tile_idx) + .map(|(_, tile)| f64::max(1., tile.min_size().h) + gaps) + .sum::<f64>() + }; let height_left = working_size - gaps - min_height_taken - gaps; let height_left = f64::max(1., tile.window_height_for_tile_height(height_left)); window_height = f64::min(height_left, window_height); @@ -3888,8 +3997,17 @@ impl<W: LayoutElement> Column<W> { } fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) { - let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); - self.data[tile_idx].height = WindowHeight::auto_1(); + if self.display_mode == ColumnDisplay::Tabbed { + // When tabbed, reset window height should work on any window, not just the fixed-size + // one. + for data in &mut self.data { + data.height = WindowHeight::auto_1(); + } + } else { + let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); + self.data[tile_idx].height = WindowHeight::auto_1(); + } + self.update_tile_sizes(animate); } @@ -3965,6 +4083,14 @@ impl<W: LayoutElement> Column<W> { self.update_tile_sizes(false); } + fn toggle_tabbed_display(&mut self) { + self.display_mode = match self.display_mode { + ColumnDisplay::Normal => ColumnDisplay::Tabbed, + ColumnDisplay::Tabbed => ColumnDisplay::Normal, + }; + self.update_tile_sizes(true); + } + fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> { for (tile, pos) in self.tiles() { if tile.window().id() == id { @@ -4007,6 +4133,7 @@ impl<W: LayoutElement> Column<W> { // the workspace or some other reason. let center = self.options.center_focused_column == CenterFocusedColumn::Always; let gaps = self.options.gaps; + let tabbed = self.display_mode == ColumnDisplay::Tabbed; let col_width = if self.tiles.is_empty() { 0. } else { @@ -4031,7 +4158,9 @@ impl<W: LayoutElement> Column<W> { pos.x += col_width - data.size.w; } - origin.y += data.size.h + gaps; + if !tabbed { + origin.y += data.size.h + gaps; + } pos }) @@ -4068,14 +4197,22 @@ impl<W: LayoutElement> Column<W> { zip(&mut self.tiles, offsets) } - fn tiles_in_render_order(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> + '_ { + fn tiles_in_render_order( + &self, + ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> + '_ { let offsets = self.tile_offsets_in_render_order(self.data.iter().copied()); let (first, rest) = self.tiles.split_at(self.active_tile_idx); let (active, rest) = rest.split_at(1); - let tiles = active.iter().chain(first).chain(rest); - zip(tiles, offsets) + let active = active.iter().map(|tile| (tile, true)); + + let rest_visible = self.display_mode != ColumnDisplay::Tabbed; + let rest = first.iter().chain(rest); + let rest = rest.map(move |tile| (tile, rest_visible)); + + let tiles = active.chain(rest); + zip(tiles, offsets).map(|((tile, visible), pos)| (tile, pos, visible)) } fn tiles_in_render_order_mut( @@ -4104,6 +4241,8 @@ impl<W: LayoutElement> Column<W> { assert!(idx < self.options.preset_column_widths.len()); } + let is_tabbed = self.display_mode == ColumnDisplay::Tabbed; + let tile_count = self.tiles.len(); if tile_count == 1 { if let WindowHeight::Auto { weight } = self.data[0].height { @@ -4168,7 +4307,8 @@ impl<W: LayoutElement> Column<W> { total_min_height += min_tile_height; } - if tile_count > 1 + if !is_tabbed + && tile_count > 1 && self.scale.round() == self.scale && working_size.h.round() == working_size.h && gaps.round() == gaps diff --git a/src/layout/tests.rs b/src/layout/tests.rs index e688c8bd..55bbecb9 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -406,6 +406,7 @@ enum Op { ConsumeWindowIntoColumn, ExpelWindowFromColumn, SwapWindowInDirection(#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection), + ToggleColumnTabbedDisplay, CenterColumn, CenterWindow { #[proptest(strategy = "proptest::option::of(1..=5usize)")] @@ -969,6 +970,7 @@ impl Op { Op::ConsumeWindowIntoColumn => layout.consume_into_column(), Op::ExpelWindowFromColumn => layout.expel_from_column(), Op::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction), + Op::ToggleColumnTabbedDisplay => layout.toggle_column_tabbed_display(), Op::CenterColumn => layout.center_column(), Op::CenterWindow { id } => { let id = id.filter(|id| layout.has_window(id)); @@ -1462,6 +1464,7 @@ fn operations_dont_panic() { Op::ConsumeOrExpelWindowLeft { id: None }, Op::ConsumeOrExpelWindowRight { id: None }, Op::MoveWorkspaceToOutput(1), + Op::ToggleColumnTabbedDisplay, ]; for third in every_op { @@ -1636,6 +1639,7 @@ fn operations_from_starting_state_dont_panic() { Op::MoveWindowUpOrToWorkspaceUp, Op::ConsumeOrExpelWindowLeft { id: None }, Op::ConsumeOrExpelWindowRight { id: None }, + Op::ToggleColumnTabbedDisplay, ]; for third in every_op { diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 0dca0805..f811b37e 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -577,10 +577,10 @@ impl<W: LayoutElement> Workspace<W> { self.floating.add_tile_above(next_to, tile, activate); } else { // FIXME: use static pos - let (next_to_tile, render_pos) = self + let (next_to_tile, render_pos, _visible) = self .scrolling .tiles_with_render_positions() - .find(|(tile, _)| tile.window().id() == next_to) + .find(|(tile, _, _)| tile.window().id() == next_to) .unwrap(); // Position the new tile in the center above the next_to tile. Think a @@ -1022,6 +1022,13 @@ impl<W: LayoutElement> Workspace<W> { self.scrolling.swap_window_in_direction(direction); } + pub fn toggle_column_tabbed_display(&mut self) { + if self.floating_is_active.get() { + return; + } + self.scrolling.toggle_column_tabbed_display(); + } + pub fn center_column(&mut self) { if self.floating_is_active.get() { self.floating.center_window(None); @@ -1336,7 +1343,6 @@ impl<W: LayoutElement> Workspace<W> { &self, ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> { let scrolling = self.scrolling.tiles_with_render_positions(); - let scrolling = scrolling.map(|(tile, pos)| (tile, pos, true)); let floating = self.floating.tiles_with_render_positions(); let visible = self.is_floating_visible(); diff --git a/wiki/Configuration:-Layout.md b/wiki/Configuration:-Layout.md index 1c8c7214..0017db25 100644 --- a/wiki/Configuration:-Layout.md +++ b/wiki/Configuration:-Layout.md @@ -10,6 +10,7 @@ layout { center-focused-column "never" always-center-single-column empty-workspace-above-first + default-column-display "tabbed" preset-column-widths { proportion 0.33333 @@ -123,6 +124,20 @@ layout { } ``` +### `default-column-display` + +<sup>Since: next release</sup> + +Sets the default display mode for new columns. +Can be `normal` or `tabbed`. + +```kdl +// Make all new columns tabbed by default. +layout { + default-column-display "tabbed" +} +``` + ### `preset-column-widths` Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between. |
