- Simplify PowerShell discovery, and add support for .NET global tool
installations.
- Fix Git Bash discovery in the case where Zed was launched from within
Git Bash. Git Bash prepends its internal `mingw64\bin` directory to
`PATH`, so `which git` returns a different `bash` executable that causes
the relative path lookup to fail.
Release Notes:
- N/A
# Objective
Fixes#48927. On Windows, when monitors have different DPI scaling
factors, a new window opened on a secondary monitor can be positioned
partially off-screen, with the title bar unreachable.
## Solution
`WindowsWindow::new` calls `CreateWindowExW` with `CW_USEDEFAULT` for
the window's initial position, so Windows picks a starting spot
(typically based on where the previous window was, which is often the
primary monitor) before the window is explicitly moved to its intended
monitor via `SetWindowPlacement`.
`WindowsWindowState::new` immediately reads the window's scale factor
via `GetDpiForWindow(hwnd)` at that point — i.e. the DPI of wherever
Windows initially placed it, not necessarily the target monitor.
`retrieve_window_placement` then used that scale factor to convert the
target monitor's logical bounds into physical pixels. When the two
monitors have different scaling (e.g. 150% vs 100%, as in the linked
issue), this produces the wrong physical rect, leaving the window
off-screen.
The target `WindowsDisplay` already computes its own correct
`scale_factor` from the actual monitor (`get_scale_factor_for_monitor`)
— this is also what `check_given_bounds` already uses. This PR exposes
that value via `WindowsDisplay::scale_factor()` and uses it in
`retrieve_window_placement` instead of the transient window's scale
factor, since the bounds being converted are expressed in logical pixels
for that target display.
## Testing
- Verified the logic by tracing through `WindowsWindowState::new` →
`retrieve_window_placement` → `calculate_window_rect` and confirming the
target monitor's own DPI (not the temporarily-assigned window DPI) is
now used to compute the physical placement rect, matching the approach
`WindowsDisplay::check_given_bounds` already uses for the same
target/temporary-monitor mismatch.
- I wasn't able to build/run Zed locally in this environment (missing
Windows SDK components in the local toolchain, unrelated to this change)
to reproduce the original multi-monitor/mixed-DPI repro steps
first-hand. I'd appreciate a maintainer or anyone who can reproduce
#48927 double-checking on real mixed-DPI hardware.
- Platforms: Windows only (`gpui_windows` crate); no other platform code
touched.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments (no new unsafe
blocks added)
- [ ] The content adheres to Zed's UI standards (N/A, no UI change)
- [ ] Tests cover the new/changed behavior (no existing test harness for
Windows-specific placement logic; happy to add one if pointed at the
right pattern)
- [x] Performance impact has been considered and is acceptable (no
measurable impact; same number of DPI queries, just reading from the
already-computed `WindowsDisplay` instead of the window)
Release Notes:
- Fixed: new windows on Windows could open partially off-screen when
placed on a secondary monitor with a different DPI scaling factor than
the monitor Windows initially placed them on.
---------
Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
# Objective
`gpui_macos` implements `PlatformWindow::render_to_image` — the
MetalRenderer samples an offscreen target, so a **hidden** window can be
captured. `gpui_windows` does not implement it, so the method falls
through to the trait default and returns `Err("render_to_image not
implemented for this platform")`.
That leaves Windows consumers (tests, headless screenshot harnesses) on
`PrintWindow`-style workarounds, which require the window to actually be
on screen: a DirectComposition swap chain that never presents has no
frame to read, so you get a visible flash — or an off-screen parking
trick — for every capture.
This closes the macOS/Windows gap. Linux (`gpui_linux`) still returns
the trait default; a Blade/wgpu readback is the obvious follow-up but is
not in this PR.
## Solution
The DirectX analogue of the Metal path:
- `DirectXRenderer::draw`'s clear, scene upload and batch encoding are
factored into a shared `render(scene, background_appearance)`. `draw` is
now `render` + `present`; `render_to_image` is `render` + readback.
Nothing about how a frame is produced is duplicated between them, so the
two cannot drift.
- `render_to_image` renders into the **existing** render target —
created at window construction, so no window need ever be shown — then
copies it into a `D3D11_USAGE_STAGING` texture, `Map`s it, and converts
BGRA → RGBA. `RowPitch` padding is honoured per row; the copy loop
carries a safety comment.
- It refuses to run while `skip_draws` is set. That flag marks a pending
device-lost recovery, where the atlas still holds tile references from
the previous device — drawing before the forced re-render rebuilds them
panics in `DirectXAtlasState::texture` (the case
`WindowState::force_render_pending` documents). Returning an error beats
panicking in a capture harness.
- Gated on `cfg(any(test, feature = "test-support"))` to match the trait
method. `gpui_platform`'s `test-support` now forwards to
`gpui_windows/test-support`, the way it already does for `gpui_macos`.
No behaviour change outside `test-support`/`cfg(test)` builds beyond the
`render` factoring, which is a pure code move.
## Testing
- **Windows, functional:** an earlier revision of this patch has been in
production use since June in the `--screenshot` harness of a GPUI app I
ship ([Ferail](https://github.com/jonx/Ferail)), capturing a hidden
window (`show: false`) with no flash. BGRA→RGBA output was verified
against the on-screen rendering.
- **This exact revision** has been cross-checked from macOS only: `cargo
clippy -p gpui_windows --no-default-features --features test-support
--target x86_64-pc-windows-msvc -- -D warnings` is clean, both with and
without `test-support` (clang-cl + `xwin`; `--no-default-features` skips
only the `windows-manifest` embed-resource step, which is irrelevant
here). I'm relying on CI for a native Windows build of this revision —
happy to report back from a Windows host if that's a blocker for review.
- **Not covered by an automated test.** The behaviour needs a live D3D11
device, so it can only be exercised by a Windows-hosted test. Happy to
add one under `gpui_windows` if you'd like it — say the word and I'll
push it rather than guess at the shape you want.
- **Reviewers:** the interesting part is the staging-texture readback in
`directx_renderer.rs`. Worth a second pair of eyes on the `skip_draws`
guard and on premultiplied alpha (the swap chain is
`DXGI_ALPHA_MODE_PREMULTIPLIED`, so non-opaque captures come back
premultiplied — same as the macOS path, but it does mean
semi-transparent regions are not straight-alpha).
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines) — n/a, no UI surface
- [ ] Tests cover the new/changed behavior — see Testing; needs a
Windows host, happy to add
- [x] Performance impact has been considered and is acceptable — the
readback path is `test-support`-only; `draw` is unchanged apart from the
`render` factoring
## Showcase
<!-- TODO: drop in the headless Windows capture (and the macOS one
beside it) -->
---
Release Notes:
- N/A
Co-authored-by: Lukas Wirth <lukas@zed.dev>
… layers
# Objective
olor emoji (COLR glyphs) are rasterized on the GPU by compositing each
glyph layer into a D3D11 render target that is created with
`pInitialData: None` and, until now, never cleared. The texture contents
are undefined: texels not covered by any layer quad retained garbage
(typically leftovers from previous rasterizations via the DXGI
allocation pool).
## Solution
- Clear the render target with `[0, 0, 0, 0]` (premultiplied
transparent) immediately after binding it and before drawing the layers.
## Testing
add test to test this specifically
## Showcase
before :
<img width="1634" height="754" alt="ezgif-1a339c7dea79f905"
src="https://github.com/user-attachments/assets/6e282c46-d2a4-4043-9c1c-7f4ee9232fcc"
/>
after :
<img width="1677" height="690" alt="Screenshot 2026-08-26 015248"
src="https://github.com/user-attachments/assets/c922909e-c70b-49f0-88fd-5a0c7cbe574b"
/>
---
Release Notes:
- Windows: Fixed artifacts around color emoji glyphs
Handle the `WM_QUERYENDSESSION` and `WM_ENDSESSION` messages, which are
used by Windows when shutting down an application. For example, Restart
Manager uses this mechanism to gracefully close applications (e.g. when
installing an update to the application).
Release Notes:
- N/A
# Objective
Fixes#57701.
When Zed is launched with `--user-data-dir`, a Zed-initiated
restart—such as `workspace: clear trusted worktrees` or Restart to
Update—can relaunch Zed without that argument. The restarted process
then uses the default data directory instead of the directory selected
by the user.
## Solution
- Store the resolved custom data directory as GPUI restart arguments
alongside the existing optional restart executable path.
- Forward those arguments when relaunching on Linux, macOS, and Windows.
- Preserve argument boundaries and native `OsString` values, including
paths containing spaces.
- On Windows updates, carry the arguments through
`auto_update_helper.exe` when it launches the updated `Zed.exe`.
Normal launches without `--user-data-dir` continue to restart without
additional arguments. This does not attempt to preserve unrelated CLI
arguments or general launch state.
## Testing
- `cargo fmt --all -- --check`
- `cargo test -p gpui`
- `cargo test -p auto_update --features workspace/test-support`
- `cargo check -p auto_update_helper --tests --target
x86_64-pc-windows-msvc`
- `./script/clippy -p gpui -p gpui_macos -p auto_update -p zed`
- `./script/clippy -p auto_update_helper --target
x86_64-pc-windows-msvc`
The regression tests cover simultaneous restart executable and argument
forwarding, Windows paths containing spaces and trailing backslashes,
updater-helper argument parsing, and the final `Zed.exe` command.
Compilation and unit tests were run on macOS. Linux and Windows runtime
behavior was reviewed but not run locally. A full `gpui_windows`
cross-target check was attempted, but its `psm` dependency requires the
MSVC `lib.exe` tool, which is not available on this macOS host; the
standalone Windows updater helper and its tests compile successfully for
the Windows target.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards (no UI changes)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed Zed forgetting an explicitly passed `--user-data-dir` argument
after restarting (e.g. clicking "Restart to Update")
---------
Co-authored-by: Tom Houlé <tom@tomhoule.com>
Factoring some changes out of
https://github.com/zed-industries/zed/pull/61461:
- On Metal and wgpu, bulk-upload instance buffers at the start of the
frame rather than uploading chunks as we draw them. The depth buffer
patch needs this architecture since all opaque quads in the scene are
painted before anything else. This also lets us get rid of the awkward
retry loop when the GPU buffer is too small for the scene.
- On Windows, use a constant buffer for the instance offset rather than
creating SRVs with different `FirstElement`s. `FirstElement` is buggy on
some vendors' drivers, and reusing the SRV object should reduce D3D
runtime overhead.
- Misc. refactoring: remove redundant parameters, deduplicate code,
create fewer bind groups on wgpu, etc.
Fixes https://github.com/zed-industries/zed/issues/49588
Fixes https://github.com/zed-industries/zed/issues/59192
Release Notes:
- Fixed an issue where UI elements would flicker on some Windows
devices.
*As an LLM was used to translate the PR message, the original text is
included alongside the translation in accordance with the contributing
guidelines.
# Objective
> On Windows, GPUI accepted `WindowKind::PopUp` and `focus: false`, but
the window creation path ignored both options. This caused two problems
with the notification popup shown when an agent finished its work:
>
> * When another window had focus, the popup could appear behind it.
> * When Zed had focus, the popup could steal focus.
>
> This pull request applies `WindowKind::PopUp` and `focus: false` to
windows on Windows, bringing popup behavior in line with other operating
systems.
Windows版GPUIでは、`WindowKind::PopUp`と`focus:
false`を指定しても、それらのオプションが実際のウィンドウ生成処理では無視されていました。そのため、エージェントの作業が完了したときに表示される通知で次のような問題が発生していました。
- ほかのウィンドウにフォーカスがある場合にポップアップがほかのウィンドウの背後に隠れてしまう
- Zedにフォーカスがある場合はポップアップがフォーカスを奪ってしまう
このプルリクエストでは、Windows上でも`WindowKind::PopUp`と`focus:
false`をウィンドウに反映させるようにし、ポップアップの挙動をほかのOSと一致させます。
## Solution
> This PR updates popup creation in GPUI on Windows to match the
behavior on other platforms. When you specify `WindowKind::PopUp` and
`focus: false`, GPUI now keeps the popup above other windows without
stealing focus.
>
> Keeping a `focus: false` popup above other windows caused the IME to
commit text under composition when the popup appeared. This PR also
fixes that issue.
Windows版GPUIのポップアップの生成処理をほかのプラットフォームの挙動と一致するように変更しました。このPRでは、`WindowKind::PopUp`と`focus:
false`を指定したときにポップアップウィンドウを最前面に表示し、フォーカスを奪わないようにしました。
また、`focus:
false`のポップアップが最前面に表示されるようにしたことで、ポップアップが表示されたときにIMEで入力中のテキストが意図せず確定されるようになってしまったため、それに対する修正も含まれています。
## Testing
> I built Zed on Windows and displayed a test agent notification to
verify the following:
>
> * The popup stays above other windows when another window has focus.
> * When Zed has focus, a popup with `focus: false` does not steal focus
or interrupt IME input.
> * The buttons in the popup remain clickable.
Windows上でZedをビルドし、テスト用のエージェント通知を表示して次の項目をテストしました。
- ほかのウィンドウにフォーカスがある場合でもポップアップが最前面に表示される
- Zedにフォーカスがある場合に`focus: false`のポップアップがフォーカスを奪わず、IMEを使った入力も阻害しない
- ポップアップ内のボタンをクリックできる
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
> Note: I added a button to display an agent notification for testing in
this video, but this PR does not include that button.
※この動画ではテスト目的でエージェント通知を表示するボタンを追加していますが、このPRの変更には含まれていません。
### Before
The popup window is hidden when another window has focus:
https://github.com/user-attachments/assets/5312729b-1409-4c81-a169-dd031099059b
A popup with `focus: false` steals focus:
https://github.com/user-attachments/assets/dd1ac2a2-d5e8-4f8f-bc47-6cc659a10951
### After
The popup window stays on top even when another window has focus:
https://github.com/user-attachments/assets/af058921-14fc-4ca5-9698-758202c4c888
A popup with `focus: false` does not steal focus or interrupt text
input:
https://github.com/user-attachments/assets/9bc487c6-ae68-4ec0-9073-1257b91e06a9
---
Release Notes:
- Fixed Windows agent notifications appearing behind other windows,
stealing input focus, or committing active IME composition.
---------
Co-authored-by: John Tur <john-tur@outlook.com>
GPUI allocates every element for a draw in a single per-App bump arena,
and `Arena::clear()` assumed no draw was in progress. But draws can
nest: on Windows, the window procedure re-enters whenever the main
thread pumps messages mid-draw (cross-thread `SendMessage` dispatch,
modal message loops entered by COM/OLE calls such as clipboard reads),
and on any platform a draw can be triggered from within another draw
(e.g. `open_window`). When a nested draw finished, its arena clear freed
and rewound memory the outer draw was still using. The `ArenaBox`
validity flag only guards new derefs, so `&mut` references already held
by the outer draw silently pointed into reused memory. That
use-after-free corrupted element-state keys
(`GlobalElementId`/`SharedString` Arcs) and heap metadata, crashing
later in innocent-looking frames — seen in the wild as
`EXCEPTION_ACCESS_VIOLATION_READ / 0xffffffffffffffff` in
`Frame::finish` and element allocation
([ZED-9QN](https://zed-dev.sentry.io/issues/7577818040/),
[ZED-7JC](https://sentry.io/organizations/zed-dev/issues/7465778926/),
[ZED-7C6](https://zed-dev.sentry.io/issues/7460258444/)), plus
occasional "attempted to dereference an ArenaRef after its Arena was
cleared" panics (ZED-96P, ZED-8XN), Windows-dominant and spanning
versions 1.1.7–1.8.2.
The fix makes the arena sound under nesting: `ElementArenaScope` tracks
a scope depth on the arena, and `Arena::clear()` is deferred while any
scope is active — the outermost draw's clear drops both draws'
allocations. Since arena chunks are stable heap blocks and allocation
only appends, nested allocation was already safe; the mid-draw clear was
the only destructive operation. Scopes are ended via a consuming
`exit(arena)` call that asserts arena identity (by pointer comparison,
never dereferencing), and `ArenaClearNeeded::clear(cx)` reaches the
arena through the App, so this bookkeeping contains no unsafe code
beyond what existed before. A panic that unwinds a draw balances the
scope depth in the guard's `Drop`, so later clears still run; only the
unwound draw's own clear token is never produced, leaving the arena
populated until the next draw's clear — a one-frame leak at worst, never
a use-after-free.
Additionally, draws are no longer run re-entrantly: GPUI's
`on_request_frame` callback skips requests that arrive while a draw is
already on the thread's stack (remembering `force_render` for the next
frame), and on Windows a `DrawCoordinator` owned by the platform and
shared with every window additionally guards the wider `draw_window`
span (presentation, IME updates). Deferred windows validate their update
region (so nested message pumps don't busy-loop on WM_PAINT) and are
repainted at most one vsync later by the vsync thread's existing
per-tick invalidation.
An integration test opens a window from within an element's paint;
without the deferred clear it reproduces the exact "ArenaRef after its
Arena was cleared" panic seen in the wild. Deferrals are logged so the
diagnosis can be confirmed from user logs.
Closes FR-110
Closes FR-114
Release Notes:
- Fixed a crash on Windows caused by re-entrant window drawing
corrupting UI element memory.
# Objective
Unify dependencies inside of the root `Cargo.toml` to prevent versions
from going out of sync and creating copies of dependencies.
## Testing
- All tests pass, nothing touches the `Cargo.lock`
## Review
Review is easiest by going through commit by commit.
Release Notes:
- N/A or Added/Fixed/Improved ...
Restore an optimization that I mistakenly removed in
https://github.com/zed-industries/zed/pull/54728.
Drawing a quad with a border but no fill color will run the quad
fragment shader for every transparent interior pixel, which is
especially costly when the quad is large. Instead, split these quads
into four non-overlapping strips that cover the regions where borders
are painted. The side strips own the straight left and right edges,
while the top and bottom strips own the horizontal edges and the rounded
corners.
Previously, this optimization only applied to borders drawn by the Taffy
layout. I've decided to reinstate the logic directly in `paint_quad` so
that the optimization can be applied more generally... though if this
feels like too much policy, we can move it back.
This reduces the GPU time spent painting a representative Zed scene by
about 20% or so.
Release Notes:
- N/A
Release Notes:
- On agent notifications, the corresponding zed window will now mark
itself for OS level attention
---------
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
This hardens the Windows text rendering path in gpui_windows against
undefined behavior and out-of-bounds access. DirectWrite hands back
glyph-run arrays (glyph indices, advances, offsets) and cluster maps as
raw pointers that may be null; these are now interpreted through a
shared `slice_from_nullable` helper that treats a null pointer with zero
length as an empty slice and returns `E_INVALIDARG` when a null pointer
arrives with a nonzero length, instead of constructing a slice from a
null pointer. It also fixes a provenance bug: the renderer context
passed to `IDWriteTextLayout::Draw` was derived via `&raw const` from a
non-mut binding, but the `DrawGlyphRun` callback recovers that pointer
and writes through it, which is undefined behavior under Stacked/Tree
Borrows; the binding is now `mut` and the pointer is derived from `&raw
mut` so its provenance matches the write (codegen is unchanged).
On the DirectX side, the atlas upload is now bounded by the source slice
length so a mismatched destination region cannot read past the end of
the source, and the color-glyph staging texture is unmapped after
readback so the mapping is no longer leaked and the resource pinned. The
color-glyph rasterizer drives the shared, non-thread-safe D3D11
immediate context that `DirectXRenderer` and `DirectXAtlas` also use, so
it must stay on the main UI thread — which it always is today; a comment
documents that invariant.
Since gpui_windows is a Windows-only crate, it cannot be compiled on
non-Windows hosts, so Windows CI is the compile gate for these changes.
Release Notes:
- N/A
Dropping a scheduled runnable cancels its task and makes the next poll
of any awaiter panic with "Task polled after completion." The only paths
where we drop these runnables seem to be during shutdown or extreme
resource exhaustion, so, let's leak the runnables instead of crashing.
On Windows, we also moved to calling the Win32 thread pool API directly,
because 1) WinRT thread pool API is just a wrapper that adds overhead we
don't need, and 2) the closure we pass to the `WorkItemHandler` object
takes ownership of the runnable object, so if the WinRT thread pool
releases the delegate, it can free the runnable without our control.
Release Notes:
- N/A
`PolychromeSprite` in `crates/gpui/src/scene.rs` is `#[repr(C)]` and had
a `grayscale: bool` field followed by 3 compiler-inserted padding bytes
that were never written. The wgpu renderer's `instance_bytes`
reinterprets `&[PolychromeSprite]` as `&[u8]` via
`slice::from_raw_parts` and passes it to `queue.write_buffer`, so those
uninitialized padding bytes were exposed behind a shared `&[u8]` on
every frame that draws an image or emoji, which is undefined behavior.
Rather than widening the field to a raw `u32` (which would suggest
values other than 0 and 1 are meaningful), this introduces
`PaddedBool32`: a `#[repr(transparent)]` wrapper around `u32` whose only
public constructor is `From<bool>`, so the 0-or-1 invariant is enforced
by the type while the layout has no padding. `Underline.wavy`, which was
already a raw `u32` for the same reason, is converted too.
cbindgen emits the wrapper as `typedef uint32_t PaddedBool32;`, so the
generated Metal header and shaders are unchanged. The WGSL and HLSL
shaders already declared these fields as `u32`/`uint`; their `& 0xFFu`
masks, which existed to ignore the garbage padding bytes, are now
simplified to plain comparisons.
Release Notes:
- N/A
Checks that SetWindowCompositionAttribute was found before transmuting
and calling the function pointer, making the composition setup a no-op
when the undocumented export is unavailable.
Release Notes:
- N/A
# Objective
gpui can't show UI that extends past the window it belongs to. Menus,
dropdowns and tooltips are drawn as elements inside the window, so they
clip at its edges. This PR adds a window kind for platform-native popups
anchored to a parent window, as groundwork for real native menus,
dropdowns and tooltips.
## Solution
`WindowKind::AnchoredPopup(PopupOptions)` opens a popup positioned
relative to a parent window. Instead of giving the popup an absolute
position, you describe where it should go and the platform figures out
the rest:
- `parent`: the window to anchor to
- `anchor_rect`: a rectangle in the parent, e.g. the button that opened
the menu
- `anchor` and `gravity`: which point of that rect to attach to, and
which direction to grow
- `constraint_adjustment`: what the platform may do if the popup would
leave the screen (slide, flip, resize)
- `grab`: menu behavior, the popup takes focus and is dismissed when
clicking outside the app
The popup's size comes from `WindowOptions::window_bounds`.
This model mirrors Wayland's `xdg_positioner`, where the compositor owns
positioning and the client can only describe intent. Since that's the
most restrictive case, the other platforms can implement the same
description later with simple math against screen bounds.
Only Wayland is implemented so far, via `xdg_popup` on top of the
existing surface implementation. Popups can be parented to toplevels,
layer-shell surfaces (a menu opened from a panel) and other popups
(nested menus). macOS, Windows, X11 and web reject the kind with
`PopupNotSupportedError`, so callers can detect that and fall back to
in-window popovers.
Some Wayland details that might help during review:
- Anchor rects are translated from gpui coordinates into the parent's
window geometry space and clamped to it. A rect outside the geometry, or
with zero size, is a fatal protocol error
- Resizing a mapped popup goes through `xdg_popup.reposition`
- Mouse press serials are now recorded on press only, not release.
Compositors decline grabs and interactive moves that reference a release
serial
## Testing
Tested manually on Wayland with an example app: the menu opens anchored
below its button, extends past the parent window, flips above the button
near the bottom of the screen, and a grabbing popup is dismissed when
clicking into another application.
Nested menus were tested in one of my projects (ignore that they are
ugly, that's just a prototype 😛):
https://github.com/user-attachments/assets/2cd3e2e9-87f7-4b02-986f-48e5633e205c
I also have a complete runnable example demonstrating it. I did not add
it to the PR, because this might give the impression that
`WindowKind::AnchoredPopup` are a complete implementation, despite only
working on wayland so far:
<details>
<summary>Click to view example</summary>
```rust
//! Example and manual test for platform-native popups (`WindowKind::AnchoredPopup`).
//!
//! A native popup is a real, parent-anchored window that can extend beyond its parent onto the
//! screen, unlike gpui's in-window popovers. Run it, open the menu, and confirm the points listed
//! in the window. On a platform without an implementation the button reports that popups are not
//! supported instead of opening anything.
//!
//! Run with: cargo run -p gpui --example popup
#![cfg_attr(target_family = "wasm", no_main)]
use gpui::{
AnyWindowHandle, App, Bounds, Context, MouseButton, SharedString, Window, WindowBounds,
WindowHandle, WindowKind, WindowOptions, div, point, popup::*, prelude::*, px, rgb, size,
};
use gpui_platform::application;
/// The trigger button, at a fixed position so the popup can anchor to a known rectangle. Real code
/// would anchor to the measured bounds of whatever element opens the popup.
const BUTTON_BOUNDS: Bounds<gpui::Pixels> = Bounds {
origin: point(px(24.), px(24.)),
size: size(px(200.), px(32.)),
};
const POPUP_SIZE: gpui::Size<gpui::Pixels> = size(px(260.), px(320.));
struct Menu;
impl Render for Menu {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let item = |label: &str| {
div()
.id(label.to_string())
.px_3()
.py_1()
.rounded_sm()
.hover(|this| this.bg(rgb(0x3a3a3a)))
.cursor_pointer()
.child(label.to_string())
.on_click(|_, window, _| window.remove_window())
};
div()
.id("menu-root")
.size_full()
.p_1()
.flex()
.flex_col()
.gap_0p5()
.bg(rgb(0x2a2a2a))
.text_color(gpui::white())
.rounded_md()
.border_1()
.border_color(rgb(0x454545))
.child(item("Foo"))
.child(item("Bar"))
.child(item("Baz"))
.child(item("Qux"))
.child(item("Alice"))
.child(item("Bob"))
}
}
struct PopupExample {
menu: Option<WindowHandle<Menu>>,
status: SharedString,
}
impl Default for PopupExample {
fn default() -> Self {
Self {
menu: None,
status: "Click \"Open menu\" to open a native popup.".into(),
}
}
}
impl PopupExample {
/// Closes the menu if it is open. Returns true if a menu was actually open.
fn close_menu(&mut self, cx: &mut App) -> bool {
match self.menu.take() {
Some(menu) => menu
.update(cx, |_, window, _| window.remove_window())
.is_ok(),
None => false,
}
}
fn toggle_menu(&mut self, parent: AnyWindowHandle, cx: &mut App) {
if self.close_menu(cx) {
return;
}
match open_menu(parent, cx) {
Ok(menu) => {
self.menu = Some(menu);
self.status = "Menu open. Dismiss it by selecting an item, clicking elsewhere in \
this window, or clicking another application."
.into();
}
// A real application would fall back to an in-window popover here.
Err(error) => {
self.status = format!("Failed to open a native popup: {error}").into();
log::error!("failed to open popup: {error}");
}
}
}
}
impl Render for PopupExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let bullet = |text: &str| div().child(format!("• {text}"));
div()
.id("root")
.size_full()
.bg(rgb(0xf7f7f7))
.text_color(rgb(0x222222))
// Same-app clicks don't auto-dismiss a grabbing popup (see `PopupOptions::grab`).
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _window, cx| {
this.close_menu(cx);
}),
)
.child(
div()
.size_full()
.p_5()
.pt(px(76.))
.flex()
.flex_col()
.gap_3()
.child(div().text_xl().child("Native popup test"))
.child(div().text_sm().child(
"WindowKind::AnchoredPopup opens a real, parent-anchored window that can \
extend past this window onto the screen. Only some platforms implement \
it so far.",
))
.child(
div()
.flex()
.flex_col()
.gap_1()
.text_sm()
.text_color(rgb(0x555555))
.child(div().child("Verify:"))
.child(bullet("The menu opens anchored below the button."))
.child(bullet(
"The menu extends past the bottom edge of this window.",
))
.child(bullet(
"Near the bottom of the screen, the menu flips above the button.",
))
.child(bullet("Clicking another application dismisses the menu."))
.child(bullet(
"Selecting an item or clicking in this window dismisses it.",
)),
)
.child(
div()
.text_sm()
.text_color(rgb(0x333333))
.child(self.status.clone()),
),
)
.child(
div()
.absolute()
.left(BUTTON_BOUNDS.origin.x)
.top(BUTTON_BOUNDS.origin.y)
.w(BUTTON_BOUNDS.size.width)
.h(BUTTON_BOUNDS.size.height)
.flex()
.items_center()
.justify_center()
.bg(rgb(0xffffff))
.border_1()
.border_color(rgb(0xd0d0d0))
.rounded_md()
.cursor_pointer()
.id("open-menu")
.active(|this| this.bg(rgb(0xeeeeee)))
.child("Open menu ▾")
// Open on mouse-down, not on click, so the grab is taken while the button is still held.
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
// Don't let the window handler above close the menu we are opening.
cx.stop_propagation();
this.toggle_menu(window.window_handle(), cx);
}),
),
)
}
}
fn open_menu(parent: AnyWindowHandle, cx: &mut App) -> anyhow::Result<WindowHandle<Menu>> {
cx.open_window(
WindowOptions {
titlebar: None,
// Sizes the popup. The platform decides the position, so the origin is ignored.
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(0.), px(0.)),
size: POPUP_SIZE,
})),
kind: WindowKind::AnchoredPopup(PopupOptions {
parent,
anchor_rect: BUTTON_BOUNDS,
// Anchor to the button's bottom-left and grow down-right so the menu drops beneath it.
anchor: PopupAnchor::BottomLeft,
gravity: PopupGravity::BottomRight,
// Slide horizontally and flip vertically if the menu would leave the screen.
constraint_adjustment: PopupConstraintAdjustment::SLIDE_X
| PopupConstraintAdjustment::FLIP_Y,
offset: point(px(0.), px(4.)),
// Grab input so the compositor dismisses the popup on clicks into other applications.
grab: true,
}),
..Default::default()
},
|_, cx| cx.new(|_| Menu),
)
}
fn run_example() {
application().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(100.), px(100.)),
size: size(px(420.), px(300.)),
})),
..Default::default()
},
|_, cx| cx.new(|_| PopupExample::default()),
)
.unwrap();
cx.activate(true);
});
}
#[cfg(not(target_family = "wasm"))]
fn main() {
run_example();
}
#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
gpui_platform::web_init();
run_example();
}
```
</details>
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- N/A
## Summary
- Store Windows window minimizable and resizable capabilities on
`WindowsWindowInner`.
- Prevent custom titlebar hit testing from returning minimize/maximize
button hit targets when the corresponding capability is disabled.
- Swallow disabled native non-client minimize/maximize button mouse
events so they cannot fall through to the default window procedure or
GPUI's manual `ShowWindowAsync` handling.
Fixes#52067.
## Validation
- `cargo fmt --package gpui_windows`
- `cargo check -p gpui_windows`
- `git diff --check`
Release Notes:
- Fixed disabled minimize and maximize window controls still activating
on Windows.
I encountered an issue where I couldn't sign in to ChatGPT Subscription
on Windows:
```
ChatGPT subscription sign-in failed to persist credentials:
Failed to write credentials to Windows Credential Manager:
占位程序接收到错误数据。 (0x800706F7)
```
Interestingly, only one of my three ChatGPT accounts had this problem —
the other two signed in successfully.
## What I Found
After investigating, I noticed that the OAuth scope in
`openai_subscribed.rs` requests 6 permissions:
```
openid profile email offline_access api.connectors.read api.connectors.invoke
```
I wrote a test script to measure token sizes and found that:
- With 6 scopes: my problematic account's token was **2578 bytes**
- With 4 scopes (removing `api.connectors.read` and
`api.connectors.invoke`): the same token was **2516 bytes**
Since Windows Credential Manager has a 2560-byte limit
(`CRED_MAX_CREDENTIAL_BLOB_SIZE`), this could explain why some accounts
fail while others succeed — it depends on the base token size.
## My Hypothesis
The issue seems to be that:
- Error code `0x800706F7` = `RPC_X_BAD_STUB_DATA` (size limit, not
format)
- Accounts with larger base tokens exceed the limit when the extra 48
chars are added
- Accounts with smaller base tokens still fit, which is why this isn't
universally reproducible
I'm not 100% certain this is the root cause, but the evidence seems to
point in this direction.
## Proposed Fix
I removed `api.connectors.read` and `api.connectors.invoke` from the
OAuth scope, since:
- These scopes don't appear to be used by Zed's current ChatGPT
integration
- OpenCode (another tool using the same OAuth provider) successfully
uses only 4 scopes
- This fix allowed my problematic account to sign in
However, I'd like to ask the maintainers:
- Are there plans to use OpenAI Connectors API in the future?
- Are there security or compliance reasons for requesting these extra
scopes?
If the answer is yes to either, we'd need a different approach (e.g.,
splitting credentials, using encrypted file storage, or checking token
size before storage).
## Testing
- [x] Verified my problematic account now signs in successfully
- [x] Verified my other accounts still work
- [x] Ran `cargo check -p language_models` (compilation successful)
- [ ] Would appreciate help testing on macOS/Linux
## Questions
1. Does this analysis make sense?
2. Are there any other considerations I'm missing?
3. Would it be helpful to add a size check with a clearer error message
for future cases?
I'm happy to adjust this approach based on your feedback.
Release Notes:
- Fixed ChatGPT Subscription sign-in failing on Windows for some
accounts by removing unused OAuth scopes (`api.connectors.read`,
`api.connectors.invoke`) that pushed JWT tokens over Windows Credential
Manager's limit.
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
# Objective
GPUI apps currently don't have a cross-platform way to react when the
system wakes from sleep.
## Solution
Add `Application::on_system_wake`, backed by platform hooks for macOS,
Windows, and Linux. The platform listeners are registered lazily once an
app installs a wake callback.
## Testing
- N/A
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
This removes gpui's dependency on the `util` crate, which depends on
async process. It now depends on `gpui_util` only, and uses
`std::process::Command` for the two cases where it was previously using
an async `Command`. This lifts the requirement of non-Zed consumers of
gpui to depend on our forked async-process (see #59156).
Release Notes:
- N/A
Partially addresses #54659
`PlatformAtlas::remove` removed the tile from `tiles_by_key` and
decremented the texture's refcount, but never returned the tile's
rectangle to the etagere allocator. Removed tiles' space could only be
reclaimed if the entire texture was dropped, which doesn't happen if
glyph/emoji share the texture. Because we don't have correct glyph/emoji
removal
I added regression tests on all three platforms to prevent this from
occurring as well.
Note: this doesn't fix glyph eviction or the nil-texture panic from
#54659; those are follow ups.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes #ISSUE
Release Notes:
- gpui: Fix GPUI memory growing unboundedly when images are repeatedly
loaded and released
We have a lot of long blocking tasks on both the foreground and
background, this is a start of getting some insight into those.
We will now log tasks running longer then 100ms on the foreground or
background. Hanging actions will also be logged including their name. We
simultaneously collect statistics on task and action performance and
send those to telemetry. This includes quantiles and averages for each
hanging task.
Finally this adds tree dev actions:
- hang action
- hang foreground
- hang background
These cause a hang to check if hang reporting is working and in the
future telemetry.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes #ISSUE
Release Notes:
- Added logging and telemetry of tasks and actions with performance
issues
GPUI AccessKit integration
This PR is replacing #51097 , and is much more limited in scope. This PR
*ONLY* adds AccessKit support to GPUI, and doesn't touch Zed. Once this
lands, we can start adding aria attributes to Zed's components.
This PR is the first step to addressing #41138 .
Release Notes:
- N/A or Added/Fixed/Improved ...
---------
Co-authored-by: John Tur <john-tur@outlook.com>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
This PR adds support for inset shadows in the box shadow through the
`inset: true` field. It includes support for both the macOS as well WGSL
shaders. For now, there is no immediate application of it in Zed, so
nothing should change in the app.
<img width="600" alt="Screenshot 2026-05-25 at 8 46@2x"
src="https://github.com/user-attachments/assets/db564a6b-8af5-491a-a573-17c060a3647c"
/>
Run the example above with `cargo run --example shadow -p gpui`.
Release Notes:
- N/A
---------
Co-authored-by: Agus Zubiaga <agus@zed.dev>
We were incorrectly calling this with a thread handle, additionally
changing the process priority here doesn't make sense, so just drop
this.
Release Notes:
- N/A or Added/Fixed/Improved ...
---------
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
## Problem
A Sentry-reported crash on Windows (Intel Iris Xe Graphics, v1.0.1):
```
index out of bounds: the len is 1 but the index is 1
```
panicking at `DirectXAtlasState::texture` in
[`crates/gpui_windows/src/directx_atlas.rs`](https://github.com/zed-industries/zed/blob/main/crates/gpui_windows/src/directx_atlas.rs):
```rust
AtlasTextureKind::Subpixel => {
&self.subpixel_textures[id.index as usize].as_ref().unwrap()
}
```
## Root cause
After a GPU device-lost recovery, GPUI's view cache replays stale
`AtlasTile`
references from the previous frame's `paint_operations` via
`Scene::replay`.
1. **Atlas grows past one texture.** A long enough session pushes
`subpixel_textures.textures.len() ≥ 2` (easy on Iris Xe at the default
1024×1024 atlas size). Top-level views in Zed use `cached(...)`, so
their
`AnyViewState.paint_range` records into
`rendered_frame.scene.paint_operations`,
referencing both index `0` and index `1`.
2. **Device lost.** `handle_device_lost` clears every `AtlasTextureList`
(`textures.len() == 0`)
and `tiles_by_key`, then sets `skip_draws = true`.
3. **`WM_GPUI_FORCE_UPDATE_WINDOW` arrives.** `mark_drawable()` flips
`skip_draws` back to `false` and `request_frame` runs with
`force_render: true`.
4. **The cache hit.** Inside `Window::draw`, `AnyView::prepaint`'s cache
check (`!dirty_views.contains(...) && !window.refreshing`) succeeds for
every cached view because the recovery doesn't touch invalidator state
and
`force_render` doesn't propagate into `Window`. `AnyView::paint` calls
`window.reuse_paint` → `Scene::replay` → `primitive.clone()`, which
(since
`SubpixelSprite`/`AtlasTile` are `Copy`) verbatim copies a
`Primitive::SubpixelSprite { tile: { texture_id: { index: 1, ... }, ...
} }`
into `next_frame.scene`.
5. **Atlas regrows to one.** Dirty/uncached parts of the same frame
(caret, animations, anything that called `cx.notify`) fall through to
`paint_glyph` → `get_or_insert_with` → `push_texture`, growing
`subpixel_textures.textures` from `0` to **`1`** with index `0` valid.
6. **Panic.** After `mem::swap`, `rendered_frame.scene` contains a mix
of
fresh `index = 0` and replayed `index = 1` sprites. `Scene::batches`
emits separate batches per `texture_id`; the `index = 1` batch reaches
`atlas.get_texture_view` → `subpixel_textures[1]` → panic with
`len = 1, index = 1`.
The two earlier related fixes do not catch this:
- **#52389 / c8f20c78** (`if force_render { mark_drawable }`) protects
the
200 ms recovery sleep — pending `WM_PAINT`s carry `force_render = false`
and so do not clear `skip_draws`. But `WM_GPUI_FORCE_UPDATE_WINDOW`
carries `force_render = true`, so `mark_drawable` runs, then
`Window::draw`'s `reuse_paint` still reproduces stale tiles.
- The unmerged Windows draft `2e5d890e37`
(`force_render_after_recovery`)
similarly only forces the forced-render branch — it doesn't bypass the
view cache.
## Fix
Two parts:
**1. Bypass the view cache on a forced draw (cross-platform).**
In the platform-agnostic `request_frame` closure in `Window::new`, call
`window.refresh()` whenever `RequestFrameOptions::force_render` is
`true`.
`Window::refresh` is the documented escape hatch for cached views (per
the
`AnyView::cached` docs: *"The one exception is when [Window::refresh] is
called, in which case caching is ignored."*). With `refreshing = true`
every `AnyView::prepaint` cache check fails, every cached view fully
repaints, and `paint_glyph` allocates fresh tiles for every glyph, so
`rendered_frame.scene` ends up free of stale `AtlasTile`s.
**2. Add the `force_render_after_recovery` flag on Windows.**
Mirror the Linux fix from #52389: a per-window `Cell<bool>` set after
`WindowsWindowInner::handle_device_lost` succeeds and consumed at the
top
of `draw_window`. Together with the GPUI change above, the first frame
after recovery (whether a stray `WM_PAINT` during the 200 ms recovery
sleep or the explicit `WM_GPUI_FORCE_UPDATE_WINDOW`) is treated as a
forced render that both clears `skip_draws` and bypasses the view cache.
## Testing
- `script/clippy -p gpui` is clean.
- I do not have a Windows toolchain available locally, so I have not
cross-compiled `gpui_windows`. Reviewers with Windows access — please
smoke-test on a machine where the device-lost path can be exercised
(Intel iGPU, suspend/resume, or running a TDR-inducing test on a GPU
driver).
## Related
- Sentry issue ID 7457971403 (DirectX subpixel atlas crash, Intel Iris
Xe).
- Builds on / fixes the residual gap in #52389 (`gpui_linux: Force scene
rebuild after GPU device recovery"). The GPUI change here also hardens
the corresponding Linux path against the same `reuse_paint` mechanism.
Release Notes:
- Fixed a crash on Windows when the GPU device is lost and recovered
during use (typically driver crash, suspend/resume, or display
reconfiguration, most commonly on Intel iGPUs)
Fixes ZED-5K1
Release Notes:
- Fixed a panic on windows when a monitor disappears from windows
monitor enumeration
---------
Co-authored-by: John Tur <john-tur@outlook.com>
Instead of manually handing hiding the cursor on keyboard input at the
editor level, GPUI will now take care of it.
This makes it significantly easier to handle the edge cases, and allows
delegating the cursor restoration to the platform itself in the macOS
case. On Linux and Windows, we still have to restore the cursor on
movement ourselves, but this now happens at the platform-specific level.
Bugs fixed by this change:
- No cursor when "Unsaved edits" prompt appears
- Cursor disappears when clicking a panel button if it contains a search
bar (e.g. collab panel)
### Setting rename
The `hide_mouse` setting value `"on_typing_and_movement"` has been
renamed to `"on_typing_and_action"` to better reflect what it actually
does — it hides the cursor when a keystroke resolves to an action (e.g.
cursor movement, deletion). Existing settings are migrated
automatically.
### Tested platforms
- [x] macOS
- [x] Wayland
- [x] X11
- [x] Windows
- [x] Web
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Renamed the `hide_mouse` setting value `on_typing_and_movement` to
`on_typing_and_action` to better describe its behavior (existing
settings are auto-migrated)
- Fixed a few situations where the mouse cursor would be incorrectly
hidden
AWS-LC registers an `atexit` handler that intentionally acquires a lock
without releasing it. AWS-LC also has `thread_local` objects which
acquire this lock in their destructor. Destructors for `thread_local`s
run under the loader lock. So, there is a race condition where, if a
thread exits after `atexit` handlers have run, the TLS destructors will
block indefinitely on this lock while holding the loader lock. Since
`ExitProcess` also requires the loader lock, process teardown will
deadlock.
Closes#54856
Release Notes:
- Fixed an issue where the Zed process wouldn't exit after closing all
windows
## Context
Fixes a bug on Windows where the Alt modifier key becomes stuck in the
pressed state after using Alt-Tab to switch away from and back to the
Zed application.
### Root Cause
In `crates/gpui_windows/src/events.rs`, when the window loses focus
during Alt-Tab, the WM_KEYUP event for the Alt key is never delivered to
the application. The pending modifier state in GPUI remains as
`alt=true`. When the window regains focus, there is no synchronization
point to reset this stale state.
## How to Review
In `handle_activate_msg()` (WM_ACTIVATE handler), when the window is
activated:
1. Reset the cached modifier tracking state (`last_reported_modifiers`
and `last_reported_capslock` set to `None`)
2. Query the actual current modifier state from Windows using
`GetKeyState()` APIs
3. Dispatch a `ModifiersChanged` event with the true state to
synchronize GPUI
## Self-Review Checklist
<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
I think it's difficult to add tests for this because it's
platform-dependent, but I recorded a video for comparison — sorry I only
have an anime‑style app to show the key presses.
<details>
<summary>Video</summary>
https://github.com/user-attachments/assets/b545178c-b360-4e2b-8d60-d6f95a0b4b79https://github.com/user-attachments/assets/bc4d41eb-6f42-4040-a588-0fc46c576db7
</details>
Closes#45485
Release Notes:
- Fixed Alt modifier key stuck after Alt-Tab on Windows. Modifier state
is now synchronized when the window regains focus, ensuring correct key
interpretation after window switching.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
Relates to #5303 and
https://github.com/zed-industries/zed/issues/40826#issuecomment-3684556858
although I haven't found anywhere an actual request for `gpui` itself to
support a system alert sound.
### What
Basically, this PR adds a function that triggers an OS-dependent alert
sound, commonly used by terminal applications for `\a` / `BEL`, and GUI
applications to indicate an action failed in some small way (e.g. no
search results found, unable to move cursor, button disabled).
Also updated the `input` example, which now plays the bell if the user
presses <kbd>backspace</kbd> with nothing behind the cursor to delete,
or <kbd>delete</kbd> with nothing in front of the cursor.
Test with `cargo run --example input --features gpui_platform/font-kit`.
### Why
If this is merged, I plan to take a second step:
- Add a new Zed setting (probably something like
`terminal.audible_bell`)
- If enabled, `printf '\a'`, `tput bel` etc. would call this new API to
play an audible sound
This isn't the super-shiny dream of #5303 but it would allow users to
more easily configure tasks to notify when done. Plus, any TUI/CLI apps
that expect this functionality will work. Also, I think many terminal
users expect something like this (WezTerm, iTerm, etc. almost all
support this).
### Notes
~I was only able to test on macOS and Windows, so if there are any Linux
users who could verify this works for X11 / Wayland that would be a huge
help! If not I can try~
Confirmed Wayland + X11 both working when I ran the example on a NixOS
desktop
Release Notes:
- N/A
Linear gradients in dark color ranges (5-15% lightness) show visible
banding due to 8-bit quantization — only ~7 distinct values exist in
that range, producing hard steps instead of smooth transitions. This
affects every dark theme in Zed.
## What this does
Adds triangular-distributed dithering after gradient interpolation in
both the Metal and HLSL fragment shaders. The noise breaks up
quantization steps at the sub-pixel level, producing perceptually smooth
gradients.
## How it works
Two hash-based pseudo-random values (seeded from fragment position x
golden ratio) are summed to produce a triangular probability
distribution. This is added to the RGB channels at +/-1/255 amplitude.
- **Triangular PDF** — mean-zero, so no brightness shift across the
gradient
- **+/-1/255 amplitude** — below perceptual threshold, invisible on
bright gradients where 8-bit precision is already sufficient
- **Deterministic per-pixel** — seeded from position, no temporal
flickering
- **Zero-cost** — a couple of `fract`/`sin` per fragment, negligible vs.
the existing gradient math
### Channel-specific amplitudes
| Channel | Amplitude | Rationale |
|---------|-----------|----------------------------------------------------------------------------------------------------|
| RGB | ±2/255 | Breaks dark-on-dark banding where adjacent 8-bit values
are perceptually close |
| Alpha | ±3/255 | Alpha gradients over dark backgrounds need stronger
noise — α × dark color = tiny composited steps |
The higher alpha amplitude is necessary because when a semi-transparent
gradient (e.g., 0.4 → 0.0 alpha) composites over a dark background, the
effective visible difference per quantization step is smaller than the
RGB case. ±3/255 is still well below the perceptual threshold on
bright/opaque elements.
## Scope
Two files changed, purely additive:
| File | Change |
|------|--------|
| `crates/gpui_macos/src/shaders.metal` | 13 lines after `mix()` in
`fill_color()` |
| `crates/gpui_windows/src/shaders.hlsl` | 13 lines after `lerp()` in
`gradient_color()` |
No changes to Rust code, no API changes, no new dependencies.
## Screenshots
<img width="1886" height="1003" alt="gradient_dithering_before"
src="https://github.com/user-attachments/assets/f75ae93b-b142-4d0e-9b61-e08f30fe1758"
/>
_Before_
<img width="1902" height="1052" alt="gradient_dithering_after"
src="https://github.com/user-attachments/assets/7aee9a36-f578-4e08-a846-44d092bcf043"
/>
_After_
## Test plan
This is a shader-level fix; no Rust test harness exists for visual
output. Manual testing is appropriate here. Visual regression tests
cover UI layout, not sub-pixel rendering quality.
**Manual (macOS):**
- [x] Dark gradients (5-13% lightness range) — banding eliminated
- [x] Bright gradients — no visible difference (dither amplitude below
precision threshold)
- [x] Oklab and sRGB color spaces — both paths dithered
- [x] Solid colours, pattern fills, checkerboard — unaffected (dither
only applies to LinearGradient case)
- [x] Alpha gradients (semi-transparent over dark bg) — banding
eliminated with alpha dithering
- [x] Path gradients (paint_path) — same fill_colour() function,
dithering applies
**Windows:** HLSL change is identical logic with HLSL built-ins
(`frac`/`lerp` vs `fract`/`mix`) — not tested locally.
Release Notes:
- Improved linear gradient rendering by adding dithering to eliminate
visible banding in dark color ranges
Liam
Closes#51312
- Remove platform-specific #[cfg] gates from PinchEvent, event
listeners, and dispatch logic in GPUI
- Windows: Intercept Ctrl+ScrollWheel (emitted by precision trackpads
for pinch gestures) and convert them to GPUI PinchEvents
- Image Viewer: remove redundant platform-specific blocks
- X11: Bump XInput version to 2.4 and implement handlers for
XinputGesturePinch events
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Release Notes:
- Pinching gestures now available on all devices.
---------
Co-authored-by: John Tur <john-tur@outlook.com>
Previously, the GPUI profiler allocates one CircularBuffer per thread,
and `CircularBuffer<N>` always preallocates space for N entries. As a
result it allocates ~20MB/thread, and on my machine about 33 threads are
created at startup for a total of 600MB used.
In this PR I change it to use a VecDeque that can gradually grow up to
20MB as data is written. At least in my experiments it seems that this
caps overall usage at about 21MB perhaps because only one thread writes
very much usage data.
Since this is fixed overhead for everyone running Zed it seems like a
worthwhile gain.
This also folds duplicated code across platforms into the common gpui
profiler.
Before:
<img width="4804" height="2192" alt="Image"
src="https://github.com/user-attachments/assets/7060ee5b-ef80-49cb-b7be-de33e9a2e7a5"
/>
After:
<img width="5052" height="1858" alt="image"
src="https://github.com/user-attachments/assets/513494df-0974-4604-9796-15a12ef1c134"
/>
I got here from #35780 but I don't think this is tree-size related, it
seems to be fixed overhead.
Release Notes:
- Improved: Significantly less memory used to record internal profiling
information.
---------
Co-authored-by: MrSubidubi <finn@zed.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
After GPU device recovery clears the atlas, the next frame could
re-present a stale scene via the needs_present path (which skips scene
rebuilding). The stale scene references texture IDs that no longer exist
in the cleared atlas, causing an index-out-of-bounds panic.
Fix this by setting a force_render_after_recovery flag when device
recovery completes. The platform refresh loop reads this flag and passes
force_render: true in RequestFrameOptions, ensuring GPUI rebuilds the
scene before presenting.
Fixes ZED-5QT
Release Notes:
- N/A or Added/Fixed/Improved ...
MetalAtlas::remove() used tiles_by_key.get(key) to look up the texture
ID but only called tiles_by_key.remove(key) when the texture became
fully unreferenced. When a tile was removed while other tiles remained
on the same texture, the key stayed in tiles_by_key as a stale entry.
Once subsequent removals deleted the texture, get_or_insert_with could
return the stale tile referencing a now-deleted texture slot, causing an
unwrap panic in MetalAtlasState::texture().
Fix: change .get(key) to .remove(key) unconditionally, matching the WGPU
and DirectX atlas implementations which already do this correctly.
Closes ZED-5KV
Release Notes:
- N/A or Added/Fixed/Improved ...