From e1fad994da9565b43c7fb139cb2fb7bf404cc320 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 2 Sep 2025 08:07:22 +0300 Subject: Implement maximize-to-edges (true Wayland maximize) --- src/layout/floating.rs | 13 +- src/layout/mod.rs | 73 ++++++++-- src/layout/scrolling.rs | 363 ++++++++++++++++++++++++++++++++++++------------ src/layout/tests.rs | 163 +++++++++++++++++++--- src/layout/tile.rs | 171 ++++++++++++++++------- src/layout/workspace.rs | 131 +++++++++++++---- 6 files changed, 722 insertions(+), 192 deletions(-) (limited to 'src/layout') diff --git a/src/layout/floating.rs b/src/layout/floating.rs index 260775d3..5ef0a265 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -413,8 +413,8 @@ impl FloatingSpace { // unfullscreen it. let floating_size = tile.floating_window_size; let win = tile.window_mut(); - let mut size = if win.is_pending_fullscreen() { - // If the window was fullscreen without a floating size, ask for (0, 0). + let mut size = if !win.pending_sizing_mode().is_normal() { + // If the window was fullscreen or maximized without a floating size, ask for (0, 0). floating_size.unwrap_or_default() } else { // If the window wasn't fullscreen without a floating size (e.g. it was tiled before), @@ -1312,6 +1312,8 @@ impl FloatingSpace { assert_eq!(self.tiles.len(), self.data.len()); for (i, (tile, data)) in zip(&self.tiles, &self.data).enumerate() { + use crate::layout::SizingMode; + assert!(Rc::ptr_eq(&self.options, &tile.options)); assert_eq!(self.view_size, tile.view_size()); assert_eq!(self.clock, tile.clock); @@ -1325,9 +1327,10 @@ impl FloatingSpace { assert!(idx < self.options.layout.preset_window_heights.len()); } - assert!( - !tile.window().is_pending_fullscreen(), - "floating windows cannot be fullscreen" + assert_eq!( + tile.window().pending_sizing_mode(), + SizingMode::Normal, + "floating windows cannot be maximized or fullscreen" ); data.verify_invariants(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 525b9eda..8761c0e3 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -118,6 +118,13 @@ niri_render_elements! { pub type LayoutElementRenderSnapshot = RenderSnapshot>, BakedBuffer>; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SizingMode { + Normal, + Maximized, + Fullscreen, +} + pub trait LayoutElement { /// Type that can be used as a unique ID of this element. type Id: PartialEq + std::fmt::Debug + Clone; @@ -185,14 +192,14 @@ pub trait LayoutElement { fn request_size( &mut self, size: Size, - is_fullscreen: bool, + mode: SizingMode, animate: bool, transaction: Option, ); /// Requests the element to change size once, clearing the request afterwards. fn request_size_once(&mut self, size: Size, animate: bool) { - self.request_size(size, false, animate, None); + self.request_size(size, SizingMode::Normal, animate, None); } fn min_size(&self) -> Size; @@ -214,15 +221,15 @@ pub trait LayoutElement { fn configure_intent(&self) -> ConfigureIntent; fn send_pending_configure(&mut self); - /// Whether the element is currently fullscreen. + /// The element's current sizing mode. /// /// This will *not* switch immediately after a [`LayoutElement::request_size()`] call. - fn is_fullscreen(&self) -> bool; + fn sizing_mode(&self) -> SizingMode; - /// Whether we're requesting the element to be fullscreen. + /// The sizing mode that we're requesting the element to assume. /// /// This *will* switch immediately after a [`LayoutElement::request_size()`] call. - fn is_pending_fullscreen(&self) -> bool; + fn pending_sizing_mode(&self) -> SizingMode; /// Size previously requested through [`LayoutElement::request_size()`]. fn requested_size(&self) -> Option>; @@ -240,7 +247,7 @@ pub trait LayoutElement { /// /// The default impl is for testing only, it will not preserve the window's own size changes. fn expected_size(&self) -> Option> { - if self.is_fullscreen() { + if self.sizing_mode().is_fullscreen() { return None; } @@ -502,6 +509,23 @@ struct OverviewGesture { value: f64, } +impl SizingMode { + #[must_use] + pub fn is_normal(&self) -> bool { + matches!(self, Self::Normal) + } + + #[must_use] + pub fn is_fullscreen(&self) -> bool { + matches!(self, Self::Fullscreen) + } + + #[must_use] + pub fn is_maximized(&self) -> bool { + matches!(self, Self::Maximized) + } +} + impl InteractiveMoveState { fn moving(&self) -> Option<&InteractiveMoveData> { match self { @@ -2321,7 +2345,7 @@ impl Layout { } InteractiveMoveState::Moving(move_) => { assert_eq!(self.clock, move_.tile.clock); - assert!(!move_.tile.window().is_pending_fullscreen()); + assert!(move_.tile.window().pending_sizing_mode().is_normal()); move_.tile.verify_invariants(); @@ -3456,7 +3480,7 @@ impl Layout { pub fn toggle_windowed_fullscreen(&mut self, id: &W::Id) { let (_, window) = self.windows().find(|(_, win)| win.id() == id).unwrap(); - if window.is_pending_fullscreen() { + if window.pending_sizing_mode().is_fullscreen() { // Remove the real fullscreen. for ws in self.workspaces_mut() { if ws.has_window(id) { @@ -3474,6 +3498,36 @@ impl Layout { }); } + pub fn set_maximized(&mut self, id: &W::Id, maximize: bool) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == id { + return; + } + } + + for ws in self.workspaces_mut() { + if ws.has_window(id) { + ws.set_maximized(id, maximize); + return; + } + } + } + + pub fn toggle_maximized(&mut self, id: &W::Id) { + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { + if move_.tile.window().id() == id { + return; + } + } + + for ws in self.workspaces_mut() { + if ws.has_window(id) { + ws.toggle_maximized(id); + return; + } + } + } + pub fn workspace_switch_gesture_begin(&mut self, output: &Output, is_touchpad: bool) { let monitors = match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => monitors, @@ -3845,6 +3899,7 @@ impl Layout { .find(|ws| ws.has_window(&window_id)) .unwrap(); ws.set_fullscreen(window, false); + ws.set_maximized(window, false); let RemovedTile { mut tile, diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 02541d1b..fc163d72 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -18,6 +18,7 @@ use super::workspace::{InteractiveResize, ResolvedSize}; use super::{ConfigureIntent, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile}; use crate::animation::{Animation, Clock}; use crate::input::swipe_tracker::SwipeTracker; +use crate::layout::SizingMode; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; @@ -62,8 +63,8 @@ pub struct ScrollingSpace { /// The value is the view offset that the previous column had before, to restore it. activate_prev_column_on_removal: Option, - /// View offset to restore after unfullscreening. - view_offset_before_fullscreen: Option, + /// View offset to restore after unfullscreening or unmaximizing. + view_offset_to_restore: Option, /// Windows in the closing animation. closing_windows: Vec, @@ -177,6 +178,13 @@ pub struct Column { /// take some time to catch up and actually unfullscreen. is_pending_fullscreen: bool, + /// Whether this column is going to be maximized. + /// + /// Can be `true` together with `is_pending_fullscreen`, which means that the column is + /// effectively pending fullscreen, but unfullscreening should go back to maximized state, + /// rather than normal. + is_pending_maximized: bool, + /// How this column displays and arranges windows. display_mode: ColumnDisplay, @@ -192,6 +200,11 @@ pub struct Column { /// Latest known working area for this column's workspace. working_area: Rectangle, + /// Working area for this column's workspace excluding struts. + /// + /// Used for maximize-to-edges. + parent_area: Rectangle, + /// Scale of the output the column is on (and rounds its sizes to). scale: f64, @@ -285,7 +298,7 @@ impl ScrollingSpace { interactive_resize: None, view_offset: ViewOffset::Static(0.), activate_prev_column_on_removal: None, - view_offset_before_fullscreen: None, + view_offset_to_restore: None, closing_windows: Vec::new(), view_size, working_area, @@ -306,7 +319,7 @@ impl ScrollingSpace { let working_area = compute_working_area(parent_area, scale, options.layout.struts); for (column, data) in zip(&mut self.columns, &mut self.data) { - column.update_config(view_size, working_area, scale, options.clone()); + column.update_config(view_size, working_area, parent_area, scale, options.clone()); data.update(column); } @@ -440,7 +453,7 @@ impl ScrollingSpace { } let col = &self.columns[self.active_column_idx]; - col.is_pending_fullscreen + col.pending_sizing_mode().is_fullscreen() } pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size { @@ -533,24 +546,25 @@ impl ScrollingSpace { target_x: Option, col_x: f64, width: f64, - is_fullscreen: bool, + mode: SizingMode, ) -> f64 { - if is_fullscreen { + if mode.is_fullscreen() { return 0.; } + let (area, padding) = if mode.is_maximized() { + (self.parent_area, 0.) + } else { + (self.working_area, self.options.layout.gaps) + }; + let target_x = target_x.unwrap_or_else(|| self.target_view_pos()); - let new_offset = compute_new_view_offset( - target_x + self.working_area.loc.x, - self.working_area.size.w, - col_x, - width, - self.options.layout.gaps, - ); + let new_offset = + compute_new_view_offset(target_x + area.loc.x, area.size.w, col_x, width, padding); // Non-fullscreen windows are always offset at least by the working area position. - new_offset - self.working_area.loc.x + new_offset - area.loc.x } fn compute_new_view_offset_centered( @@ -558,18 +572,24 @@ impl ScrollingSpace { target_x: Option, col_x: f64, width: f64, - is_fullscreen: bool, + mode: SizingMode, ) -> f64 { - if is_fullscreen { - return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); + if mode.is_fullscreen() { + return self.compute_new_view_offset_fit(target_x, col_x, width, mode); } + let area = if mode.is_maximized() { + self.parent_area + } else { + self.working_area + }; + // Columns wider than the view are left-aligned (the fit code can deal with that). - if self.working_area.size.w <= width { - return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen); + if area.size.w <= width { + return self.compute_new_view_offset_fit(target_x, col_x, width, mode); } - -(self.working_area.size.w - width) / 2. - self.working_area.loc.x + -(area.size.w - width) / 2. - area.loc.x } fn compute_new_view_offset_for_column_fit(&self, target_x: Option, idx: usize) -> f64 { @@ -578,7 +598,7 @@ impl ScrollingSpace { target_x, self.column_x(idx), col.width(), - col.is_fullscreen(), + col.sizing_mode(), ) } @@ -592,7 +612,7 @@ impl ScrollingSpace { target_x, self.column_x(idx), col.width(), - col.is_fullscreen(), + col.sizing_mode(), ) } @@ -633,6 +653,8 @@ impl ScrollingSpace { let target_col_x = self.column_x(idx); let target_col_width = self.columns[idx].width(); + // NOTE: This logic won't work entirely correctly with small fixed-size maximized + // windows (they have a different area and padding). let total_width = if source_col_x < target_col_x { // Source is left from target. target_col_x - source_col_x + target_col_width @@ -770,7 +792,7 @@ impl ScrollingSpace { // A different column was activated; reset the flag. self.activate_prev_column_on_removal = None; - self.view_offset_before_fullscreen = None; + self.view_offset_to_restore = None; self.interactive_resize = None; } } @@ -855,6 +877,7 @@ impl ScrollingSpace { tile, self.view_size, self.working_area, + self.parent_area, self.scale, width, is_full_width, @@ -956,6 +979,7 @@ impl ScrollingSpace { column.update_config( self.view_size, self.working_area, + self.parent_area, self.scale, self.options.clone(), ); @@ -1059,15 +1083,15 @@ impl ScrollingSpace { tile.animate_alpha(0., 1., movement_config); } - let was_fullscreen = column.is_fullscreen(); + let was_normal = column.sizing_mode().is_normal(); let tile = column.tiles.remove(tile_idx); column.data.remove(tile_idx); // If an active column became non-fullscreen after removing the tile, clear the stored // unfullscreen offset. - if column_idx == self.active_column_idx && was_fullscreen && !column.is_fullscreen() { - self.view_offset_before_fullscreen = None; + if column_idx == self.active_column_idx && !was_normal && column.sizing_mode().is_normal() { + self.view_offset_to_restore = None; } // If one window is left, reset its weight to 1. @@ -1171,7 +1195,7 @@ impl ScrollingSpace { } if column_idx == self.active_column_idx { - self.view_offset_before_fullscreen = None; + self.view_offset_to_restore = None; } if self.columns.is_empty() { @@ -1225,7 +1249,7 @@ impl ScrollingSpace { .enumerate() .find(|(_, col)| col.contains(window)) .unwrap(); - let was_fullscreen = column.is_fullscreen(); + let was_normal = column.sizing_mode().is_normal(); let prev_origin = column.tiles_origin(); let (tile_idx, tile) = column @@ -1337,9 +1361,9 @@ impl ScrollingSpace { } // When the active column goes fullscreen, store the view offset to restore later. - let is_fullscreen = self.columns[col_idx].is_fullscreen(); - if !was_fullscreen && is_fullscreen { - self.view_offset_before_fullscreen = Some(self.view_offset.stationary()); + let is_normal = self.columns[col_idx].sizing_mode().is_normal(); + if was_normal && !is_normal { + self.view_offset_to_restore = Some(self.view_offset.stationary()); } // Upon unfullscreening, restore the view offset. @@ -1348,11 +1372,11 @@ impl ScrollingSpace { // will unfullscreen one by one, and the column width will shrink only when the // last tile unfullscreens. This is when we want to restore the view offset, // otherwise it will immediately reset back by the animate_view_offset below. - let unfullscreen_offset = if was_fullscreen && !is_fullscreen { + let unfullscreen_offset = if !was_normal && is_normal { // Take the value unconditionally, even if the view is currently frozen by // a view gesture. It shouldn't linger around because it's only valid for this // particular unfullscreen. - self.view_offset_before_fullscreen.take() + self.view_offset_to_restore.take() } else { None }; @@ -2165,6 +2189,7 @@ impl ScrollingSpace { if col.display_mode != ColumnDisplay::Tabbed && col.tiles.len() > 1 { let window = col.tiles[col.active_tile_idx].window().id().clone(); self.set_fullscreen(&window, false); + self.set_maximized(&window, false); } } @@ -2307,6 +2332,10 @@ impl ScrollingSpace { iter::once(active_pos).chain(offsets) } + pub fn columns(&self) -> impl Iterator> { + self.columns.iter() + } + fn columns_mut(&mut self) -> impl Iterator, f64)> + '_ { let offsets = self.column_xs(self.data.iter().copied()); zip(&mut self.columns, offsets) @@ -2474,7 +2503,7 @@ impl ScrollingSpace { // Adjust for place-within-column tab indicator. let origin_x = col.tiles_origin().x; - let extra_w = if is_tabbed && !col.is_fullscreen() { + let extra_w = if is_tabbed && col.sizing_mode().is_normal() { col.tab_indicator.extra_size(col.tiles.len(), col.scale).w } else { 0. @@ -2491,9 +2520,14 @@ impl ScrollingSpace { // effect here. if self.columns.is_empty() { let view_offset = if self.is_centering_focused_column() { - self.compute_new_view_offset_centered(Some(0.), 0., hint_area.size.w, false) + self.compute_new_view_offset_centered( + Some(0.), + 0., + hint_area.size.w, + SizingMode::Normal, + ) } else { - self.compute_new_view_offset_fit(Some(0.), 0., hint_area.size.w, false) + self.compute_new_view_offset_fit(Some(0.), 0., hint_area.size.w, SizingMode::Normal) }; hint_area.loc.x -= view_offset; } else { @@ -2693,7 +2727,7 @@ impl ScrollingSpace { } let col = &mut self.columns[self.active_column_idx]; - if col.is_pending_fullscreen || col.is_full_width { + if !col.pending_sizing_mode().is_normal() || col.is_full_width { return; } @@ -2709,6 +2743,9 @@ impl ScrollingSpace { return; } + // NOTE: This logic won't work entirely correctly with small fixed-size maximized windows + // (they have a different area and padding). + // Consider the end of an ongoing animation because that's what compute to fit does too. let view_x = self.target_view_pos(); let working_x = self.working_area.loc.x; @@ -2812,6 +2849,37 @@ impl ScrollingSpace { true } + pub fn set_maximized(&mut self, window: &W::Id, maximize: bool) -> bool { + let mut col_idx = self + .columns + .iter() + .position(|col| col.contains(window)) + .unwrap(); + + if maximize == self.columns[col_idx].is_pending_maximized { + return false; + } + + let mut col = &mut self.columns[col_idx]; + let is_tabbed = col.display_mode == ColumnDisplay::Tabbed; + + cancel_resize_for_column(&mut self.interactive_resize, col); + + if maximize && (col.tiles.len() > 1 && !is_tabbed) { + // This wasn't the only window in its column; extract it into a separate column. + self.consume_or_expel_window_right(Some(window)); + col_idx += 1; + col = &mut self.columns[col_idx]; + } + + col.set_maximized(maximize); + + // With place_within_column, the tab indicator changes the column size immediately. + self.data[col_idx].update(col); + + true + } + pub fn render_above_top_layer(&self) -> bool { // Render above the top layer if we're on a fullscreen window and the view is stationary. if self.columns.is_empty() { @@ -2822,7 +2890,9 @@ impl ScrollingSpace { return false; } - self.columns[self.active_column_idx].is_fullscreen() + self.columns[self.active_column_idx] + .sizing_mode() + .is_fullscreen() } pub fn render_elements( @@ -2902,7 +2972,7 @@ impl ScrollingSpace { let col_render_off = col.render_offset(); // Hit the tab indicator. - if col.display_mode == ColumnDisplay::Tabbed && !col.is_fullscreen() { + if col.display_mode == ColumnDisplay::Tabbed && col.sizing_mode().is_normal() { let col_pos = view_off + col_off + col_render_off; let col_pos = col_pos.to_physical_precise_round(scale).to_logical(scale); @@ -3134,20 +3204,26 @@ impl ScrollingSpace { let mut snapping_points = Vec::new(); - let left_strut = self.working_area.loc.x; - let right_strut = self.view_size.w - self.working_area.size.w - self.working_area.loc.x; - if self.is_centering_focused_column() { let mut col_x = 0.; for (col_idx, col) in self.columns.iter().enumerate() { let col_w = col.width(); + let mode = col.sizing_mode(); - let view_pos = if col.is_fullscreen() { + let area = if mode.is_maximized() { + self.parent_area + } else { + self.working_area + }; + + let left_strut = area.loc.x; + + let view_pos = if mode.is_fullscreen() { col_x - } else if self.working_area.size.w <= col_w { + } else if area.size.w <= col_w { col_x - left_strut } else { - col_x - (self.working_area.size.w - col_w) / 2. - left_strut + col_x - (area.size.w - col_w) / 2. - left_strut }; snapping_points.push(Snap { view_pos, col_idx }); @@ -3160,34 +3236,50 @@ impl ScrollingSpace { ); let view_width = self.view_size.w; - let working_area_width = self.working_area.size.w; let gaps = self.options.layout.gaps; let snap_points = |col_x, col: &Column, prev_col_w: Option, next_col_w: Option| { let col_w = col.width(); + let mode = col.sizing_mode(); + + let area = if mode.is_maximized() { + self.parent_area + } else { + self.working_area + }; + + let left_strut = area.loc.x; + let right_strut = self.view_size.w - area.size.w - area.loc.x; // Normal columns align with the working area, but fullscreen columns align with // the view size. - if col.is_fullscreen() { + if mode.is_fullscreen() { let left = col_x; - let right = col_x + col_w; + let right = left + col_w; (left, right) } else { // Logic from compute_new_view_offset. - let padding = ((working_area_width - col_w) / 2.).clamp(0., gaps); + let padding = if mode.is_maximized() { + 0. + } else { + ((area.size.w - col_w) / 2.).clamp(0., gaps) + }; - let center = if self.working_area.size.w <= col_w { + let center = if area.size.w <= col_w { col_x - left_strut } else { - col_x - (self.working_area.size.w - col_w) / 2. - left_strut + col_x - (area.size.w - col_w) / 2. - left_strut }; let is_overflowing = |adj_col_w: Option| { center_on_overflow && adj_col_w .filter(|adj_col_w| { + // NOTE: This logic won't work entirely correctly with small + // fixed-size maximized windows (they have a different area + // and padding). center_on_overflow - && adj_col_w + 3.0 * gaps + col_w > working_area_width + && adj_col_w + 3.0 * gaps + col_w > area.size.w }) .is_some() }; @@ -3211,6 +3303,14 @@ impl ScrollingSpace { // // It's ok if leftmost_snap is > rightmost_snap (this happens if the columns on a // workspace total up to less than the workspace width). + + // The first column's left snap isn't actually guaranteed to be the *leftmost* snap. + // With weird enough left strut and perhaps a maximized small fixed-size window, you + // can make the second window's left snap be further to the left than the first + // window's. The same goes for the rightmost snap. + // + // This isn't actually a big problem because it's very much an obscure edge case. Just + // need to make sure the code doesn't panic when that happens. let leftmost_snap = snap_points( 0., &self.columns[0], @@ -3295,16 +3395,28 @@ impl ScrollingSpace { let col = &self.columns[col_idx]; let col_x = self.column_x(col_idx); let col_w = col.width(); + let mode = col.sizing_mode(); + + let area = if mode.is_maximized() { + self.parent_area + } else { + self.working_area + }; - if col.is_fullscreen() { + let left_strut = area.loc.x; + + if mode.is_fullscreen() { if target_snap.view_pos + self.view_size.w < col_x + col_w { break; } } else { - let padding = ((self.working_area.size.w - col_w) / 2.) - .clamp(0., self.options.layout.gaps); - if target_snap.view_pos + left_strut + self.working_area.size.w - < col_x + col_w + padding + let padding = if mode.is_maximized() { + 0. + } else { + ((area.size.w - col_w) / 2.).clamp(0., self.options.layout.gaps) + }; + + if target_snap.view_pos + left_strut + area.size.w < col_x + col_w + padding { break; } @@ -3317,14 +3429,27 @@ impl ScrollingSpace { let col = &self.columns[col_idx]; let col_x = self.column_x(col_idx); let col_w = col.width(); + let mode = col.sizing_mode(); - if col.is_fullscreen() { + let area = if mode.is_maximized() { + self.parent_area + } else { + self.working_area + }; + + let left_strut = area.loc.x; + + if mode.is_fullscreen() { if col_x < target_snap.view_pos { break; } } else { - let padding = ((self.working_area.size.w - col_w) / 2.) - .clamp(0., self.options.layout.gaps); + let padding = if mode.is_maximized() { + 0. + } else { + ((area.size.w - col_w) / 2.).clamp(0., self.options.layout.gaps) + }; + if col_x - padding < target_snap.view_pos + left_strut { break; } @@ -3339,7 +3464,7 @@ impl ScrollingSpace { let delta = active_col_x - new_col_x; if self.active_column_idx != new_col_idx { - self.view_offset_before_fullscreen = None; + self.view_offset_to_restore = None; } self.active_column_idx = new_col_idx; @@ -3391,7 +3516,7 @@ impl ScrollingSpace { .find(|col| col.contains(&window)) .unwrap(); - if col.is_pending_fullscreen { + if !col.pending_sizing_mode().is_normal() { return false; } @@ -3637,11 +3762,11 @@ impl ScrollingSpace { let col = &self.columns[self.active_column_idx]; - if self.view_offset_before_fullscreen.is_some() { + if self.view_offset_to_restore.is_some() { assert!( - col.is_fullscreen(), - "when view_offset_before_fullscreen is set, \ - the active column must be fullscreen" + !col.sizing_mode().is_normal(), + "when view_offset_to_restore is set, \ + the active column must be fullscreen or maximized" ); } } @@ -3796,6 +3921,7 @@ impl Column { tile: Tile, view_size: Size, working_area: Rectangle, + parent_area: Rectangle, scale: f64, width: ColumnWidth, is_full_width: bool, @@ -3815,29 +3941,33 @@ impl Column { width, preset_width_idx: None, is_full_width, + is_pending_maximized: false, is_pending_fullscreen: false, display_mode, tab_indicator: TabIndicator::new(options.layout.tab_indicator), move_animation: None, view_size, working_area, + parent_area, scale, clock: tile.clock.clone(), options, }; - let is_pending_fullscreen = tile.window().is_pending_fullscreen(); + let pending_sizing_mode = tile.window().pending_sizing_mode(); rv.add_tile_at(0, tile); - if is_pending_fullscreen { - rv.set_fullscreen(true); + match pending_sizing_mode { + SizingMode::Normal => (), + SizingMode::Maximized => rv.set_maximized(true), + SizingMode::Fullscreen => rv.set_fullscreen(true), } // Animate the tab indicator for new columns. if display_mode == ColumnDisplay::Tabbed && !rv.options.layout.tab_indicator.hide_when_single_tab - && !rv.is_fullscreen() + && rv.sizing_mode().is_normal() { // Usually new columns are created together with window movement actions. For new // windows, we handle that in start_open_animation(). @@ -3852,12 +3982,16 @@ impl Column { &mut self, view_size: Size, working_area: Rectangle, + parent_area: Rectangle, scale: f64, options: Rc, ) { let mut update_sizes = false; - if self.view_size != view_size || self.working_area != working_area { + if self.view_size != view_size + || self.working_area != working_area + || self.parent_area != parent_area + { update_sizes = true; } @@ -3895,6 +4029,7 @@ impl Column { .update_config(options.layout.tab_indicator); self.view_size = view_size; self.working_area = working_area; + self.parent_area = parent_area; self.scale = scale; self.options = options; @@ -3954,7 +4089,7 @@ impl Column { // you don't want that to happen in fullscreen. Also, laying things out correctly when the // tab indicator is within the column and the column goes fullscreen, would require too // many changes to the code for too little benefit (it's mostly invisible anyway). - let enabled = self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen(); + let enabled = self.display_mode == ColumnDisplay::Tabbed && self.sizing_mode().is_normal(); self.tab_indicator.update_render_elements( enabled, @@ -3967,6 +4102,24 @@ impl Column { ); } + pub fn is_pending_fullscreen(&self) -> bool { + self.is_pending_fullscreen + } + + pub fn is_pending_maximized(&self) -> bool { + self.is_pending_maximized + } + + pub fn pending_sizing_mode(&self) -> SizingMode { + if self.is_pending_fullscreen { + SizingMode::Fullscreen + } else if self.is_pending_maximized { + SizingMode::Maximized + } else { + SizingMode::Normal + } + } + pub fn render_offset(&self) -> Point { let mut offset = Point::from((0., 0.)); @@ -4040,7 +4193,7 @@ impl Column { /// - is_fullscreen() can suddenly change when consuming/expelling a fullscreen tile into/from a /// non-fullscreen column. This can influence the code that saves/restores the unfullscreen /// view offset. - fn is_fullscreen(&self) -> bool { + fn sizing_mode(&self) -> SizingMode { // Behaviors that we want: // // 1. The common case: single tile in a column. Assume no animations. Fullscreening the tile @@ -4058,7 +4211,23 @@ impl Column { // mode change applies instantly). // // The logic that satisfies these behaviors is to check if *any* tile is fullscreen. - self.tiles.iter().any(|tile| tile.is_fullscreen()) + let mut any_fullscreen = false; + let mut any_maximized = false; + for tile in &self.tiles { + match tile.sizing_mode() { + SizingMode::Normal => (), + SizingMode::Maximized => any_maximized = true, + SizingMode::Fullscreen => any_fullscreen = true, + } + } + + if any_fullscreen { + SizingMode::Fullscreen + } else if any_maximized { + SizingMode::Maximized + } else { + SizingMode::Normal + } } pub fn contains(&self, window: &W::Id) -> bool { @@ -4102,6 +4271,7 @@ impl Column { if self.display_mode != ColumnDisplay::Tabbed { self.is_pending_fullscreen = false; + self.is_pending_maximized = false; } self.data @@ -4213,7 +4383,8 @@ impl Column { } fn update_tile_sizes_with_transaction(&mut self, animate: bool, transaction: Transaction) { - if self.is_pending_fullscreen { + let sizing_mode = self.pending_sizing_mode(); + if matches!(sizing_mode, SizingMode::Fullscreen | SizingMode::Maximized) { for (tile_idx, tile) in self.tiles.iter_mut().enumerate() { // In tabbed mode, only the visible window participates in the transaction. let is_active = tile_idx == self.active_tile_idx; @@ -4223,7 +4394,11 @@ impl Column { Some(transaction.clone()) }; - tile.request_fullscreen(animate, transaction); + if matches!(sizing_mode, SizingMode::Fullscreen) { + tile.request_fullscreen(animate, transaction); + } else { + tile.request_maximized(self.parent_area.size, animate, transaction); + } } return; } @@ -4510,7 +4685,7 @@ impl Column { .map(NotNan::into_inner) .unwrap(); - if self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen() { + if self.display_mode == ColumnDisplay::Tabbed && self.sizing_mode().is_normal() { let extra_size = self.tab_indicator.extra_size(self.tiles.len(), self.scale); tiles_width += extra_size.w; } @@ -4875,6 +5050,19 @@ impl Column { self.update_tile_sizes(true); } + fn set_maximized(&mut self, maximize: bool) { + if self.is_pending_maximized == maximize { + return; + } + + if maximize { + assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed); + } + + self.is_pending_maximized = maximize; + self.update_tile_sizes(true); + } + fn set_column_display(&mut self, display: ColumnDisplay) { if self.display_mode == display { return; @@ -4936,8 +5124,13 @@ impl Column { fn tiles_origin(&self) -> Point { let mut origin = Point::from((0., 0.)); - if self.is_fullscreen() { - return origin; + match self.sizing_mode() { + SizingMode::Normal => (), + SizingMode::Maximized => { + origin.y += self.parent_area.loc.y; + return origin; + } + SizingMode::Fullscreen => return origin, } origin.y += self.working_area.loc.y + self.options.layout.gaps; @@ -5096,7 +5289,7 @@ impl Column { // Animate the appearance of the tab indicator. if self.display_mode == ColumnDisplay::Tabbed - && !self.is_fullscreen() + && self.sizing_mode().is_normal() && self.tiles.len() == 1 && !self.tab_indicator.config().hide_when_single_tab { @@ -5119,7 +5312,7 @@ impl Column { assert!(self.active_tile_idx < self.tiles.len()); assert_eq!(self.tiles.len(), self.data.len()); - if self.is_pending_fullscreen { + if !self.pending_sizing_mode().is_normal() { assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed); } @@ -5151,8 +5344,8 @@ impl Column { assert_eq!(self.clock, tile.clock); assert_eq!(self.scale, tile.scale()); assert_eq!( - self.is_pending_fullscreen, - tile.window().is_pending_fullscreen() + self.pending_sizing_mode(), + tile.window().pending_sizing_mode() ); assert_eq!(self.view_size, tile.view_size()); tile.verify_invariants(); @@ -5178,7 +5371,7 @@ impl Column { tile.tile_height_for_window_height(f64::from(requested_size.h)); let min_tile_height = f64::max(1., tile.min_size_nonfullscreen().h); - if !self.is_pending_fullscreen + if self.pending_sizing_mode().is_normal() && 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 2dc989fd..ced647b6 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -33,9 +33,9 @@ struct TestWindowInner { forced_size: Cell>>, min_size: Size, max_size: Size, - pending_fullscreen: Cell, + pending_sizing_mode: Cell, pending_activated: Cell, - is_fullscreen: Cell, + sizing_mode: Cell, is_windowed_fullscreen: Cell, is_pending_windowed_fullscreen: Cell, animate_next_configure: Cell, @@ -81,9 +81,9 @@ impl TestWindow { forced_size: Cell::new(None), min_size: params.min_max_size.0, max_size: params.min_max_size.1, - pending_fullscreen: Cell::new(false), + pending_sizing_mode: Cell::new(SizingMode::Normal), pending_activated: Cell::new(false), - is_fullscreen: Cell::new(false), + sizing_mode: Cell::new(SizingMode::Normal), is_windowed_fullscreen: Cell::new(false), is_pending_windowed_fullscreen: Cell::new(false), animate_next_configure: Cell::new(false), @@ -126,8 +126,8 @@ impl TestWindow { self.0.animate_next_configure.set(false); - if self.0.is_fullscreen.get() != self.0.pending_fullscreen.get() { - self.0.is_fullscreen.set(self.0.pending_fullscreen.get()); + if self.0.sizing_mode.get() != self.0.pending_sizing_mode.get() { + self.0.sizing_mode.set(self.0.pending_sizing_mode.get()); changed = true; } @@ -175,7 +175,7 @@ impl LayoutElement for TestWindow { fn request_size( &mut self, size: Size, - is_fullscreen: bool, + mode: SizingMode, _animate: bool, _transaction: Option, ) { @@ -184,9 +184,9 @@ impl LayoutElement for TestWindow { self.0.animate_next_configure.set(true); } - self.0.pending_fullscreen.set(is_fullscreen); + self.0.pending_sizing_mode.set(mode); - if is_fullscreen { + if mode.is_fullscreen() { self.0.is_pending_windowed_fullscreen.set(false); } } @@ -235,20 +235,12 @@ impl LayoutElement for TestWindow { fn set_floating(&mut self, _floating: bool) {} - fn is_fullscreen(&self) -> bool { - if self.0.is_windowed_fullscreen.get() { - return false; - } - - self.0.is_fullscreen.get() + fn sizing_mode(&self) -> SizingMode { + self.0.sizing_mode.get() } - fn is_pending_fullscreen(&self) -> bool { - if self.0.is_pending_windowed_fullscreen.get() { - return false; - } - - self.0.pending_fullscreen.get() + fn pending_sizing_mode(&self) -> SizingMode { + self.0.pending_sizing_mode.get() } fn requested_size(&self) -> Option> { @@ -585,6 +577,10 @@ enum Op { id: Option, }, MaximizeColumn, + MaximizeWindowToEdges { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + id: Option, + }, SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange), SetWindowWidth { #[proptest(strategy = "proptest::option::of(1..=5usize)")] @@ -1310,6 +1306,16 @@ impl Op { layout.toggle_window_height(id.as_ref(), false); } Op::MaximizeColumn => layout.toggle_full_width(), + Op::MaximizeWindowToEdges { id } => { + let id = id.or_else(|| layout.focus().map(|win| *win.id())); + let Some(id) = id else { + return; + }; + if !layout.has_window(&id) { + return; + } + layout.toggle_maximized(&id); + } Op::SetColumnWidth(change) => layout.set_column_width(change), Op::SetWindowWidth { id, change } => { let id = id.filter(|id| layout.has_window(id)); @@ -1674,6 +1680,9 @@ fn operations_dont_panic() { Op::FullscreenWindow(1), Op::FullscreenWindow(2), Op::FullscreenWindow(3), + Op::MaximizeWindowToEdges { id: Some(1) }, + Op::MaximizeWindowToEdges { id: Some(2) }, + Op::MaximizeWindowToEdges { id: Some(3) }, Op::FocusColumnLeft, Op::FocusColumnRight, Op::FocusColumnRightOrFirst, @@ -1829,6 +1838,9 @@ fn operations_from_starting_state_dont_panic() { Op::FullscreenWindow(1), Op::FullscreenWindow(2), Op::FullscreenWindow(3), + Op::MaximizeWindowToEdges { id: Some(1) }, + Op::MaximizeWindowToEdges { id: Some(2) }, + Op::MaximizeWindowToEdges { id: Some(3) }, Op::SetFullscreenWindow { window: 1, is_fullscreen: false, @@ -3492,6 +3504,115 @@ fn move_column_to_workspace_focus_false_on_floating_window() { assert_eq!(monitors[0].active_workspace_idx, 0); } +#[test] +fn restore_to_floating_persists_across_fullscreen_maximize() { + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + params: TestWindowParams::new(1), + }, + Op::ToggleWindowFloating { id: None }, + // Maximize then fullscreen. + Op::MaximizeWindowToEdges { id: None }, + Op::FullscreenWindow(1), + // Unfullscreen. + Op::FullscreenWindow(1), + ]; + + let mut layout = check_ops(ops); + + // Unfullscreening should return the window to the maximized state. + let scrolling = layout.active_workspace().unwrap().scrolling(); + assert!(scrolling.tiles().next().is_some()); + + let ops = [ + // Unmaximize. + Op::MaximizeWindowToEdges { id: None }, + ]; + check_ops_on_layout(&mut layout, ops); + + // Unmaximize should return the window back to floating. + let scrolling = layout.active_workspace().unwrap().scrolling(); + assert!(scrolling.tiles().next().is_none()); +} + +#[test] +fn unmaximize_during_fullscreen_does_not_float() { + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + params: TestWindowParams::new(1), + }, + Op::ToggleWindowFloating { id: None }, + // Maximize then fullscreen. + Op::MaximizeWindowToEdges { id: None }, + Op::FullscreenWindow(1), + // Unmaximize. + Op::MaximizeWindowToEdges { id: None }, + ]; + + let mut layout = check_ops(ops); + + // Unmaximize shouldn't have changed the window state since it's fullscreen. + let scrolling = layout.active_workspace().unwrap().scrolling(); + assert!(scrolling.tiles().next().is_some()); + + let ops = [ + // Unfullscreen. + Op::FullscreenWindow(1), + ]; + check_ops_on_layout(&mut layout, ops); + + // Unfullscreen should return the window back to floating. + let scrolling = layout.active_workspace().unwrap().scrolling(); + assert!(scrolling.tiles().next().is_none()); +} + +#[test] +fn move_column_to_workspace_maximize_and_fullscreen() { + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + params: TestWindowParams::new(1), + }, + Op::MaximizeWindowToEdges { id: None }, + Op::FullscreenWindow(1), + Op::MoveColumnToWorkspaceDown(true), + Op::FullscreenWindow(1), + ]; + + let layout = check_ops(ops); + let (_, win) = layout.windows().next().unwrap(); + + // Unfullscreening should return to maximized because the window was maximized before. + assert_eq!(win.pending_sizing_mode(), SizingMode::Maximized); +} + +#[test] +fn move_window_to_workspace_maximize_and_fullscreen() { + let ops = [ + Op::AddOutput(1), + Op::AddWindow { + params: TestWindowParams::new(1), + }, + Op::MaximizeWindowToEdges { id: None }, + Op::FullscreenWindow(1), + Op::MoveWindowToWorkspaceDown(true), + Op::FullscreenWindow(1), + ]; + + let layout = check_ops(ops); + let (_, win) = layout.windows().next().unwrap(); + + // Unfullscreening should return to maximized because the window was maximized before. + // + // FIXME: it currently doesn't because windows themselves can only be either fullscreen or + // maximized. So when a window is fullscreen, whether it is also maximized or not is stored in + // the column. MoveWindowToWorkspace removes the window from the column and this information is + // forgotten. + assert_eq!(win.pending_sizing_mode(), SizingMode::Normal); +} + fn parent_id_causes_loop(layout: &Layout, id: usize, mut parent_id: usize) -> bool { if parent_id == id { return true; diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 1e6f1a21..bc39db6d 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -16,6 +16,7 @@ use super::{ SizeFrac, RESIZE_ANIMATION_THRESHOLD, }; use crate::animation::{Animation, Clock}; +use crate::layout::SizingMode; use crate::niri_render_elements; use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage}; @@ -45,17 +46,17 @@ pub struct Tile { /// The shadow around the window. shadow: Shadow, - /// Whether this tile is fullscreen. + /// This tile's current sizing mode. /// - /// This will update only when the `window` actually goes fullscreen, rather than right away, - /// to avoid black backdrop flicker before the window has had a chance to resize. - is_fullscreen: bool, + /// This will update only when the `window` actually goes maximized or fullscreen, rather than + /// right away, to avoid black backdrop flicker before the window has had a chance to resize. + sizing_mode: SizingMode, /// The black backdrop for fullscreen windows. fullscreen_backdrop: SolidColorBuffer, /// Whether the tile should float upon unfullscreening. - pub(super) unfullscreen_to_floating: bool, + pub(super) restore_to_floating: bool, /// The size that the window should assume when going floating. /// @@ -146,6 +147,8 @@ struct ResizeAnimation { // Note that this can be set even if this specific resize is between two non-fullscreen states, // for example when issuing a new resize during an unfullscreen resize. fullscreen_progress: Option, + // Similar to above but for fullscreen-or-maximized. + expanded_progress: Option, } #[derive(Debug)] @@ -178,16 +181,16 @@ impl Tile { let border_config = options.layout.border.merged_with(&rules.border); let focus_ring_config = options.layout.focus_ring.merged_with(&rules.focus_ring); let shadow_config = options.layout.shadow.merged_with(&rules.shadow); - let is_fullscreen = window.is_fullscreen(); + let sizing_mode = window.sizing_mode(); Self { window, border: FocusRing::new(border_config.into()), focus_ring: FocusRing::new(focus_ring_config), shadow: Shadow::new(shadow_config), - is_fullscreen, + sizing_mode, fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]), - unfullscreen_to_floating: false, + restore_to_floating: false, floating_window_size: None, floating_pos: None, floating_preset_width_idx: None, @@ -248,8 +251,8 @@ impl Tile { } pub fn update_window(&mut self) { - let was_fullscreen = self.is_fullscreen; - self.is_fullscreen = self.window.is_fullscreen(); + let prev_sizing_mode = self.sizing_mode; + self.sizing_mode = self.window.sizing_mode(); if let Some(animate_from) = self.window.take_animation_snapshot() { let params = if let Some(resize) = self.resize_animation.take() { @@ -265,10 +268,10 @@ impl Tile { size.h = size_from.h + (size.h - size_from.h) * val; let mut tile_size = animate_from.size; - if was_fullscreen { + if prev_sizing_mode.is_fullscreen() { tile_size.w = f64::max(tile_size.w, self.view_size.w); tile_size.h = f64::max(tile_size.h, self.view_size.h); - } else if !self.border.is_off() { + } else if prev_sizing_mode.is_normal() && !self.border.is_off() { let width = self.border.width(); tile_size.w += width * 2.; tile_size.h += width * 2.; @@ -280,29 +283,56 @@ impl Tile { let fullscreen_from = resize .fullscreen_progress .map(|anim| anim.clamped_value().clamp(0., 1.)) - .unwrap_or(if was_fullscreen { 1. } else { 0. }); + .unwrap_or(if prev_sizing_mode.is_fullscreen() { + 1. + } else { + 0. + }); + + let expanded_from = resize + .expanded_progress + .map(|anim| anim.clamped_value().clamp(0., 1.)) + .unwrap_or(if prev_sizing_mode.is_normal() { 0. } else { 1. }); // Also try to reuse the existing offscreen buffer if we have one. - (size, tile_size, fullscreen_from, resize.offscreen) + ( + size, + tile_size, + fullscreen_from, + expanded_from, + resize.offscreen, + ) } else { let size = animate_from.size; // Compute like in tile_size(). let mut tile_size = size; - if was_fullscreen { + if prev_sizing_mode.is_fullscreen() { tile_size.w = f64::max(tile_size.w, self.view_size.w); tile_size.h = f64::max(tile_size.h, self.view_size.h); - } else if !self.border.is_off() { + } else if prev_sizing_mode.is_normal() && !self.border.is_off() { let width = self.border.width(); tile_size.w += width * 2.; tile_size.h += width * 2.; } - let fullscreen_from = if was_fullscreen { 1. } else { 0. }; + let fullscreen_from = if prev_sizing_mode.is_fullscreen() { + 1. + } else { + 0. + }; + + let expanded_from = if prev_sizing_mode.is_normal() { 0. } else { 1. }; - (size, tile_size, fullscreen_from, OffscreenBuffer::default()) + ( + size, + tile_size, + fullscreen_from, + expanded_from, + OffscreenBuffer::default(), + ) }; - let (size_from, tile_size_from, fullscreen_from, offscreen) = params; + let (size_from, tile_size_from, fullscreen_from, expanded_from, offscreen) = params; let change = self.window.size().to_f64().to_point() - size_from.to_point(); let change = f64::max(change.x.abs(), change.y.abs()); @@ -318,9 +348,16 @@ impl Tile { self.options.animations.window_resize.anim, ); - let fullscreen_to = if self.is_fullscreen { 1. } else { 0. }; + let fullscreen_to = if self.sizing_mode.is_fullscreen() { + 1. + } else { + 0. + }; + let expanded_to = if self.sizing_mode.is_normal() { 0. } else { 1. }; let fullscreen_progress = (fullscreen_from != fullscreen_to) .then(|| anim.restarted(fullscreen_from, fullscreen_to, 0.)); + let expanded_progress = (expanded_from != expanded_to) + .then(|| anim.restarted(expanded_from, expanded_to, 0.)); self.resize_animation = Some(ResizeAnimation { anim, @@ -329,6 +366,7 @@ impl Tile { offscreen, tile_size_from, fullscreen_progress, + expanded_progress, }); } else { self.resize_animation = None; @@ -406,7 +444,7 @@ impl Tile { pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle) { let rules = self.window.rules(); let animated_tile_size = self.animated_tile_size(); - let fullscreen_progress = self.fullscreen_progress(); + let expanded_progress = self.expanded_progress(); let draw_border_with_background = rules .draw_border_with_background @@ -425,7 +463,7 @@ impl Tile { .map_or(CornerRadius::default(), |radius| { radius.expanded_by(border_width as f32) }) - .scaled_by(1. - fullscreen_progress as f32); + .scaled_by(1. - expanded_progress as f32); self.border.update_render_elements( border_window_size, is_active, @@ -437,7 +475,7 @@ impl Tile { ), radius, self.scale, - 1. - fullscreen_progress as f32, + 1. - expanded_progress as f32, ); let radius = if self.visual_border_width().is_some() { @@ -446,17 +484,17 @@ impl Tile { rules .geometry_corner_radius .unwrap_or_default() - .scaled_by(1. - fullscreen_progress as f32) + .scaled_by(1. - expanded_progress as f32) }; self.shadow.update_render_elements( animated_tile_size, is_active, radius, self.scale, - 1. - fullscreen_progress as f32, + 1. - expanded_progress as f32, ); - let draw_focus_ring_with_background = if self.border.is_off() && fullscreen_progress < 1. { + let draw_focus_ring_with_background = if self.border.is_off() { draw_border_with_background } else { false @@ -470,7 +508,7 @@ impl Tile { view_rect, radius, self.scale, - 1. - fullscreen_progress as f32, + 1. - expanded_progress as f32, ); self.fullscreen_backdrop.resize(animated_tile_size); @@ -609,8 +647,8 @@ impl Tile { &mut self.window } - pub fn is_fullscreen(&self) -> bool { - self.is_fullscreen + pub fn sizing_mode(&self) -> SizingMode { + self.sizing_mode } fn fullscreen_progress(&self) -> f64 { @@ -620,16 +658,30 @@ impl Tile { } } - if self.is_fullscreen { + if self.sizing_mode.is_fullscreen() { 1. } else { 0. } } + fn expanded_progress(&self) -> f64 { + if let Some(resize) = &self.resize_animation { + if let Some(anim) = &resize.expanded_progress { + return anim.clamped_value().clamp(0., 1.); + } + } + + if self.sizing_mode.is_normal() { + 0. + } else { + 1. + } + } + /// Returns `None` if the border is hidden and `Some(width)` if it should be shown. pub fn effective_border_width(&self) -> Option { - if self.is_fullscreen { + if !self.sizing_mode.is_normal() { return None; } @@ -645,10 +697,10 @@ impl Tile { return None; } - let fullscreen_progress = self.fullscreen_progress(); + let expanded_progress = self.expanded_progress(); - // Only hide the border when fully fullscreen to avoid jarring border appearance. - if fullscreen_progress == 1. { + // Only hide the border when fully expanded to avoid jarring border appearance. + if expanded_progress == 1. { return None; } @@ -688,7 +740,7 @@ impl Tile { pub fn tile_size(&self) -> Size { let mut size = self.window_size(); - if self.is_fullscreen { + if self.sizing_mode.is_fullscreen() { // Normally we'd just return the fullscreen size here, but this makes things a bit // nicer if a fullscreen window is bigger than the fullscreen size for some reason. size.w = f64::max(size.w, self.view_size.w); @@ -707,7 +759,7 @@ impl Tile { pub fn tile_expected_or_current_size(&self) -> Size { let mut size = self.window_expected_or_current_size(); - if self.is_fullscreen { + if self.sizing_mode.is_fullscreen() { // Normally we'd just return the fullscreen size here, but this makes things a bit // nicer if a fullscreen window is bigger than the fullscreen size for some reason. size.w = f64::max(size.w, self.view_size.w); @@ -836,8 +888,12 @@ impl Tile { // The size request has to be i32 unfortunately, due to Wayland. We floor here instead of // round to avoid situations where proportionally-sized columns don't fit on the screen // exactly. - self.window - .request_size(size.to_i32_floor(), false, animate, transaction); + self.window.request_size( + size.to_i32_floor(), + SizingMode::Normal, + animate, + transaction, + ); } pub fn tile_width_for_window_width(&self, size: f64) -> f64 { @@ -872,9 +928,27 @@ impl Tile { } } + pub fn request_maximized( + &mut self, + size: Size, + animate: bool, + transaction: Option, + ) { + self.window.request_size( + size.to_i32_round(), + SizingMode::Maximized, + animate, + transaction, + ); + } + pub fn request_fullscreen(&mut self, animate: bool, transaction: Option