mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
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.
This commit is contained in:
@@ -1476,6 +1476,9 @@ pub enum Action {
|
||||
FullscreenWindow,
|
||||
#[knuffel(skip)]
|
||||
FullscreenWindowById(u64),
|
||||
ToggleWindowedFullscreen,
|
||||
#[knuffel(skip)]
|
||||
ToggleWindowedFullscreenById(u64),
|
||||
#[knuffel(skip)]
|
||||
FocusWindow(u64),
|
||||
FocusWindowInColumn(#[knuffel(argument)] u8),
|
||||
@@ -1680,6 +1683,12 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::CloseWindow { id: Some(id) } => Self::CloseWindowById(id),
|
||||
niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow,
|
||||
niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id),
|
||||
niri_ipc::Action::ToggleWindowedFullscreen { id: None } => {
|
||||
Self::ToggleWindowedFullscreen
|
||||
}
|
||||
niri_ipc::Action::ToggleWindowedFullscreen { id: Some(id) } => {
|
||||
Self::ToggleWindowedFullscreenById(id)
|
||||
}
|
||||
niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id),
|
||||
niri_ipc::Action::FocusWindowInColumn { index } => Self::FocusWindowInColumn(index),
|
||||
niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious,
|
||||
|
@@ -221,6 +221,18 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Toggle windowed (fake) fullscreen on a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
|
||||
)]
|
||||
ToggleWindowedFullscreen {
|
||||
/// Id of the window to toggle windowed fullscreen of.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Focus a window by id.
|
||||
FocusWindow {
|
||||
/// Id of the window to focus.
|
||||
|
@@ -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());
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user