aboutsummaryrefslogtreecommitdiff
path: root/src/handlers/xdg_shell.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-09-02 08:07:22 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-10-15 09:04:16 +0300
commite1fad994da9565b43c7fb139cb2fb7bf404cc320 (patch)
tree305fa0714d66ad2b4346b3aee6eb785099b29fa1 /src/handlers/xdg_shell.rs
parente5d4e7c1b1a0b61770b6711a53fe41920d56452d (diff)
downloadniri-e1fad994da9565b43c7fb139cb2fb7bf404cc320.tar.gz
niri-e1fad994da9565b43c7fb139cb2fb7bf404cc320.tar.bz2
niri-e1fad994da9565b43c7fb139cb2fb7bf404cc320.zip
Implement maximize-to-edges (true Wayland maximize)
Diffstat (limited to 'src/handlers/xdg_shell.rs')
-rw-r--r--src/handlers/xdg_shell.rs234
1 files changed, 222 insertions, 12 deletions
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 5d8adc06..e5d91f16 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -428,18 +428,205 @@ impl XdgShellHandler for State {
});
}
- fn maximize_request(&mut self, surface: ToplevelSurface) {
- // FIXME
+ fn maximize_request(&mut self, toplevel: ToplevelSurface) {
+ if let Some((mapped, _)) = self
+ .niri
+ .layout
+ .find_window_and_output_mut(toplevel.wl_surface())
+ {
+ // A configure is required in response to this event regardless if there are pending
+ // changes.
+ mapped.set_needs_configure();
- // A configure is required in response to this event. However, if an initial configure
- // wasn't sent, then we will send this as part of the initial configure later.
- if surface.is_initial_configure_sent() {
- surface.send_configure();
+ let window = mapped.window.clone();
+ self.niri.layout.set_maximized(&window, true);
+ } else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
+ match &mut unmapped.state {
+ InitialConfigureState::NotConfigured {
+ wants_maximized, ..
+ } => {
+ *wants_maximized = true;
+
+ // The required configure will be the initial configure.
+ }
+ InitialConfigureState::Configured {
+ rules,
+ output,
+ is_pending_maximized,
+ ..
+ } => {
+ // Figure out the monitor following a similar logic to initial configure.
+ // FIXME: deduplicate.
+ let mon = output
+ .as_ref()
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ .map(|mon| (mon, false))
+ // If not, check if we have a parent with a monitor.
+ .or_else(|| {
+ toplevel
+ .parent()
+ .and_then(|parent| self.niri.layout.find_window_and_output(&parent))
+ .and_then(|(_win, output)| output)
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ .map(|mon| (mon, true))
+ })
+ // If not, fall back to the active monitor.
+ .or_else(|| {
+ self.niri
+ .layout
+ .active_monitor_ref()
+ .map(|mon| (mon, false))
+ });
+
+ *output = mon
+ .filter(|(_, parent)| !parent)
+ .map(|(mon, _)| mon.output().clone());
+ let mon = mon.map(|(mon, _)| mon);
+
+ let ws = mon
+ .map(|mon| mon.active_workspace_ref())
+ .or_else(|| self.niri.layout.active_workspace());
+
+ if let Some(ws) = ws {
+ // If the window is pending fullscreen, then this will do nothing. But
+ // that's expected: the window remains fullscreen, and we simply remember
+ // that it is now pending maximized.
+ *is_pending_maximized = true;
+ toplevel.with_pending_state(|state| {
+ if !state.states.contains(xdg_toplevel::State::Fullscreen) {
+ state.states.set(xdg_toplevel::State::Maximized);
+ }
+ });
+ ws.configure_new_window(&unmapped.window, None, None, false, rules);
+ }
+
+ // We already sent the initial configure, so we need to reconfigure.
+ toplevel.send_configure();
+ }
+ }
+ } else {
+ error!("couldn't find the toplevel in maximize_request()");
+ toplevel.send_configure();
}
}
- fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
- // FIXME
+ fn unmaximize_request(&mut self, toplevel: ToplevelSurface) {
+ if let Some((mapped, _)) = self
+ .niri
+ .layout
+ .find_window_and_output_mut(toplevel.wl_surface())
+ {
+ // A configure is required in response to this event regardless if there are pending
+ // changes.
+ mapped.set_needs_configure();
+
+ let window = mapped.window.clone();
+ self.niri.layout.set_maximized(&window, false);
+ } else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
+ match &mut unmapped.state {
+ InitialConfigureState::NotConfigured {
+ wants_maximized, ..
+ } => {
+ *wants_maximized = false;
+
+ // The required configure will be the initial configure.
+ }
+ InitialConfigureState::Configured {
+ rules,
+ width,
+ height,
+ floating_width,
+ floating_height,
+ is_full_width,
+ output,
+ workspace_name,
+ is_pending_maximized,
+ } => {
+ // Figure out the monitor following a similar logic to initial configure.
+ // FIXME: deduplicate.
+ let mon = workspace_name
+ .as_deref()
+ .and_then(|name| self.niri.layout.monitor_for_workspace(name))
+ .map(|mon| (mon, false));
+
+ let mon = mon.or_else(|| {
+ output
+ .as_ref()
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ .map(|mon| (mon, false))
+ // If not, check if we have a parent with a monitor.
+ .or_else(|| {
+ toplevel
+ .parent()
+ .and_then(|parent| {
+ self.niri.layout.find_window_and_output(&parent)
+ })
+ .and_then(|(_win, output)| output)
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ .map(|mon| (mon, true))
+ })
+ // If not, fall back to the active monitor.
+ .or_else(|| {
+ self.niri
+ .layout
+ .active_monitor_ref()
+ .map(|mon| (mon, false))
+ })
+ });
+
+ *output = mon
+ .filter(|(_, parent)| !parent)
+ .map(|(mon, _)| mon.output().clone());
+ let mon = mon.map(|(mon, _)| mon);
+
+ let ws = workspace_name
+ .as_deref()
+ .and_then(|name| mon.map(|mon| mon.find_named_workspace(name)))
+ .unwrap_or_else(|| {
+ mon.map(|mon| mon.active_workspace_ref())
+ .or_else(|| self.niri.layout.active_workspace())
+ });
+
+ if let Some(ws) = ws {
+ // If the window is pending fullscreen, then this will do nothing since
+ // then the Maximized state is already unset. But that's expected: the
+ // window remains fullscreen, and we simply remember that it is no
+ // longer pending maximized.
+ *is_pending_maximized = false;
+ toplevel.with_pending_state(|state| {
+ state.states.unset(xdg_toplevel::State::Maximized);
+ });
+
+ let is_floating = rules.compute_open_floating(&toplevel);
+ let configure_width = if is_floating {
+ *floating_width
+ } else if *is_full_width {
+ Some(PresetSize::Proportion(1.))
+ } else {
+ *width
+ };
+ let configure_height = if is_floating {
+ *floating_height
+ } else {
+ *height
+ };
+ ws.configure_new_window(
+ &unmapped.window,
+ configure_width,
+ configure_height,
+ is_floating,
+ rules,
+ );
+ }
+
+ // We already sent the initial configure, so we need to reconfigure.
+ toplevel.send_configure();
+ }
+ }
+ } else {
+ error!("couldn't find the toplevel in unmaximize_request()");
+ toplevel.send_configure();
+ }
}
fn fullscreen_request(
@@ -474,7 +661,9 @@ impl XdgShellHandler for State {
self.niri.layout.set_fullscreen(&window, true);
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
- InitialConfigureState::NotConfigured { wants_fullscreen } => {
+ InitialConfigureState::NotConfigured {
+ wants_fullscreen, ..
+ } => {
*wants_fullscreen = Some(requested_output);
// The required configure will be the initial configure.
@@ -517,6 +706,7 @@ impl XdgShellHandler for State {
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
+ state.states.unset(xdg_toplevel::State::Maximized);
});
ws.configure_new_window(&unmapped.window, None, None, false, rules);
}
@@ -545,7 +735,9 @@ impl XdgShellHandler for State {
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
- InitialConfigureState::NotConfigured { wants_fullscreen } => {
+ InitialConfigureState::NotConfigured {
+ wants_fullscreen, ..
+ } => {
*wants_fullscreen = None;
// The required configure will be the initial configure.
@@ -559,6 +751,7 @@ impl XdgShellHandler for State {
is_full_width,
output,
workspace_name,
+ is_pending_maximized,
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
@@ -608,6 +801,10 @@ impl XdgShellHandler for State {
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
+
+ if *is_pending_maximized {
+ state.states.set(xdg_toplevel::State::Maximized);
+ }
});
let is_floating = rules.compute_open_floating(&toplevel);
@@ -858,7 +1055,11 @@ impl State {
let Unmapped { window, state, .. } = unmapped;
- let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
+ let InitialConfigureState::NotConfigured {
+ wants_fullscreen,
+ wants_maximized,
+ } = state
+ else {
error!("window must not be already configured in send_initial_configure()");
return;
};
@@ -934,14 +1135,22 @@ impl State {
.or_else(|| self.niri.layout.active_workspace())
});
+ let mut is_pending_maximized = false;
if let Some(ws) = ws {
- // Set a fullscreen state based on window request and window rule.
+ // Set a fullscreen and maximized state based on window request and window rule.
+ is_pending_maximized = (*wants_maximized && rules.open_maximized_to_edges.is_none())
+ || rules.open_maximized_to_edges == Some(true);
+
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|| rules.open_fullscreen == Some(true)
{
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
+ } else if is_pending_maximized {
+ toplevel.with_pending_state(|state| {
+ state.states.set(xdg_toplevel::State::Maximized);
+ });
}
width = ws.resolve_default_width(rules.default_width, false);
@@ -979,6 +1188,7 @@ impl State {
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name().cloned()),
+ is_pending_maximized,
};
trace!(surface = %toplevel.wl_surface().id(), "sending initial configure");