Implement tabbed column display mode

This commit is contained in:
Ivan Molodetskikh
2025-02-01 10:46:52 +03:00
parent 55e2ea0c3b
commit f90eb0cbe4
10 changed files with 266 additions and 56 deletions
+18
View File
@@ -458,6 +458,8 @@ pub struct Layout {
pub always_center_single_column: bool, pub always_center_single_column: bool,
#[knuffel(child)] #[knuffel(child)]
pub empty_workspace_above_first: bool, pub empty_workspace_above_first: bool,
#[knuffel(child, unwrap(argument), default)]
pub default_column_display: ColumnDisplay,
#[knuffel(child, unwrap(argument), default = Self::default().gaps)] #[knuffel(child, unwrap(argument), default = Self::default().gaps)]
pub gaps: FloatOrInt<0, 65535>, pub gaps: FloatOrInt<0, 65535>,
#[knuffel(child, default)] #[knuffel(child, default)]
@@ -476,6 +478,7 @@ impl Default for Layout {
center_focused_column: Default::default(), center_focused_column: Default::default(),
always_center_single_column: false, always_center_single_column: false,
empty_workspace_above_first: false, empty_workspace_above_first: false,
default_column_display: Default::default(),
gaps: FloatOrInt(16.), gaps: FloatOrInt(16.),
struts: Default::default(), struts: Default::default(),
preset_window_heights: Default::default(), preset_window_heights: Default::default(),
@@ -794,6 +797,16 @@ impl From<PresetSize> for SizeChange {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct DefaultPresetSize(pub Option<PresetSize>); 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)] #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct Struts { pub struct Struts {
#[knuffel(child, unwrap(argument), default)] #[knuffel(child, unwrap(argument), default)]
@@ -1367,6 +1380,7 @@ pub enum Action {
ExpelWindowFromColumn, ExpelWindowFromColumn,
SwapWindowLeft, SwapWindowLeft,
SwapWindowRight, SwapWindowRight,
ToggleColumnTabbedDisplay,
CenterColumn, CenterColumn,
CenterWindow, CenterWindow,
#[knuffel(skip)] #[knuffel(skip)]
@@ -1563,6 +1577,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn, niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight, niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight,
niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft, niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft,
niri_ipc::Action::ToggleColumnTabbedDisplay {} => Self::ToggleColumnTabbedDisplay,
niri_ipc::Action::CenterColumn {} => Self::CenterColumn, niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow, niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id), niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
@@ -3490,6 +3505,8 @@ mod tests {
center-focused-column "on-overflow" center-focused-column "on-overflow"
default-column-display "tabbed"
insert-hint { insert-hint {
color "rgb(255, 200, 127)" color "rgb(255, 200, 127)"
gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view" gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view"
@@ -3754,6 +3771,7 @@ mod tests {
bottom: FloatOrInt(0.), bottom: FloatOrInt(0.),
}, },
center_focused_column: CenterFocusedColumn::OnOverflow, center_focused_column: CenterFocusedColumn::OnOverflow,
default_column_display: ColumnDisplay::Tabbed,
always_center_single_column: false, always_center_single_column: false,
empty_workspace_above_first: false, empty_workspace_above_first: false,
}, },
+2
View File
@@ -321,6 +321,8 @@ pub enum Action {
SwapWindowRight {}, SwapWindowRight {},
/// Swap focused window with one to the left /// Swap focused window with one to the left
SwapWindowLeft {}, SwapWindowLeft {},
/// Toggle the focused column between normal and tabbed display.
ToggleColumnTabbedDisplay {},
/// Center the focused column on the screen. /// Center the focused column on the screen.
CenterColumn {}, CenterColumn {},
/// Center a window on the screen. /// Center a window on the screen.
+5
View File
@@ -525,6 +525,11 @@ binds {
Mod+V { toggle-window-floating; } Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; } 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. // Actions to switch layouts.
// Note: if you uncomment these, make sure you do NOT have // Note: if you uncomment these, make sure you do NOT have
// a matching layout switch hotkey configured in xkb options above. // a matching layout switch hotkey configured in xkb options above.
+6
View File
@@ -1259,6 +1259,12 @@ impl State {
// FIXME: granular // FIXME: granular
self.niri.queue_redraw_all(); 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 => { Action::SwitchPresetColumnWidth => {
self.niri.layout.toggle_width(); self.niri.layout.toggle_width();
} }
+11 -1
View File
@@ -39,7 +39,7 @@ use std::time::Duration;
use monitor::MonitorAddWindowTarget; use monitor::MonitorAddWindowTarget;
use niri_config::{ use niri_config::{
CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts, CenterFocusedColumn, ColumnDisplay, Config, CornerRadius, FloatOrInt, PresetSize, Struts,
Workspace as WorkspaceConfig, WorkspaceReference, Workspace as WorkspaceConfig, WorkspaceReference,
}; };
use niri_ipc::{PositionChange, SizeChange}; use niri_ipc::{PositionChange, SizeChange};
@@ -316,6 +316,7 @@ pub struct Options {
pub center_focused_column: CenterFocusedColumn, pub center_focused_column: CenterFocusedColumn,
pub always_center_single_column: bool, pub always_center_single_column: bool,
pub empty_workspace_above_first: bool, pub empty_workspace_above_first: bool,
pub default_column_display: ColumnDisplay,
/// Column or window widths that `toggle_width()` switches between. /// Column or window widths that `toggle_width()` switches between.
pub preset_column_widths: Vec<PresetSize>, pub preset_column_widths: Vec<PresetSize>,
/// Initial width for new columns. /// Initial width for new columns.
@@ -340,6 +341,7 @@ impl Default for Options {
center_focused_column: Default::default(), center_focused_column: Default::default(),
always_center_single_column: false, always_center_single_column: false,
empty_workspace_above_first: false, empty_workspace_above_first: false,
default_column_display: Default::default(),
preset_column_widths: vec![ preset_column_widths: vec![
PresetSize::Proportion(1. / 3.), PresetSize::Proportion(1. / 3.),
PresetSize::Proportion(0.5), PresetSize::Proportion(0.5),
@@ -552,6 +554,7 @@ impl Options {
center_focused_column: layout.center_focused_column, center_focused_column: layout.center_focused_column,
always_center_single_column: layout.always_center_single_column, always_center_single_column: layout.always_center_single_column,
empty_workspace_above_first: layout.empty_workspace_above_first, empty_workspace_above_first: layout.empty_workspace_above_first,
default_column_display: layout.default_column_display,
preset_column_widths, preset_column_widths,
default_column_width, default_column_width,
animations: config.animations.clone(), animations: config.animations.clone(),
@@ -2141,6 +2144,13 @@ impl<W: LayoutElement> Layout<W> {
monitor.swap_window_in_direction(direction); 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) { pub fn center_column(&mut self) {
let Some(monitor) = self.active_monitor() else { let Some(monitor) = self.active_monitor() else {
return; return;
+4
View File
@@ -732,6 +732,10 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().swap_window_in_direction(direction); 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) { pub fn center_column(&mut self) {
self.active_workspace().center_column(); self.active_workspace().center_column();
} }
+192 -52
View File
@@ -3,7 +3,7 @@ use std::iter::{self, zip};
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use niri_config::{CenterFocusedColumn, CornerRadius, PresetSize, Struts}; use niri_config::{CenterFocusedColumn, ColumnDisplay, CornerRadius, PresetSize, Struts};
use niri_ipc::SizeChange; use niri_ipc::SizeChange;
use ordered_float::NotNan; use ordered_float::NotNan;
use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::gles::GlesRenderer;
@@ -171,6 +171,9 @@ pub struct Column<W: LayoutElement> {
/// Whether this column contains a single full-screened window. /// Whether this column contains a single full-screened window.
is_fullscreen: bool, is_fullscreen: bool,
/// How this column displays and arranges windows.
display_mode: ColumnDisplay,
/// Animation of the render offset during window swapping. /// Animation of the render offset during window swapping.
move_animation: Option<Animation>, move_animation: Option<Animation>,
@@ -744,15 +747,28 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Find the closest gap between tiles. // Find the closest gap between tiles.
let col = &self.columns[col_idx]; let col = &self.columns[col_idx];
let (closest_tile_idx, tile_off) = col
.tile_offsets() let (closest_tile_idx, tile_y) = if col.display_mode == ColumnDisplay::Tabbed {
.enumerate() // In tabbed mode, there's only one tile visible, and we want to check its top and
.min_by_key(|(_, tile_off)| NotNan::new((tile_off.y - y).abs()).unwrap()) // bottom.
.unwrap(); 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. // Return the closest among the vertical and the horizontal gap.
let vert_dist = (col_x - x).abs(); 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 { if vert_dist <= hor_dist {
InsertPosition::NewColumn(closest_col_idx) InsertPosition::NewColumn(closest_col_idx)
} else { } else {
@@ -1288,6 +1304,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let col = &self.columns[col_idx]; let col = &self.columns[col_idx];
let removing_last = col.tiles.len() == 1; 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(); tile_pos.x += self.view_pos();
if col_idx < self.active_column_idx { if col_idx < self.active_column_idx {
@@ -1936,6 +1957,16 @@ impl<W: LayoutElement> ScrollingSpace<W> {
self.activate_column(target_column_idx); 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) { pub fn center_column(&mut self) {
if self.columns.is_empty() { if self.columns.is_empty() {
return; return;
@@ -2054,19 +2085,21 @@ impl<W: LayoutElement> ScrollingSpace<W> {
pub fn tiles_with_render_positions( pub fn tiles_with_render_positions(
&self, &self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> { ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
let scale = self.scale; let scale = self.scale;
let view_off = Point::from((-self.view_pos(), 0.)); let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order() self.columns_in_render_order()
.flat_map(move |(col, col_x)| { .flat_map(move |(col, col_x)| {
let col_off = Point::from((col_x, 0.)); let col_off = Point::from((col_x, 0.));
let col_render_off = col.render_offset(); let col_render_off = col.render_offset();
col.tiles_in_render_order().map(move |(tile, tile_off)| { col.tiles_in_render_order()
let pos = view_off + col_off + col_render_off + tile_off + tile.render_offset(); .map(move |(tile, tile_off, visible)| {
// Round to physical pixels. let pos =
let pos = pos.to_physical_precise_round(scale).to_logical(scale); view_off + col_off + col_render_off + tile_off + tile.render_offset();
(tile, pos) // 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; return None;
} }
let (height, y) = if tile_index == 0 { let is_tabbed = col.display_mode == ColumnDisplay::Tabbed;
(150., col.tile_offset(tile_index).y)
} else if tile_index == col.tiles.len() { let (height, y) = if is_tabbed {
( // In tabbed mode, there's only one tile visible, and we want to draw the hint
150., // at its top or bottom.
col.tile_offset(tile_index).y - self.options.gaps - 150., 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 { } else {
( let top = col.tile_offset(tile_index).y;
300.,
col.tile_offset(tile_index).y - self.options.gaps / 2. - 150., 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)); let size = Size::from((self.data[column_index].width, height));
@@ -2455,11 +2499,15 @@ impl<W: LayoutElement> ScrollingSpace<W> {
} }
let mut first = true; 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. // For the active tile (which comes first), draw the focus ring.
let focus_ring = focus_ring && first; let focus_ring = focus_ring && first;
first = false; first = false;
if !visible {
continue;
}
rv.extend( rv.extend(
tile.render(renderer, tile_pos, scale, focus_ring, target) tile.render(renderer, tile_pos, scale, focus_ring, target)
.map(Into::into), .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 { let intent = if self.options.disable_resize_throttling {
ConfigureIntent::CanSend ConfigureIntent::CanSend
} else if self.options.disable_transactions { } else if individual_throttling {
// When transactions are disabled, we don't use combined throttling, but rather // In this case, we don't use combined throttling, but rather compute throttling
// compute throttling individually below. // individually below.
ConfigureIntent::CanSend ConfigureIntent::CanSend
} else { } else {
col.tiles col.tiles
@@ -2937,7 +2992,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
win.set_active_in_column(active_in_column); win.set_active_in_column(active_in_column);
win.set_floating(false); 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_activated(active);
win.set_interactive_resize(col_resize_data); win.set_interactive_resize(col_resize_data);
@@ -2950,9 +3009,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
); );
win.set_bounds(bounds); win.set_bounds(bounds);
// If transactions are disabled, also disable combined throttling, for more let intent = if individual_throttling {
// intuitive behavior.
let intent = if self.options.disable_transactions {
win.configure_intent() win.configure_intent()
} else { } else {
intent intent
@@ -3166,6 +3223,8 @@ impl<W: LayoutElement> Column<W> {
is_full_width: bool, is_full_width: bool,
animate_resize: bool, animate_resize: bool,
) -> Self { ) -> Self {
let options = tile.options.clone();
let mut rv = Self { let mut rv = Self {
tiles: vec![], tiles: vec![],
data: vec![], data: vec![],
@@ -3174,12 +3233,13 @@ impl<W: LayoutElement> Column<W> {
preset_width_idx: None, preset_width_idx: None,
is_full_width, is_full_width,
is_fullscreen: false, is_fullscreen: false,
display_mode: options.default_column_display,
move_animation: None, move_animation: None,
view_size, view_size,
working_area, working_area,
scale, scale,
clock: tile.clock.clone(), clock: tile.clock.clone(),
options: tile.options.clone(), options,
}; };
let is_pending_fullscreen = tile.window().is_pending_fullscreen(); let is_pending_fullscreen = tile.window().is_pending_fullscreen();
@@ -3373,13 +3433,15 @@ impl<W: LayoutElement> Column<W> {
tile.update_window(); tile.update_window();
self.data[tile_idx].update(tile); self.data[tile_idx].update(tile);
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
// Move windows below in tandem with resizing. // Move windows below in tandem with resizing.
// //
// FIXME: in always-centering mode, window resizing will affect the offsets of all other // 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 // 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 // animated vs. non-animated resizes? For example, an animated +20 resize followed by two
// non-animated -10 resizes. // 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..] { for tile in &mut self.tiles[tile_idx + 1..] {
tile.animate_move_y_from_with_config( tile.animate_move_y_from_with_config(
offset, offset,
@@ -3417,6 +3479,8 @@ impl<W: LayoutElement> Column<W> {
return; return;
} }
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
let min_size: Vec<_> = self let min_size: Vec<_> = self
.tiles .tiles
.iter() .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 // If there are multiple windows in a column, clamp the non-auto window's height according
// to other windows' min sizes. // to other windows' min sizes.
let mut max_non_auto_window_height = None; 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 if let Some(non_auto_idx) = self
.data .data
.iter() .iter()
@@ -3522,6 +3586,39 @@ impl<W: LayoutElement> Column<W> {
}) })
.collect::<Vec<_>>(); .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 gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64;
let mut height_left = working_size.h - gaps_left; let mut height_left = working_size.h - gaps_left;
let mut auto_tiles_left = self.tiles.len(); let mut auto_tiles_left = self.tiles.len();
@@ -3633,13 +3730,22 @@ impl<W: LayoutElement> Column<W> {
assert_eq!(auto_tiles_left, 0); 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 { let WindowHeight::Fixed(height) = h else {
unreachable!() unreachable!()
}; };
let size = Size::from((width, height)); 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. // Clamp the height according to other windows' min sizes, or simply to working area height.
let min_height_taken = self let min_height_taken = if self.display_mode == ColumnDisplay::Tabbed {
.tiles 0.
.iter() } else {
.enumerate() self.tiles
.filter(|(idx, _)| *idx != tile_idx) .iter()
.map(|(_, tile)| f64::max(1., tile.min_size().h) + self.options.gaps) .enumerate()
.sum::<f64>(); .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 = working_size - gaps - min_height_taken - gaps;
let height_left = f64::max(1., tile.window_height_for_tile_height(height_left)); let height_left = f64::max(1., tile.window_height_for_tile_height(height_left));
window_height = f64::min(height_left, window_height); 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) { fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); if self.display_mode == ColumnDisplay::Tabbed {
self.data[tile_idx].height = WindowHeight::auto_1(); // 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); self.update_tile_sizes(animate);
} }
@@ -3965,6 +4083,14 @@ impl<W: LayoutElement> Column<W> {
self.update_tile_sizes(false); 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>> { fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
for (tile, pos) in self.tiles() { for (tile, pos) in self.tiles() {
if tile.window().id() == id { if tile.window().id() == id {
@@ -4007,6 +4133,7 @@ impl<W: LayoutElement> Column<W> {
// the workspace or some other reason. // the workspace or some other reason.
let center = self.options.center_focused_column == CenterFocusedColumn::Always; let center = self.options.center_focused_column == CenterFocusedColumn::Always;
let gaps = self.options.gaps; let gaps = self.options.gaps;
let tabbed = self.display_mode == ColumnDisplay::Tabbed;
let col_width = if self.tiles.is_empty() { let col_width = if self.tiles.is_empty() {
0. 0.
} else { } else {
@@ -4031,7 +4158,9 @@ impl<W: LayoutElement> Column<W> {
pos.x += col_width - data.size.w; pos.x += col_width - data.size.w;
} }
origin.y += data.size.h + gaps; if !tabbed {
origin.y += data.size.h + gaps;
}
pos pos
}) })
@@ -4068,14 +4197,22 @@ impl<W: LayoutElement> Column<W> {
zip(&mut self.tiles, offsets) 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 offsets = self.tile_offsets_in_render_order(self.data.iter().copied());
let (first, rest) = self.tiles.split_at(self.active_tile_idx); let (first, rest) = self.tiles.split_at(self.active_tile_idx);
let (active, rest) = rest.split_at(1); let (active, rest) = rest.split_at(1);
let tiles = active.iter().chain(first).chain(rest); let active = active.iter().map(|tile| (tile, true));
zip(tiles, offsets)
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( fn tiles_in_render_order_mut(
@@ -4104,6 +4241,8 @@ impl<W: LayoutElement> Column<W> {
assert!(idx < self.options.preset_column_widths.len()); assert!(idx < self.options.preset_column_widths.len());
} }
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
let tile_count = self.tiles.len(); let tile_count = self.tiles.len();
if tile_count == 1 { if tile_count == 1 {
if let WindowHeight::Auto { weight } = self.data[0].height { if let WindowHeight::Auto { weight } = self.data[0].height {
@@ -4168,7 +4307,8 @@ impl<W: LayoutElement> Column<W> {
total_min_height += min_tile_height; total_min_height += min_tile_height;
} }
if tile_count > 1 if !is_tabbed
&& tile_count > 1
&& self.scale.round() == self.scale && self.scale.round() == self.scale
&& working_size.h.round() == working_size.h && working_size.h.round() == working_size.h
&& gaps.round() == gaps && gaps.round() == gaps
+4
View File
@@ -406,6 +406,7 @@ enum Op {
ConsumeWindowIntoColumn, ConsumeWindowIntoColumn,
ExpelWindowFromColumn, ExpelWindowFromColumn,
SwapWindowInDirection(#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection), SwapWindowInDirection(#[proptest(strategy = "arbitrary_scroll_direction()")] ScrollDirection),
ToggleColumnTabbedDisplay,
CenterColumn, CenterColumn,
CenterWindow { CenterWindow {
#[proptest(strategy = "proptest::option::of(1..=5usize)")] #[proptest(strategy = "proptest::option::of(1..=5usize)")]
@@ -969,6 +970,7 @@ impl Op {
Op::ConsumeWindowIntoColumn => layout.consume_into_column(), Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
Op::ExpelWindowFromColumn => layout.expel_from_column(), Op::ExpelWindowFromColumn => layout.expel_from_column(),
Op::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction), Op::SwapWindowInDirection(direction) => layout.swap_window_in_direction(direction),
Op::ToggleColumnTabbedDisplay => layout.toggle_column_tabbed_display(),
Op::CenterColumn => layout.center_column(), Op::CenterColumn => layout.center_column(),
Op::CenterWindow { id } => { Op::CenterWindow { id } => {
let id = id.filter(|id| layout.has_window(id)); let id = id.filter(|id| layout.has_window(id));
@@ -1462,6 +1464,7 @@ fn operations_dont_panic() {
Op::ConsumeOrExpelWindowLeft { id: None }, Op::ConsumeOrExpelWindowLeft { id: None },
Op::ConsumeOrExpelWindowRight { id: None }, Op::ConsumeOrExpelWindowRight { id: None },
Op::MoveWorkspaceToOutput(1), Op::MoveWorkspaceToOutput(1),
Op::ToggleColumnTabbedDisplay,
]; ];
for third in every_op { for third in every_op {
@@ -1636,6 +1639,7 @@ fn operations_from_starting_state_dont_panic() {
Op::MoveWindowUpOrToWorkspaceUp, Op::MoveWindowUpOrToWorkspaceUp,
Op::ConsumeOrExpelWindowLeft { id: None }, Op::ConsumeOrExpelWindowLeft { id: None },
Op::ConsumeOrExpelWindowRight { id: None }, Op::ConsumeOrExpelWindowRight { id: None },
Op::ToggleColumnTabbedDisplay,
]; ];
for third in every_op { for third in every_op {
+9 -3
View File
@@ -577,10 +577,10 @@ impl<W: LayoutElement> Workspace<W> {
self.floating.add_tile_above(next_to, tile, activate); self.floating.add_tile_above(next_to, tile, activate);
} else { } else {
// FIXME: use static pos // FIXME: use static pos
let (next_to_tile, render_pos) = self let (next_to_tile, render_pos, _visible) = self
.scrolling .scrolling
.tiles_with_render_positions() .tiles_with_render_positions()
.find(|(tile, _)| tile.window().id() == next_to) .find(|(tile, _, _)| tile.window().id() == next_to)
.unwrap(); .unwrap();
// Position the new tile in the center above the next_to tile. Think a // 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); 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) { pub fn center_column(&mut self) {
if self.floating_is_active.get() { if self.floating_is_active.get() {
self.floating.center_window(None); self.floating.center_window(None);
@@ -1336,7 +1343,6 @@ impl<W: LayoutElement> Workspace<W> {
&self, &self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> { ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
let scrolling = self.scrolling.tiles_with_render_positions(); 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 floating = self.floating.tiles_with_render_positions();
let visible = self.is_floating_visible(); let visible = self.is_floating_visible();
+15
View File
@@ -10,6 +10,7 @@ layout {
center-focused-column "never" center-focused-column "never"
always-center-single-column always-center-single-column
empty-workspace-above-first empty-workspace-above-first
default-column-display "tabbed"
preset-column-widths { preset-column-widths {
proportion 0.33333 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` ### `preset-column-widths`
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between. Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.