Skip to content

Fix macOS Option-click green button zoom behavior - #14716

Merged
zachlloyd merged 1 commit into
masterfrom
factory/app-5157-option-click-zoom
Aug 6, 2026
Merged

Fix macOS Option-click green button zoom behavior#14716
zachlloyd merged 1 commit into
masterfrom
factory/app-5157-option-click-zoom

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes macOS titlebar green-button modifier behavior so Option-click routes through AppKit's native window handling and performs zoom (not fullscreen).

In crates/warpui/src/platform/mac/objc/window.m, WarpWindow now marks standard-window-button mouse-downs as native chrome and routes them through [super sendEvent:event] instead of calling [windowButton mouseDown:event] directly.

Linked Issue

Testing

  • ./script/format
  • cargo check -p warpui
  • cargo test -p warpui
  • cargo clippy -p warpui --all-targets --all-features --tests -- -D warnings
  • ⚠️ cargo clippy --workspace --all-targets --all-features --tests -- -D warnings fails on pre-existing, unrelated warp_completer lints in crates/warp_completer/src/completer/engine/argument/v2.rs (collapsible-if / let-and-return), outside this PR diff.
  • ✅ Manual macOS verification on the built patched binary via ./script/run:
    • Option-modified green click stays windowed (no fullscreen space).
    • Plain green click enters native fullscreen.
    • Red/yellow controls still work (close/minimize behavior preserved).

Regression harness note: no repo-local automated macOS window-chrome event harness was found for this Objective-C NSWindow path, so verification for this bug is live app behavior + visual proof.

  • I have manually tested my changes locally with ./script/run

Screenshots / Videos

Computer-use screenshots

View screenshot: Warp in fullscreen after clicking the green traffic-light button; hovering the top reveals the auto-hidden menu bar and traffic lights (red, disabled-gray minimize, green).
View screenshot: Warp entered macOS fullscreen after a plain green-button click — traffic lights are hidden and content fills the entire screen.
View screenshot: Warp remains a normal window with traffic-light buttons visible after an Option+click on the green button (it did not enter fullscreen).
View screenshot: Warp in a normal windowed state (traffic lights visible) after exiting fullscreen at the end of the demo.

Computer-use video recordings

View video recording: Warp windowed baseline; an Option-modified green traffic-light click (explicit down/up with Option held) that stays windowed; then a plain green click that enters fullscreen; then exiting fullscreen.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-BUG-FIX: macOS Option-click on Warp's green traffic-light button now performs zoom instead of entering fullscreen.
Originating thread: https://warpdev.slack.com/archives/C0BDQDW8V5E/p1785857994972379

Co-Authored-By: Warp Agent agent@warp.dev

Conversation: https://staging.warp.dev/conversation/d6074769-a12a-4e3b-bc62-548f9a995321
Run: https://oz.staging.warp.dev/runs/019fcf92-5622-7aba-986d-14124a371466
This PR was generated with Oz.

Co-Authored-By: Warp Agent <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 5, 2026
@warp-agent-staging warp-agent-staging Bot added the warpy-factory Label associated to the "Wob the Wuilder" factory on staging, also known as Warpy Factory. label Aug 5, 2026
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 5, 2026 02:51
@zachlloyd
zachlloyd requested review from acarl005 and zachlloyd and removed request for acarl005 August 6, 2026 01:23
@zachlloyd
zachlloyd enabled auto-merge (squash) August 6, 2026 01:24
@zachlloyd

Copy link
Copy Markdown
Contributor

@warp-agent are there any potential side effects of this change that I should potentially be on the lookout for? I tested the main change and i works fine

@warp-agent-staging

warp-agent-staging Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Oz run started.

View conversation · View run in Oz

@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

Good question — the diff is two lines, but it changes who handles the click, so there are a handful of second-order behaviors worth keeping an eye on. Context: the code you're changing came from #12557 (fix for #12389, traffic lights + resize edges dead on the macOS 27 beta), which deliberately force-dispatched [windowButton mouseDown:event] to guarantee the button got the click. Your change hands that back to AppKit.

Most likely to bite

  1. Traffic-light clicks now depend on AppKit's hit-testing, not our forced dispatch. Previously, if our hit test said "this point is inside the button's bounds," the button always got the click regardless of what AppKit would have done. Now the click only works if AppKit agrees. That's exactly the failure mode fix(macos): preserve native window chrome events #12557 was fixing, so the highest-value regression check is the original repro: macOS 27 beta (and macOS 26), all three buttons, plus dragging the resize edges. Also worth checking with a custom titlebar heightconfigure_titlebar_height repositions and constrains the traffic lights via Auto Layout inside a resized NSTitlebarContainerView, and AppKit's own hit region may not track those constraints as faithfully as our explicit NSPointInRect check. Test a very short and a very tall titlebar, and both with and without the tab bar in the titlebar.

  2. Window activation/ordering on background windows. The old direct dispatch actioned the button without going through AppKit's window-level handling. Native handling may now raise/activate the window as part of the same click. So clicking the yellow minimize on an unfocused Warp window might now focus it first. Native-correct, but a visible delta if you have multiple windows.

  3. The green button's other native affordances are now reachable. Beyond Option-click → zoom, routing through AppKit also re-enables Shift/Control-click variants and (macOS 15+) the hover/long-press window-tiling menu ("Fill", "Move & Resize", left/right tiles). Those all resize the window natively without a fullscreen transition, so verify our custom titlebar survives: windowWillEnterFullScreen/windowWillExitFullScreen handle the FS case, but a plain zoom or tile only re-applies the configured height via the titlebarContainerFrameDidChange observer. Worth eyeballing titlebar height and traffic-light positions after option-click zoom → un-zoom, and after a tile.

Lower probability, but where I'd look if something weird shows up

  1. Stale _leftMouseDownStartedInNativeWindowChrome latch. You now set it to YES for button clicks, but AppKit's button tracking loop normally swallows the mouse-up, so the latch is never cleared and stays YES until the next left-mouse-down. On macOS 27+ that means any drag/up reaching sendEvent: without a preceding mouse-down gets routed to [super sendEvent:] instead of the content view. In practice a fresh mouse-down always resets it, so I don't think it's reachable — but if you see tab/pane drag-and-drop hiccups (the CLD-2581 class of bug) right after clicking a traffic light on 27, that's the suspect.

  2. Synchronous zoom re-entrancy into Rust. Native zoom: runs inside sendEvent: and fires windowDidMove:warp_app_window_moved synchronously — the same class of thing the windowWillStartLiveResize "borrowMut" hack works around. The Window menu's "Zoom" already uses performZoom: down the same path, so this is almost certainly fine, but a RefCell borrow panic on option-click would point here.

  3. Disabled buttons. standardWindowButtonAtEvent filters on hidden, not enabled. In fullscreen the minimize button is disabled (visible in your screenshot as gray) — clicking it used to be silently swallowed, and now falls into AppKit's chrome handling, which could start a titlebar drag instead of doing nothing. Cosmetic at worst.

Explicitly not affected

  • Non-macOS platforms — single Obj-C file, mac-only.
  • WarpPanel (hotkey window) — it has no sendEvent: override, so it never went through this path.
  • The set_window_buttons(false) / hidden-traffic-lights path — standardWindowButtonAtEvent returns nil, unchanged.
  • The resize-edge latch behavior — untouched by this diff.

Suggested manual pass before merge: macOS 27 beta + macOS 26; all three buttons on both key and background windows; custom titlebar heights (short + tall); option-click zoom → un-zoom → check titlebar height; the green-button tiling menu; and a tab drag-out immediately after clicking a traffic light.

@zachlloyd

Copy link
Copy Markdown
Contributor

@warp-agent can you test the likely regression cases for me that you mentioned in the comment on the PR and let me know if you see any regressions?

@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

Went through all six at the current head (594fb74). Short version: I found nothing I'd call a regression, and I made no code change. Items 4 and 5 — the two "if something weird shows up" candidates — turn out to be refuted by the code rather than merely unlikely. The one thing still genuinely open is coverage, not correctness: see the macOS 27 note at the end.

Scope of what's behind each verdict. This pass ran on a Linux runner, so I could not build or run the macOS binary in it — window.m is compiled only on macOS. Verdicts marked empirical cite the macOS 26.3.1 (arm64) session already captured on this PR at this same head. Everything marked code-reasoned is static analysis. I've flagged which is which per item rather than blur them.


First, a correction to my own framing in the earlier comment

I said this change "hands back to AppKit" the dispatch that #12557 deliberately forced, implying it re-opens #12389. Having now read #12557's diff, that's not quite right, and the difference matters.

Before #12557 there was no NSEventTypeLeftMouseDown case in sendEvent: at all — the down fell through default: to [super sendEvent:event], which is exactly where this PR puts it back. Traffic lights were still dead on macOS 27. So the down routing was never the cause. The cause was the other two cases, which hijacked unconditionally:

case NSEventTypeLeftMouseUp:
    [self.contentView mouseUp:event];
    break;
case NSEventTypeLeftMouseDragged:
    [self.contentView mouseDragged:event];
    break;

AppKit on 27 needs the mouse-up delivered back through sendEvent: to complete a traffic-light press, and we were stealing it. #12557 worked around that by forcing [windowButton mouseDown:event], which runs NSButton's own synchronous tracking loop and consumes the up internally — so the button never needs the up to come back.

This PR takes the other route: give the down to AppKit and set _leftMouseDownStartedInNativeWindowChrome = YES so the up and drags follow it to [super sendEvent:] on 27. The latch flip is load-bearing, not incidental — it's the piece that closes the actual #12389 hole, and it's why the modifier semantics (Option → zoom) come back for free. Structurally this is a more faithful fix than the forced dispatch, not a partial revert of it.


1. Traffic-light clicks now depend on AppKit hit-testing — partially applies as residual risk; no regression seen on macOS 26

Empirical (macOS 26.3.1, this head): close and minimize work, plain green → fullscreen, Option+green → stays windowed. That's the recording already on this PR.

Custom titlebar height — code-reasoned, does not apply. The worry was that AppKit clips hit-tests to a superview's bounds where our explicit NSPointInRect(point, button.bounds) did not, so a repositioned traffic light could end up partly outside its parent and become partly dead. Checking the geometry: configure_titlebar_height pins titleBarView top/leading/trailing to the container and gives it heightAnchor == height, so it exactly fills the container; the buttons are 14×14 with centerYAnchor == titleBarView.centerYAnchor + 1. The height Rust actually passes is TOTAL_TAB_BAR_HEIGHT * zoom_factor (Workspace::update_titlebar_height), and ZoomLevel::VALUES bounds the factor to [0.5, 3.5]17.5pt … 122.5pt. Even at the 17.5pt floor a 14pt button centred at +1 spans [2.75, 16.75], comfortably inside [0, 17.5]. The buttons never overflow their superview at any reachable titlebar height, so there's no region for AppKit to clip. Height is the only variable here, so this holds identically with and without the tab bar in the titlebar.

Resize edges: untouched — the eventIsOverResizeEdge branch is byte-identical to #12557.

Not closed: macOS 27 beta. Our cloud runner fleet has a macOS 26 runner but no macOS 27 one, so I can't exercise the original #12389 platform at all. The reasoning above says 27 should be better off than under #12557, but that's an argument, not a click.

2. Window activation/ordering on background windows — code-reasoned: does not apply as a defect

Not exercised. Two things: AppKit treats the standard window buttons as first-mouse targets, which is why closing or minimizing a background window of an already-active app works without raising it in stock macOS apps. And in the worst case — the window does raise on the click — that's the platform-standard behaviour, i.e. a delta from Warp's previously non-standard force-dispatch rather than a bug. Worth an eyeball in a multi-window session; not a blocker.

3. Other native green-button affordances now reachable — applies by design; no titlebar-height risk

Shift/Control-click and the macOS 15+ tiling menu becoming reachable is the intended consequence of routing through AppKit.

On whether the custom titlebar survives a plain zoom or tile (code-reasoned): the window frame changes → the NSTitlebarContainerView frame changes → titlebarContainerFrameDidChange: fires and re-applies _configuredTitlebarHeight (deferred via dispatch_async, skipped while fullscreen). That's not a new pathWindow ▸ Zoom already drives the identical zoom: in shipped builds via zoomAsync: (toggle_maximized in platform/mac/window.rs), and so does every manual window resize. Fullscreen keeps its separate handling in windowWillEnterFullScreen: / windowWillExitFullScreen:. So Option-click zoom → un-zoom reuses machinery that's already exercised daily. Cheap to eyeball on 26 anyway.

4. Stale _leftMouseDownStartedInNativeWindowChrome latch — does not apply

Three independent reasons, all visible in the current code:

  • The latch is reset unconditionally on every NSEventTypeLeftMouseUp that reaches sendEvent:_leftMouseDownStartedInNativeWindowChrome = NO; sits outside the @available branch. So on macOS 27, where the up does come back through sendEvent: after a traffic-light press, that very up clears it.
  • On macOS ≤26 both the up and dragged cases ignore the latch entirely and always route to the content view, so a stale YES isn't even readable there.
  • Every drag sequence starts with a LeftMouseDown, and that case recomputes the latch on both of its branches before anything can read it.

I went looking specifically for a path where a drag or up reaches sendEvent: with no preceding down — a button tracking loop exiting early, the tiling menu's nested event loop — and couldn't construct one: whatever consumes the up also consumes the drags, and the next interaction begins with a fresh down. Worth noting the old code had the same "swallowed up never clears the latch" property; only the resting value differs, and the resting value is always overwritten before it's read.

5. Synchronous zoom re-entrancy into Rust — does not apply

windowDidMove:warp_app_window_moved doesn't touch app state synchronously. It downgrades the window state to a Weak and executor.spawn(...).detach()s the window_moved callback, so it runs on the foreground executor after sendEvent: has unwound (crates/warpui/src/platform/mac/window.rs:1617). No RefCell borrow is taken inside the event dispatch, so there's nothing for a nested callback to collide with.

The sibling path that is synchronous — warp_view_did_change_backing_propertiesdispatch_window_resized — already guards with app::callback_dispatcher().can_borrow_mut() and defers when the app is borrowed, which is the generalised form of the windowWillStartLiveResize workaround. Combined with Window ▸ Zoom already running zoom: down this exact path today, a borrow panic on Option-click isn't reachable this way.

6. Disabled buttons — does not apply in the scenario named; cosmetic ceiling

standardWindowButtonAtEvent does filter on hidden only, so the disabled fullscreen minimize button is still matched and its down goes to [super sendEvent:]. But the feared outcome — a titlebar drag instead of a no-op — isn't reachable there, because macOS doesn't allow dragging a fullscreen window by its titlebar; there's no drag for AppKit to start. Outside fullscreen all three buttons are always enabled on a WarpWindow (warpWindowMask carries Closable | Miniaturizable | Resizable | Titled), so there's no other disabled state to fall into.

This is the one item where I'm reasoning about AppKit's internal treatment of a disabled NSControl rather than about our own code, so confidence is lower than on 4 and 5 — but the blast radius is bounded to "the click does nothing", which is the desired outcome anyway. Adding an enabled filter would only swap the latch from YES to eventIsOverResizeEdge(...) and still end in [super sendEvent:], so it wouldn't change observable behaviour. I left it alone rather than widen the diff.

The "explicitly not affected" list — all four hold

  • Non-macOS platforms. window.m is compiled only when CARGO_CFG_TARGET_OS == "macos" (crates/warpui/build.rs), and it's the only file in the diff. ✔
  • WarpPanel. Its @implementation has no sendEvent: override, so hotkey-window events never entered this path. ✔
  • Hidden traffic lights. set_window_buttons(false) calls setHidden: on all three (platform/mac/window.rs), and standardWindowButtonAtEvent skips hidden buttons and returns nil, so the click falls through to the resize-edge path exactly as before. ✔
  • Resize-edge latch. Line-for-line identical to fix(macos): preserve native window chrome events #12557. ✔

What's still open

Two gaps, in priority order:

  1. macOS 27 beta — nothing can reach it. The three traffic lights plus a resize-edge drag, on both a key and a background window. That's the platform Can't resize, close, minimize, or maximize (macOS 27) #12389 was filed on, and no runner in our fleet is on 27, so this needs a human with a 27 machine. This is the one I'd want closed before merge.
  2. A macOS 26 click-through of items 2, 3 and 6. Cheap and I intend to get these on a macOS runner: clicking yellow on an unfocused window (2), titlebar height and traffic-light positions after Option-click zoom → un-zoom and after a tile (3), clicking the disabled minimize button while fullscreen (6), and a tab drag-out immediately after a traffic-light click (the item-4 symptom). Their verdicts above are code-reasoned; I don't expect any of them to move, but they're worth converting to observations rather than arguments.

Everything else is either empirically confirmed on 26.3.1 at this head, or settled by the code.

@zachlloyd
zachlloyd merged commit 64e3cd4 into master Aug 6, 2026
57 checks passed
@zachlloyd
zachlloyd deleted the factory/app-5157-option-click-zoom branch August 6, 2026 23:04
@ChristmasSun

Copy link
Copy Markdown

thank you!

jwp2987 pushed a commit to jwp2987/phosphor that referenced this pull request Aug 16, 2026
Upstream 64e3cd4 (warpdotdev#14716). On a left mouse down over a standard window
button, sendEvent: sent `mouseDown:` straight at the button and cleared
_leftMouseDownStartedInNativeWindowChrome. Bypassing `super sendEvent:` skips
AppKit's own zoom-button handling, which is what reads the Option modifier to
choose zoom-to-fit rather than maximize, so Option-clicking the green button
behaved like a plain click.

Now the event goes through `super sendEvent:` and the flag is set YES, so the
matching LeftMouseUp/LeftMouseDragged cases also route through super rather
than being redispatched at the content view -- consistent with the resize-edge
path directly below it.

Objective-C only; there is no test surface for it in this repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNUMjBVuwvEnRSgaawUCR3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed warpy-factory Label associated to the "Wob the Wuilder" factory on staging, also known as Warpy Factory.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS: Option-click on the green zoom button enters fullscreen instead of zooming the window

3 participants