From c832bdecdda44648542f60dab73904f3e80dc857 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 2 Sep 2025 14:16:39 +0300 Subject: layout/scrolling: Track pending vs. current fullscreen for Columns We already did that for Tiles, but for Columns we only tracked what was effectively pending fullscreen. We used it in several places where the current fullscreen should've been used instead, like the tile origin or the view offset. This commit splits the two and makes every place use the right one. Fixes things like tiles briefly appearing at y=0 between issuing the fullscreen command and the tile committing in response to the fullscreen configure. --- src/layout/scrolling.rs | 125 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 35 deletions(-) (limited to 'src/layout/scrolling.rs') diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 20e4c388..48773110 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -166,8 +166,15 @@ pub struct Column { /// Whether this column is full-width. is_full_width: bool, - /// Whether this column contains a single full-screened window. - is_fullscreen: bool, + /// Whether this column is going to be fullscreen. + /// + /// This is the compositor-side fullscreen state, so it changes immediately upon + /// set_fullscreen(). The actual tiles will take some time to respond to the fullscreen request + /// and become fullscreen. + /// + /// Similarly, unsetting fullscreen will change this value to false immediately, and tiles will + /// take some time to catch up and actually unfullscreen. + is_pending_fullscreen: bool, /// How this column displays and arranges windows. display_mode: ColumnDisplay, @@ -426,13 +433,13 @@ impl ScrollingSpace { Some(&mut col.tiles[col.active_tile_idx]) } - pub fn is_active_fullscreen(&self) -> bool { + pub fn is_active_pending_fullscreen(&self) -> bool { if self.columns.is_empty() { return false; } let col = &self.columns[self.active_column_idx]; - col.is_fullscreen + col.is_pending_fullscreen } pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size { @@ -570,7 +577,7 @@ impl ScrollingSpace { target_x, self.column_x(idx), col.width(), - col.is_fullscreen, + col.is_fullscreen(), ) } @@ -584,7 +591,7 @@ impl ScrollingSpace { target_x, self.column_x(idx), col.width(), - col.is_fullscreen, + col.is_fullscreen(), ) } @@ -1052,14 +1059,14 @@ impl ScrollingSpace { tile.animate_alpha(0., 1., movement_config); } - let was_fullscreen = column.is_any_fullscreen(); + let was_fullscreen = column.is_fullscreen(); 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_any_fullscreen() { + if column_idx == self.active_column_idx && was_fullscreen && !column.is_fullscreen() { self.view_offset_before_fullscreen = None; } @@ -1218,7 +1225,7 @@ impl ScrollingSpace { .enumerate() .find(|(_, col)| col.contains(window)) .unwrap(); - let was_fullscreen = column.is_any_fullscreen(); + let was_fullscreen = column.is_fullscreen(); let (tile_idx, tile) = column .tiles @@ -1317,7 +1324,7 @@ impl ScrollingSpace { } // When the active column goes fullscreen, store the view offset to restore later. - let is_fullscreen = self.columns[col_idx].is_any_fullscreen(); + let is_fullscreen = self.columns[col_idx].is_fullscreen(); if !was_fullscreen && is_fullscreen { self.view_offset_before_fullscreen = Some(self.view_offset.stationary()); } @@ -2450,7 +2457,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.is_fullscreen() { col.tab_indicator.extra_size(col.tiles.len(), col.scale).w } else { 0. @@ -2669,7 +2676,7 @@ impl ScrollingSpace { } let col = &mut self.columns[self.active_column_idx]; - if col.is_fullscreen || col.is_full_width { + if col.is_pending_fullscreen || col.is_full_width { return; } @@ -2765,7 +2772,7 @@ impl ScrollingSpace { .find_map(|(col_idx, col)| col.position(window).map(|tile_idx| (col_idx, tile_idx))) .unwrap(); - if is_fullscreen == self.columns[col_idx].is_fullscreen { + if is_fullscreen == self.columns[col_idx].is_pending_fullscreen { return false; } @@ -2813,7 +2820,7 @@ impl ScrollingSpace { return false; } - self.columns[self.active_column_idx].is_fullscreen + self.columns[self.active_column_idx].is_fullscreen() } pub fn render_elements( @@ -2893,7 +2900,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.is_fullscreen() { let col_pos = view_off + col_off + col_render_off; let col_pos = col_pos.to_physical_precise_round(scale).to_logical(scale); @@ -3133,7 +3140,7 @@ impl ScrollingSpace { for (col_idx, col) in self.columns.iter().enumerate() { let col_w = col.width(); - let view_pos = if col.is_fullscreen { + let view_pos = if col.is_fullscreen() { col_x } else if self.working_area.size.w <= col_w { col_x - left_strut @@ -3160,7 +3167,7 @@ impl ScrollingSpace { // Normal columns align with the working area, but fullscreen columns align with // the view size. - if col.is_fullscreen { + if col.is_fullscreen() { let left = col_x; let right = col_x + col_w; (left, right) @@ -3287,7 +3294,7 @@ impl ScrollingSpace { let col_x = self.column_x(col_idx); let col_w = col.width(); - if col.is_fullscreen { + if col.is_fullscreen() { if target_snap.view_pos + self.view_size.w < col_x + col_w { break; } @@ -3309,7 +3316,7 @@ impl ScrollingSpace { let col_x = self.column_x(col_idx); let col_w = col.width(); - if col.is_fullscreen { + if col.is_fullscreen() { if col_x < target_snap.view_pos { break; } @@ -3382,7 +3389,7 @@ impl ScrollingSpace { .find(|col| col.contains(&window)) .unwrap(); - if col.is_fullscreen { + if col.is_pending_fullscreen { return false; } @@ -3630,7 +3637,7 @@ impl ScrollingSpace { if self.view_offset_before_fullscreen.is_some() { assert!( - col.is_any_fullscreen(), + col.is_fullscreen(), "when view_offset_before_fullscreen is set, \ the active column must be fullscreen" ); @@ -3807,7 +3814,7 @@ impl Column { width, preset_width_idx: None, is_full_width, - is_fullscreen: false, + is_pending_fullscreen: false, display_mode, tab_indicator: TabIndicator::new(options.tab_indicator), move_animation: None, @@ -3829,7 +3836,7 @@ impl Column { // Animate the tab indicator for new columns. if display_mode == ColumnDisplay::Tabbed && !rv.options.tab_indicator.hide_when_single_tab - && !is_pending_fullscreen + && !rv.is_fullscreen() { // Usually new columns are created together with window movement actions. For new // windows, we handle that in start_open_animation(). @@ -3945,7 +3952,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.is_fullscreen(); self.tab_indicator.update_render_elements( enabled, @@ -4003,7 +4010,52 @@ impl Column { } } - fn is_any_fullscreen(&self) -> bool { + /// Returns whether this column is currently fullscreen. + /// + /// As in, if it contains one currently-fullscreen tile, or in tabbed mode, if it contains at + /// least one currently-fullscreen tile. + /// + /// This will lag behind is_pending_fullscreen, depending on when the tiles actually respond to + /// the un/fullscreen request. But, it's possible for is_fullscreen() to flip instantly, for + /// example when consuming a fullscreen tile into a non-pending-fullscreen column. + /// + /// This controls things like: + /// + /// - whether the column draws at the top of the screen or at the start of the working area + /// - whether the column draws above the top layer-shell layer + /// - whether the tab indicator is shown + /// - restoring view_offset_before_fullscreen + /// + /// Edge cases to watch out for: + /// + /// - Consuming a fullscreen tile into a non-tabbed column will keep that tile fullscreen until + /// it responds to the unfullscreen request. This tile may be anywhere in the column, + /// including at the active position. + /// + /// - Changing a fullscreen tabbed column into normal mode is an easy way to get randomly + /// delayed unfullscreening tiles in a normal 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 { + // Behaviors that we want: + // + // 1. The common case: single tile in a column. Assume no animations. Fullscreening the tile + // should make it jump to the top-left of the screen only when the tile finishes + // fullscreening. Similarly, unfullscreening should keep it at the top-left until the + // tile had unfullscreened. + // + // 2. Unfullscreening a tabbed column with multiple tiles should restore the view offset + // correctly. This means waiting for *all* tiles to unfullscreen, because otherwise the + // restored view offset will immediately get overwritted by the still screen-wide column + // (it uses the largest tile's width). + // + // 3. Changing a fullscreen tabbed column to normal should probably also restore the view + // offset correctly. Same problem as above, but now for normal columns (since display + // 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()) } @@ -4047,7 +4099,7 @@ impl Column { prev_offsets.extend(self.tile_offsets().take(self.tiles.len())); if self.display_mode != ColumnDisplay::Tabbed { - self.is_fullscreen = false; + self.is_pending_fullscreen = false; } self.data @@ -4159,7 +4211,7 @@ impl Column { } fn update_tile_sizes_with_transaction(&mut self, animate: bool, transaction: Transaction) { - if self.is_fullscreen { + if self.is_pending_fullscreen { 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; @@ -4456,7 +4508,7 @@ impl Column { .map(NotNan::into_inner) .unwrap(); - if self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen { + if self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen() { let extra_size = self.tab_indicator.extra_size(self.tiles.len(), self.scale); tiles_width += extra_size.w; } @@ -4807,7 +4859,7 @@ impl Column { } fn set_fullscreen(&mut self, is_fullscreen: bool) { - if self.is_fullscreen == is_fullscreen { + if self.is_pending_fullscreen == is_fullscreen { return; } @@ -4815,7 +4867,7 @@ impl Column { assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed); } - self.is_fullscreen = is_fullscreen; + self.is_pending_fullscreen = is_fullscreen; self.update_tile_sizes(false); } @@ -4880,7 +4932,7 @@ impl Column { fn tiles_origin(&self) -> Point { let mut origin = Point::from((0., 0.)); - if self.is_fullscreen { + if self.is_fullscreen() { return origin; } @@ -5040,7 +5092,7 @@ impl Column { // Animate the appearance of the tab indicator. if self.display_mode == ColumnDisplay::Tabbed - && !self.is_fullscreen + && !self.is_fullscreen() && self.tiles.len() == 1 && !self.tab_indicator.config().hide_when_single_tab { @@ -5063,7 +5115,7 @@ impl Column { assert!(self.active_tile_idx < self.tiles.len()); assert_eq!(self.tiles.len(), self.data.len()); - if self.is_fullscreen { + if self.is_pending_fullscreen { assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed); } @@ -5094,7 +5146,10 @@ impl Column { assert!(Rc::ptr_eq(&self.options, &tile.options)); assert_eq!(self.clock, tile.clock); assert_eq!(self.scale, tile.scale()); - assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen()); + assert_eq!( + self.is_pending_fullscreen, + tile.window().is_pending_fullscreen() + ); assert_eq!(self.view_size, tile.view_size()); tile.verify_invariants(); @@ -5119,7 +5174,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_fullscreen + if !self.is_pending_fullscreen && self.scale.round() == self.scale && working_size.h.round() == working_size.h && gaps.round() == gaps -- cgit