Skip to content

Let half actions preserve the window's size on the other axis (opt-in) - #1824

Open
X-Wei wants to merge 1 commit into
rxhanson:mainfrom
maxing-labs:halves-preserve-other-axis
Open

Let half actions preserve the window's size on the other axis (opt-in)#1824
X-Wei wants to merge 1 commit into
rxhanson:mainfrom
maxing-labs:halves-preserve-other-axis

Conversation

@X-Wei

@X-Wei X-Wei commented Aug 21, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in mode in which Left Half, Right Half, Top Half and Bottom Half only change the axis they belong to (Left/Right → width, Top/Bottom → height) and keep the window's size and position on the other axis — the way Win + arrow keys tile windows on Windows, or keyboard tiling works on KDE.

With it, the four half shortcuts are enough to reach every half and quarter: Left Half then Top Half lands in the top left quarter, Bottom Half from there expands back to Left Half, and Right Half then Left Half fills the screen.

Off by default (halvesPreserveOtherAxisSize); nothing changes unless it is enabled.

Related: discussions #665 and #811 (where this was asked for, and you said you would be fine merging a good PR for it configurable via Terminal command), and #144. This is the narrow version discussed there: no separate "Windows mode" and no preset or extra shortcuts — the existing half actions simply take the window's current position into account, the logic lives in one self-contained class, and everything sits behind a single default.

Behavior when enabled

Each half action looks at where the window currently sits along its own axis and leaves the other axis exactly as it is:

Window along the action's axis Left Half / Top Half Right Half / Bottom Half
spans the whole axis (or has a size no half action produces) dock to the left / top edge dock to the right / bottom edge
docked to the left / top edge inside a quarter: no change; plain half: repeats as today (cycle sizes, move across displays, …) expand to the whole axis
docked to the right / bottom edge expand to the whole axis inside a quarter: no change; plain half: repeats as today

Some sequences:

Sequence Result
Left Half, Top Half (or Top Half, Left Half) top left quarter
top left quarter, Bottom Half Left Half
top left quarter, Right Half Top Half
top left quarter, Top Half or Left Half no change
Right Half, Left Half full width and height (same as Maximize, including its gap setting)
Left Half, Left Half as today: cycle 1/2 → 2/3 → 1/3, move across displays, or nothing, depending on the repeated-command setting
untiled (floating, centered, …) window, any half as today

Untiled windows and repeated executions of a plain half deliberately fall through to the existing code, so the feature never changes what the repeated-command setting does.

How it works

  • HalvesPreserveOtherAxisSize.rect(for:) recognizes the window's state on each axis purely from its current frame: an axis counts as "docked" when the window's origin and extent match (within 10 pt, for apps that resize in character cells) an edge-docked rect that a half action produces on that screen — at the active split ratio or any cycle size, with or without gaps applied. Anything else counts as "whole axis". It returns nil whenever the existing logic should run.
  • LeftRightHalfCalculation, TopHalfCalculation and BottomHalfCalculation consult it first when the default is enabled; otherwise they are untouched.
  • The result reports the geometrically equivalent action (.topLeft + .topLeftQuarter, .leftHalf, .maximize, …) as resultingAction / subAction, so gaps and the window history behave exactly as if that action had been executed. For that, WindowCalculation.calculate now forwards RectResult.resultingAction instead of ignoring it — every subclass that sets it already overrides calculate, so this is not a behavior change by itself.
  • Drag to snap goes through the same calculations, so the footprint preview matches the snap. With the default unsnap-restore the dragged window is back at its original size when it snaps, so snapping behaves as before.

Testing

  • HalvesPreserveOtherAxisSizeTests (32 cases): every state transition, no-ops inside quarters, fallback to the existing logic for untiled windows and repeated plain halves, cycled 2/3 and 1/3 columns/rows, non-50 % split ratios, gaps (including the skip-top-edge option), matching tolerance, and the disabled path.
  • Full suite on top of 64a9186 (v0.99): 266 tests, 0 failures.
  • Used daily on macOS 26.6 since 2026-08-21 with 5 px gaps, 50 % splits and the default repeated-command setting (resize).

Try it

A notarized Apple-silicon build of this branch (unofficial, signed with my own Developer ID) is attached to https://github.com/maxing-labs/Rectangle/releases/tag/v0.99-halvesPreserveOtherAxis-b108 — install, then tick the checkbox at the bottom of the Extras popover in Settings.

Docs / UI

  • TerminalCommands.md: new section "Make the half actions tile like Windows or KDE" with the defaults write command.
  • Settings gets a "Half actions preserve the window's size on the other axis" checkbox, unchecked by default, at the bottom of the Extras popover in the General tab (below the side split ratio rows, as requested), with a tooltip describing the Left Half → Top Half → Bottom Half round trip; the two strings are registered in Main.xcstrings. Happy to drop the checkbox and keep this terminal-only if you prefer.

@rxhanson

Copy link
Copy Markdown
Owner

Thanks for contributing! Things look good at a quick glance. The list of checkboxes in the General tab is steadily growing and I haven't ironed out my preferred way to prevent it from turning into something unwieldy. Would you mind putting that checkbox at the bottom of the popover displayed from the "Extras" button in the General tab for now? I think I'll end up with another tab for more settings instead of the Extras button, but I haven't decided where everything will go yet.

Left Half, Right Half, Top Half and Bottom Half always give the window the
whole height or width of the screen, so reaching a quarter takes a dedicated
corner shortcut. Behind the new halvesPreserveOtherAxisSize default, each of
them only changes the axis it belongs to (Left/Right: width, Top/Bottom:
height) and keeps the other one, like Win + arrow keys on Windows or keyboard
tiling on KDE: Left Half then Top Half lands in the top left quarter, Bottom
Half from there expands back to Left Half, Right Half then Left Half fills
the screen.

The window's state on each axis is recognized purely from its current frame
(docked to an edge at the active split ratio or any cycle size, with or
without gaps, within a 10 pt tolerance), so nothing new is tracked. The
result reports the geometrically equivalent action as resultingAction /
subAction so gaps and window history behave as if that action had run;
WindowCalculation.calculate now forwards RectResult.resultingAction, which
every subclass setting it already handled by overriding calculate.

Untiled windows and repeated executions of a plain half fall through to the
existing code, so the repeated-command setting keeps working as before. The
default is off; with it off nothing changes.

- Add HalvesPreserveOtherAxisSize.swift with the per-axis state logic
- Consult it first in the left/right, top and bottom half calculations
- Add HalvesPreserveOtherAxisSizeTests (32 cases)
- Document the default in TerminalCommands.md
- Expose it as a "Half actions preserve the window's size on the other axis"
  checkbox, unchecked by default, at the bottom of the Extras popover in the
  General settings tab (title and tooltip registered in Main.xcstrings)
@X-Wei
X-Wei force-pushed the halves-preserve-other-axis branch from 7905548 to 0ea5f70 Compare August 22, 2026 17:32
@X-Wei

X-Wei commented Aug 22, 2026

Copy link
Copy Markdown
Author

Done — the checkbox now sits at the bottom of the Extras popover, below the side split ratio rows, and the General tab is untouched. Same title, tooltip and default (off); the commit is amended and force-pushed. Happy to move it again once you settle on a tab for these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants