window management library
git clone https://git.lucas.co/cce-window-manager.git
fix(fullscreen): the toggle's exit restores the mode and lock it replaced
Leaving fullscreen unlocked the window to its viewport-resolved mode, so a
window tiled by hand (locked Tiled, no rule) came back Floating at the same
geometry. That is not what a toggle implies, and it has a visible cost for
Chromium-family clients: Tiled tells the client it is maximized and only
then does it drop its client-side shadow band, so the un-tiled window grew
a margin and its corners read square.
ActionWindow carries `pre_fullscreen` — the (mode, locked) pair the
mechanism records when a SetWindowMode makes the window Fullscreen — and
the exit restores it. Without a record, or with one that itself says
Fullscreen, the resolved-mode fallback stands.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/actions.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++--------
src/api.rs | 6 ++++++
2 files changed, 55 insertions(+), 8 deletions(-)
diff --git a/src/actions.rs b/src/actions.rs
index ab0ddc0..2f24ae4 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -392,19 +392,30 @@ fn focus_directional(ctx: &ActionCtx, action: Action) -> Vec<Command> {
vec![Command::Focus(id), Command::Raise(id), Command::Relayout]
}
-/// Toggle fullscreen on the focused window. Leaving fullscreen unlocks the
-/// window back to its viewport-resolved mode (Floating if that resolution is
-/// itself Fullscreen).
+/// Toggle fullscreen on the focused window. Leaving fullscreen puts the
+/// window back the way the toggle found it — mode AND lock, so a window
+/// tiled by hand stays tiled (the mechanism records both when it applies
+/// the Fullscreen `SetWindowMode`). Without that record it unlocks to the
+/// viewport-resolved mode (Floating if that resolution is itself
+/// Fullscreen). Restoring matters beyond the mode itself: Tiled tells the
+/// client it is maximized, and Chromium-family clients drop their
+/// client-side shadow band only then.
fn fullscreen(ctx: &ActionCtx) -> Vec<Command> {
let Some(id) = ctx.focused else { return Vec::new() };
let Some(win) = window(ctx, id) else { return Vec::new() };
let cmd = if win.mode == TilingMode::Fullscreen {
- let target = if win.resolved_mode == TilingMode::Fullscreen {
- TilingMode::Floating
- } else {
- win.resolved_mode
+ let (mode, locked) = match win.pre_fullscreen {
+ Some((mode, locked)) if mode != TilingMode::Fullscreen => (mode, locked),
+ _ => {
+ let target = if win.resolved_mode == TilingMode::Fullscreen {
+ TilingMode::Floating
+ } else {
+ win.resolved_mode
+ };
+ (target, false)
+ }
};
- Command::SetWindowMode { id, mode: target, locked: false }
+ Command::SetWindowMode { id, mode, locked }
} else {
Command::SetWindowMode { id, mode: TilingMode::Fullscreen, locked: true }
};
@@ -487,6 +498,7 @@ mod tests {
scale: 1.0,
mode: TilingMode::Floating,
resolved_mode: TilingMode::Floating,
+ pre_fullscreen: None,
visible: true,
focus_cyclable: true,
overview_eligible: true,
@@ -649,6 +661,35 @@ mod tests {
);
}
+ #[test]
+ fn fullscreen_exit_restores_the_recorded_mode_and_lock() {
+ let mut c = ctx();
+ c.focused = Some(wid(1));
+ c.windows.push(win(1, 0.0, 0.0, 100.0, 100.0));
+ c.windows[0].mode = TilingMode::Fullscreen;
+ // A hand-tiled (locked) window comes back Tiled and locked, whatever
+ // the viewport would resolve it to.
+ c.windows[0].resolved_mode = TilingMode::Floating;
+ c.windows[0].pre_fullscreen = Some((TilingMode::Tiled, true));
+ assert_eq!(
+ dispatch(&c, Action::Fullscreen)[0],
+ Command::SetWindowMode { id: wid(1), mode: TilingMode::Tiled, locked: true }
+ );
+ // An unlocked Floating window comes back unlocked.
+ c.windows[0].pre_fullscreen = Some((TilingMode::Floating, false));
+ assert_eq!(
+ dispatch(&c, Action::Fullscreen)[0],
+ Command::SetWindowMode { id: wid(1), mode: TilingMode::Floating, locked: false }
+ );
+ // A record that itself says Fullscreen is useless: resolved-mode fallback.
+ c.windows[0].resolved_mode = TilingMode::Tiled;
+ c.windows[0].pre_fullscreen = Some((TilingMode::Fullscreen, true));
+ assert_eq!(
+ dispatch(&c, Action::Fullscreen)[0],
+ Command::SetWindowMode { id: wid(1), mode: TilingMode::Tiled, locked: false }
+ );
+ }
+
#[test]
fn mode_next_cycles_and_shared_hits_all_matching() {
let mut c = ctx();
diff --git a/src/api.rs b/src/api.rs
index c9b8865..72e27f3 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -371,6 +371,12 @@ pub struct ActionWindow {
/// (mechanism's `get_mode_for_window`) — what leaving Fullscreen
/// falls back to.
pub resolved_mode: TilingMode,
+ /// The mode and lock the window had when a `SetWindowMode` sent it
+ /// Fullscreen — what leaving Fullscreen restores, so a Tiled window
+ /// comes back Tiled. `None` when it is not Fullscreen, or got there by
+ /// another route (a client request, a rule); the exit then falls back
+ /// to `resolved_mode`.
+ pub pre_fullscreen: Option<(TilingMode, bool)>,
/// Mapped and not in Closing/Init teardown/startup.
pub visible: bool,
/// In the focus-cycling set: currently rendered, not minimized, not a