Skip to content

Per-OS-window zoom override (refs #1394)#10145

Open
Eridanus117 wants to merge 4 commits intowarpdotdev:masterfrom
Eridanus117:per-window-zoom-fix
Open

Per-OS-window zoom override (refs #1394)#10145
Eridanus117 wants to merge 4 commits intowarpdotdev:masterfrom
Eridanus117:per-window-zoom-fix

Conversation

@Eridanus117
Copy link
Copy Markdown

Refs #1394.

Summary

Cmd+= / Cmd+- / Cmd+0 in one Warp OS window only affects that window. Other windows keep their current zoom, which falls back to the app-wide default (WindowSettings.zoom_level) when the window has no override. The Settings UI "Default Zoom" dropdown still drives the app-wide default; windows without a per-window override follow it.

Conceptually mirrors VS Code's window.zoomLevel (global) + per-window override via window.zoomPerWindow, and Kitty's change_font_size (defaults to current OS window).

Demo: two windows, left at default, right after Cmd+= x3

Implementation shape

Three commits on top of a057a10, +191 / -28 total across 9 files:

commit scope
c3a6997 Window::zoom_factor_override: Option<ZoomFactor> + new AppContext methods window_zoom_factor / set_window_zoom_factor / reset_window_zoom_factor; renderer + Cmd-keys route through per-window override
6d38c6b Cover layout/chrome zoom readers: native macOS titlebar height, traffic-light reservation, tab-bar left padding, theme chooser margin; explicit update_titlebar_height call in adjust/reset because per-window path no longer fires WindowSettingsChangedEvent::ZoomLevel; tightened clamp; unit tests
16cff28 Align set_zoom_factor clamp [0.5, 4.0][0.5, 3.5] so neither setter can leave adjust_zoom's VALUES lookup outside the discrete table

Resolution model:

window_effective_zoom = window.zoom_factor_override ?? AppContext.zoom_factor

Pane-level overrides (PR #9705 implementing per-pane font for #6739) are on a separate axis and untouched:

pane_effective_font = pane.font_size_override ?? font_from(window_effective_zoom)

Why it's app-wide today (current master)

File:line Why
crates/warpui_core/src/core/app.rs:1188 set_zoom_factor Doc-comment explicit: "All views in every window are invalidated when this is invoked." Writes AppContext field, calls invalidate_all_views().
crates/warpui_core/src/presenter.rs:346/347/520 Renderer reads ctx.zoom_factor() from AppContext (every window same value).
app/src/window_settings.rs:73 zoom_level Field lives inside WindowSettings, but WindowSettings is an app-wide singleton (WindowSettings::handle(ctx) / as_ref(ctx) accessors take no WindowId).
app/src/workspace/view.rs:15545-15580 increase_zoom/decrease_zoom/reset_zoom/adjust_zoom write through that singleton.
app/src/settings_view/appearance_page.rs:925 Settings subscriber relays into set_zoom_factor, closing the global path.

Four additional zoom-aware sites in chrome/layout read the same singleton — any per-window scheme that only touches the renderer would leave native macOS titlebar / traffic-light spacing misaligned, so the patch covers those too:

  • app/src/workspace/view.rs:11990 update_titlebar_height
  • app/src/workspace/view.rs:17494 traffic-light reservation
  • app/src/workspace/view.rs:17516 compute_tab_bar_left_padding
  • app/src/themes/theme_chooser.rs:593 traffic-light header margin

Behavioral notes

  • Settings UI "Default Zoom" changes the app-wide default; existing windows without an override follow the new default; windows with an override are not reset (matches VS Code's window.zoomLevel semantics — reset is via Cmd+0 which clears the override).
  • Per-window override is in-memory; closing a window discards its override, app restart restores all windows to the saved global default. Persistence is a separate follow-up.

Out of scope (follow-ups)

  • Persistence of per-window overrides across (a) window close→reopen, (b) workspace reload, (c) app restart — needs stable per-window identity beyond a single process lifetime.
  • Status bar indicator when a window's zoom differs from the default.
  • window.zoomPerWindow: false toggle for users who prefer global-sync legacy behavior.
  • DPI-aware auto-scale across monitors with different scale factors.

Test plan

  • cargo build --bin warp-oss --features gui (build clean)
  • cargo test --package warpui_core zoom_tests (unit tests pass: per-window isolation, global fallback, override reset, missing-window noop, clamp range)
  • Manual: two OS windows side by side, Cmd+= × N in window-A only changes window-A; Cmd+0 returns to default; new window opens at default; native macOS titlebar height tracks per-window zoom.

Posture

Filed as draft — wanting to surface implementation shape against #1394 (canonical per-OS-window tracker per the earlier bot routing on #10115 and #1394) before requesting review. Happy to mark ready-for-review with any specific design preference on the resolution shape, scope, or clamp/range.

When UIZoom is enabled, Cmd++/Cmd+-/Cmd+0 in one window currently
propagates to every window because zoom_factor lives on AppContext and
set_zoom_factor invalidates all views. The handlers also wrote through
WindowSettings::zoom_level, which is an app-wide singleton.

Add a per-window override on top of the existing app-wide default:
  effective_zoom = window.zoom_factor_override ?? AppContext.zoom_factor

- core::Window: zoom_factor_override: Option<ZoomFactor>
- AppContext: window_zoom_factor / set_window_zoom_factor /
  reset_window_zoom_factor; the setters only invalidate the target
  window's views.
- presenter.rs: build_scene + dispatch_event read window_zoom_factor.
- Workspace adjust_zoom / reset_zoom now write the per-window override
  directly. WindowSettings.zoom_level remains the surface for the
  Settings dropdown ("Default Zoom"), which goes through the existing
  AppContext::set_zoom_factor path; windows without overrides follow it.

Aligns with the prior art shape used by VS Code (window.zoomLevel +
per-window override), Kitty (change_font_size affects current OS
window), and iTerm2 (per-session font size on top of profile).
Rev 2 addresses dual-reviewer block on Rev 1: the patch only routed
build_scene + dispatch_event through window_zoom_factor, but four
other zoom readers in the chrome/layout path still consulted the
app-wide WindowSettings::zoom_level. Cmd++/Cmd+- on macOS would
therefore scale the rendered tab bar but leave the native titlebar
height and traffic-light reservation at the global zoom, producing a
visibly broken titlebar.

- view.rs:11990 update_titlebar_height
- view.rs:17494 traffic-light reservation
- view.rs:17516 compute_tab_bar_left_padding
- theme_chooser.rs:593 traffic-light header margin

All four now read ctx.window_zoom_factor(window_id).as_f32().

Workspace::adjust_zoom / reset_zoom now invoke update_titlebar_height
explicitly because the per-window override path no longer mutates
WindowSettings::zoom_level (so WindowSettingsChangedEvent::ZoomLevel
no longer fires for Cmd++/Cmd+-/Cmd+0).

set_window_zoom_factor clamp tightened to [0.5, 3.5] to match
ZoomLevel::VALUES (50%–350%); the original [0.5, 4.0] left a 3.5–4.0
band that adjust_zoom's position lookup couldn't step within.

Add unit tests (crates/warpui_core/src/core/zoom_test.rs) covering:
  - default fallback to AppContext.zoom_factor
  - per-window isolation under set
  - global default change leaves overrides untouched, updates
    non-overridden windows
  - reset clears override
  - missing window_id is silent noop
  - clamp [0.5, 3.5] enforced
The app-wide setter clamped to [0.5, 4.0] while the new per-window
setter clamps to [0.5, 3.5] (matching ZoomLevel::VALUES, 50%–350%).
Asymmetry is observable: a caller that pushes the global default into
the [3.5, 4.0] band would leave non-overridden windows at an effective
zoom that adjust_zoom's discrete-step lookup cannot find in VALUES,
making Cmd++/Cmd+- silently no-op until reset_window_zoom_factor.

No internal caller in this PR drives set_zoom_factor outside the VALUES
range (lib.rs startup and appearance_page.rs settings dropdown both
read percentages straight from VALUES), so the immediate symptom is
not user-reachable. Aligning the clamp now keeps both setters honest
and removes a future-trap ahead of upstream review.
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 5, 2026

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @Eridanus117 on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment @cla-bot check to trigger another check.

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 5, 2026
@Eridanus117
Copy link
Copy Markdown
Author

Signed.
@cla-bot check

CC
@warp-agent
@oss-maintainers

@cla-bot cla-bot Bot added the cla-signed label May 5, 2026
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 5, 2026

The cla-bot has been summoned, and re-checked this pull request!

@Eridanus117
Copy link
Copy Markdown
Author

@oz-agent

@Eridanus117 Eridanus117 marked this pull request as ready for review May 5, 2026 15:34
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@Eridanus117

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

oz-for-oss[bot]
oz-for-oss Bot previously requested changes May 5, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds per-OS-window zoom overrides in WarpUI core and routes renderer, input scaling, and several chrome/layout zoom readers through the window-effective zoom.

Concerns

  • The keyboard zoom path can still become a no-op for valid in-range effective zoom factors that are not exactly present in ZoomLevel::VALUES.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/workspace/view.rs Outdated
let Some(current_index) = crate::window_settings::ZoomLevel::VALUES
.iter()
.position(|zoom| *zoom == current_zoom)
.position(|zoom| *zoom == current_percent)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This exact lookup still dead-ends for any valid in-range effective zoom that is not one of ZoomLevel::VALUES (for example, an app default of 1.3 after reset_window_zoom_factor), so Cmd++/Cmd+- becomes a no-op; snap to the nearest table entry before stepping or store overrides as ZoomLevel percentages.

Addresses oz-agent review on PR warpdotdev#10145: keyboard zoom path could no-op
when the effective zoom factor was a valid in-range value but not
exactly one of `ZoomLevel::VALUES`. The previous implementation looked
up the current percentage in VALUES and stepped by index; if the
current value wasn't in VALUES (e.g. a caller set the global default
to 105% or any non-discrete percentage in [0.5, 3.5]), `position()`
returned `None` and `adjust_zoom` early-returned, leaving Cmd++/Cmd+-
silently inert.

Replace the index lookup with a directional search for the next/prev
VALUES entry strictly above/below `current_percent`, falling back to
the boundary value if there is no further step. Behaviour for values
already in VALUES is unchanged; non-VALUES values now snap to the
nearest VALUES entry in the chosen direction.

Extract the step computation as a free `next_zoom_step` so the logic
can be unit-tested in isolation. Add 8 tests covering: in-VALUES step
up/down, non-VALUES snap up/down, boundary stays (at min/max), and
out-of-range graceful fallback.
@Eridanus117
Copy link
Copy Markdown
Author

Thanks for the review @oz-agent. The concern is valid — even with the [0.5, 3.5] clamp, adjust_zoom would still no-op when the effective zoom landed on a non-VALUES percentage (e.g. 105%, 230%) because the old implementation looked the current percent up by position() in ZoomLevel::VALUES and stepped by index.

Pushed e7a783f: replace the index lookup with a directional search for the next/prev VALUES entry strictly above/below current_percent, falling back to the boundary value if there is no further step. Behaviour for values already in VALUES is unchanged; non-VALUES values now snap to the nearest entry in the chosen direction.

Extracted the step computation as a free next_zoom_step so it's testable in isolation. Added 8 unit tests covering in-VALUES step, non-VALUES snap, and boundary fallback (at min, at max, above max, below min).

cargo test --package warp next_zoom_step → 8 passed.

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@Eridanus117

I'm re-reviewing this pull request in response to a review request.

I reviewed this pull request and requested human review from: @alokedesai.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot dismissed their stale review May 5, 2026 16:35

Oz no longer requests changes for this pull request after the latest automated review.

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds per-OS-window zoom overrides in WarpUI, routes rendering/input and chrome layout through the effective window zoom, and adds focused unit coverage for the new zoom invariants.

Concerns

  • No blocking correctness or security concerns found in the annotated diff.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot requested a review from alokedesai May 5, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant