diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-03-17 14:56:29 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-03-17 22:31:19 -0700 |
| commit | 39f52b75856936db01325aa1c7f15fe8f379485d (patch) | |
| tree | 08fcbf8c938aa2b10a9be3014bf292f17225063d /src | |
| parent | b447b1f4de65ee706bdbbdb9b650ab030459c0fb (diff) | |
| download | niri-39f52b75856936db01325aa1c7f15fe8f379485d.tar.gz niri-39f52b75856936db01325aa1c7f15fe8f379485d.tar.bz2 niri-39f52b75856936db01325aa1c7f15fe8f379485d.zip | |
Implement toggle-windowed-fullscreen
Windowed, or fake, or detached, fullscreen, is when a window thinks that it's
fullscreen, but the compositor treats it as a normal window.
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/mod.rs | 17 | ||||
| -rw-r--r-- | src/layout/mod.rs | 41 | ||||
| -rw-r--r-- | src/layout/tests.rs | 75 | ||||
| -rw-r--r-- | src/window/mapped.rs | 96 |
4 files changed, 225 insertions, 4 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs index 08bebe40..c9677ba2 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -690,6 +690,23 @@ impl State { self.niri.queue_redraw_all(); } } + Action::ToggleWindowedFullscreen => { + let focus = self.niri.layout.focus().map(|m| m.window.clone()); + if let Some(window) = focus { + self.niri.layout.toggle_windowed_fullscreen(&window); + // FIXME: granular + self.niri.queue_redraw_all(); + } + } + Action::ToggleWindowedFullscreenById(id) => { + let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); + let window = window.map(|(_, m)| m.window.clone()); + if let Some(window) = window { + self.niri.layout.toggle_windowed_fullscreen(&window); + // FIXME: granular + self.niri.queue_redraw_all(); + } + } Action::FocusWindow(id) => { let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); let window = window.map(|(_, m)| m.window.clone()); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 15d48d6a..64e6877c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -244,6 +244,13 @@ pub trait LayoutElement { Some(requested) } + fn is_pending_windowed_fullscreen(&self) -> bool { + false + } + fn request_windowed_fullscreen(&mut self, value: bool) { + let _ = value; + } + fn is_child_of(&self, parent: &Self) -> bool; fn rules(&self) -> &ResolvedWindowRules; @@ -3458,6 +3465,20 @@ impl<W: LayoutElement> Layout<W> { } pub fn set_fullscreen(&mut self, id: &W::Id, is_fullscreen: bool) { + // Check if this is a request to unset the windowed fullscreen state. + if !is_fullscreen { + let mut handled = false; + self.with_windows_mut(|window, _| { + if window.id() == id && window.is_pending_windowed_fullscreen() { + window.request_windowed_fullscreen(false); + handled = true; + } + }); + if handled { + return; + } + } + if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { if move_.tile.window().id() == id { return; @@ -3487,6 +3508,26 @@ impl<W: LayoutElement> Layout<W> { } } + 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() { + // Remove the real fullscreen. + for ws in self.workspaces_mut() { + if ws.has_window(id) { + ws.set_fullscreen(id, false); + break; + } + } + } + + // This will switch is_pending_fullscreen() to false right away. + self.with_windows_mut(|window, _| { + if window.id() == id { + window.request_windowed_fullscreen(!window.is_pending_windowed_fullscreen()); + } + }); + } + pub fn workspace_switch_gesture_begin(&mut self, output: &Output, is_touchpad: bool) { let monitors = match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => monitors, diff --git a/src/layout/tests.rs b/src/layout/tests.rs index 266b615e..8e1c3adf 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -29,6 +29,8 @@ struct TestWindowInner { pending_fullscreen: Cell<bool>, pending_activated: Cell<bool>, is_fullscreen: Cell<bool>, + is_windowed_fullscreen: Cell<bool>, + is_pending_windowed_fullscreen: Cell<bool>, } #[derive(Debug, Clone)] @@ -72,6 +74,8 @@ impl TestWindow { pending_fullscreen: Cell::new(false), pending_activated: Cell::new(false), is_fullscreen: Cell::new(false), + is_windowed_fullscreen: Cell::new(false), + is_pending_windowed_fullscreen: Cell::new(false), })) } @@ -101,6 +105,13 @@ impl TestWindow { changed = true; } + if self.0.is_windowed_fullscreen.get() != self.0.is_pending_windowed_fullscreen.get() { + self.0 + .is_windowed_fullscreen + .set(self.0.is_pending_windowed_fullscreen.get()); + changed = true; + } + changed } } @@ -144,6 +155,10 @@ impl LayoutElement for TestWindow { ) { self.0.requested_size.set(Some(size)); self.0.pending_fullscreen.set(is_fullscreen); + + if is_fullscreen { + self.0.is_pending_windowed_fullscreen.set(false); + } } fn min_size(&self) -> Size<i32, Logical> { @@ -191,10 +206,18 @@ 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 is_pending_fullscreen(&self) -> bool { + if self.0.is_pending_windowed_fullscreen.get() { + return false; + } + self.0.pending_fullscreen.get() } @@ -202,6 +225,14 @@ impl LayoutElement for TestWindow { self.0.requested_size.get() } + fn is_pending_windowed_fullscreen(&self) -> bool { + self.0.is_pending_windowed_fullscreen.get() + } + + fn request_windowed_fullscreen(&mut self, value: bool) { + self.0.is_pending_windowed_fullscreen.set(value); + } + fn is_child_of(&self, parent: &Self) -> bool { self.0.parent_id.get() == Some(parent.0.id) } @@ -374,6 +405,7 @@ enum Op { window: usize, is_fullscreen: bool, }, + ToggleWindowedFullscreen(#[proptest(strategy = "1..=5usize")] usize), FocusColumnLeft, FocusColumnRight, FocusColumnFirst, @@ -901,14 +933,26 @@ impl Op { layout.remove_window(&id, Transaction::new()); } Op::FullscreenWindow(id) => { + if !layout.has_window(&id) { + return; + } layout.toggle_fullscreen(&id); } Op::SetFullscreenWindow { window, is_fullscreen, } => { + if !layout.has_window(&window) { + return; + } layout.set_fullscreen(&window, is_fullscreen); } + Op::ToggleWindowedFullscreen(id) => { + if !layout.has_window(&id) { + return; + } + layout.toggle_windowed_fullscreen(&id); + } Op::FocusColumnLeft => layout.focus_left(), Op::FocusColumnRight => layout.focus_right(), Op::FocusColumnFirst => layout.focus_column_first(), @@ -3187,6 +3231,37 @@ fn unfullscreen_with_large_border() { check_ops_with_options(options, &ops); } +#[test] +fn fullscreen_to_windowed_fullscreen() { + let ops = [ + Op::AddOutput(0), + Op::AddWindow { + params: TestWindowParams::new(0), + }, + Op::FullscreenWindow(0), + Op::Communicate(0), // Make sure it goes into fullscreen. + Op::ToggleWindowedFullscreen(0), + ]; + + check_ops(&ops); +} + +#[test] +fn windowed_fullscreen_to_fullscreen() { + let ops = [ + Op::AddOutput(0), + Op::AddWindow { + params: TestWindowParams::new(0), + }, + Op::FullscreenWindow(0), + Op::Communicate(0), // Commit fullscreen state. + Op::ToggleWindowedFullscreen(0), // Switch is_fullscreen() to false. + Op::FullscreenWindow(0), // Switch is_fullscreen() back to true. + ]; + + check_ops(&ops); +} + fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool { if parent_id == id { return true; diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 9c4670dd..29dc185a 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -120,6 +120,36 @@ pub struct Mapped { /// /// Used for double-resize-click tracking. last_interactive_resize_start: Cell<Option<(Duration, ResizeEdge)>>, + + /// Whether this window is in windowed (fake) fullscreen. + /// + /// In this mode, the underlying window is told that it's fullscreen, while keeping it as + /// a regular, non-fullscreen tile. + is_windowed_fullscreen: bool, + + /// Whether this window is pending to go to windowed (fake) fullscreen. + /// + /// Several places in the layout code assume that is_fullscreen() can flip only on a commit. + /// Which is something that we do want to flip when changing is_windowed_fullscreen. Flipping + /// it right away would mean remembering to call layout.update_window() after any operation + /// that may change is_windowed_fullscreen, which is quite tricky and error-prone, especially + /// for deeply nested operations. + /// + /// It's also not clear what's the best way to go about it. Ideally we'd wait for configure ack + /// and commit before "committing" to is_windowed_fullscreen, however, since it's not real + /// Wayland state, we may end up with no Wayland state change to configure at all. + /// + /// For example: when the window is in real fullscreen, but its non-fullscreen size matches + /// its fullscreen size. Then turning on is_windowed_fullscreen will both keep the + /// fullscreen state, and keep the size (since it matches), resulting in no configure. + /// + /// So we work around this by "committing" is_pending_windowed_fullscreen to + /// is_windowed_fullscreen on receiving any actual window commit, and whenever + /// is_pending_windowed_fullscreen changes, we mark the window as needs_configure. This does + /// mean some unnecessary delays in some cases, but it also means being able to better + /// synchronize our windowed fullscreen state to the real window updates, so that's good I + /// guess. + is_pending_windowed_fullscreen: bool, } niri_render_elements! { @@ -209,6 +239,8 @@ impl Mapped { pending_transactions: Vec::new(), interactive_resize: None, last_interactive_resize_start: Cell::new(None), + is_windowed_fullscreen: false, + is_pending_windowed_fullscreen: false, } } @@ -608,10 +640,21 @@ impl LayoutElement for Mapped { animate: bool, transaction: Option<Transaction>, ) { + // Going into real fullscreen resets windowed fullscreen. + if is_fullscreen { + self.is_pending_windowed_fullscreen = false; + + if self.is_windowed_fullscreen { + // Make sure we receive a commit to update self.is_windowed_fullscreen to false + // later on. + self.needs_configure = true; + } + } + let changed = self.toplevel().with_pending_state(|state| { let changed = state.size != Some(size); state.size = Some(size); - if is_fullscreen { + if is_fullscreen || self.is_pending_windowed_fullscreen { state.states.set(xdg_toplevel::State::Fullscreen); } else { state.states.unset(xdg_toplevel::State::Fullscreen); @@ -660,7 +703,8 @@ impl LayoutElement for Mapped { let same_size = last_sent.size.unwrap_or_default() == size; let has_fullscreen = last_sent.states.contains(xdg_toplevel::State::Fullscreen); - (same_size && !has_fullscreen).then_some(last_serial) + let same_fullscreen = has_fullscreen == self.is_pending_windowed_fullscreen; + (same_size && same_fullscreen).then_some(last_serial) }); if let Some(serial) = already_sent { @@ -687,7 +731,9 @@ impl LayoutElement for Mapped { let changed = self.toplevel().with_pending_state(|state| { let changed = state.size != Some(size); state.size = Some(size); - state.states.unset(xdg_toplevel::State::Fullscreen); + if !self.is_pending_windowed_fullscreen { + state.states.unset(xdg_toplevel::State::Fullscreen); + } changed }); @@ -934,6 +980,10 @@ impl LayoutElement for Mapped { } fn is_fullscreen(&self) -> bool { + if self.is_windowed_fullscreen { + return false; + } + with_toplevel_role(self.toplevel(), |role| { role.current .states @@ -942,6 +992,10 @@ impl LayoutElement for Mapped { } fn is_pending_fullscreen(&self) -> bool { + if self.is_pending_windowed_fullscreen { + return false; + } + self.toplevel() .with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen)) } @@ -1014,7 +1068,7 @@ impl LayoutElement for Mapped { if let Some((mut size, fullscreen)) = pending { // If the pending change is fullscreen, we can't use that size. - if fullscreen { + if fullscreen && !self.is_pending_windowed_fullscreen { return None; } @@ -1034,6 +1088,33 @@ impl LayoutElement for Mapped { } } + fn is_pending_windowed_fullscreen(&self) -> bool { + self.is_pending_windowed_fullscreen + } + + fn request_windowed_fullscreen(&mut self, value: bool) { + if self.is_pending_windowed_fullscreen == value { + return; + } + + self.is_pending_windowed_fullscreen = value; + + // Set the fullscreen state to match. + // + // When going from windowed to real fullscreen, we'll use request_size() which will set the + // fullscreen state back. + self.toplevel().with_pending_state(|state| { + if value { + state.states.set(xdg_toplevel::State::Fullscreen); + } else { + state.states.unset(xdg_toplevel::State::Fullscreen); + } + }); + + // Make sure we recieve a commit later to update self.is_windowed_fullscreen. + self.needs_configure = true; + } + fn is_child_of(&self, parent: &Self) -> bool { self.toplevel().parent().as_ref() == Some(parent.toplevel().wl_surface()) } @@ -1098,5 +1179,12 @@ impl LayoutElement for Mapped { self.request_size_once = Some(RequestSizeOnce::UseWindowSize); } } + + // HACK: this is not really accurate because the commit might be for an earlier serial than + // when we requested windowed fullscren. But we don't actually care much, since this is + // entirely compositor state. We're only tying it to configure/commit as a workaround to + // the rest of the code expecting that fullscreen doesn't suddenly just change in the + // middle of something. + self.is_windowed_fullscreen = self.is_pending_windowed_fullscreen; } } |
