# Objective
The contextmenu listener cancelled the browser's native menu
unconditionally, so pages hosting a gpui app lost right-click entirely
wherever the app showed no menu of its own, and apps that do show one
had no native menu to suppress selectively.
## Solution
Record whether the right-button MouseDown dispatch was consumed (a
handler called stop_propagation, e.g. to open a custom menu) and cancel
the native menu only in that case: app menus win where they exist, the
browser menu everywhere else. Apps opening custom context menus should
mark the triggering mousedown consumed to opt in.
## Testing
Tested locally
## 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
## Showcase
<img width="816" height="288" alt="context menu"
src="https://github.com/user-attachments/assets/c79cd2ef-d465-4431-8461-7a9feb922576"
/>
Release Notes:
- N/A
# Objective
The web platform called `event.preventDefault()` unconditionally before
dispatching every keydown and keyup, destroying all browser defaults
(reload, zoom, tab search, find, address bar) whether or not the app
used the key. The dispatch result already carries the answer:
`!propagate` means a keybinding matched or a handler consumed the
keystroke in the current focus context.
## Solution
Prevent the default only when the app consumed the keystroke, when the
printable character was inserted into the input handler, or mid-IME
composition. Unbound chords now fall through to the browser, and the
behavior is focus-aware: a chord bound in an editor is consumed there
but keeps its browser default when nothing that binds it is focused.
## Testing
Tested locally.
## 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
Release Notes:
- N/A
# Objective
write_to_clipboard was a no-op and read_from_clipboard returned None.
## Solution
Writes go through the browser's asynchronous Clipboard API,
fire-and-forget, invoked synchronously inside the user's input event so
the user-activation requirement holds.
Reads cannot be implemented the same way: the async clipboard read API
cannot fit read_from_clipboard's synchronous signature. Paste is instead
delivered by a DOM paste listener on the hidden input, whose
ClipboardEvent exposes clipboardData synchronously; the text feeds the
active input handler. This covers keyboard-, menu-bar- and
context-menu-initiated paste. Note the keyboard path requires the paste
chord's keydown to reach the browser unconsumed (apps should not bind
cmd/ctrl-v on web).
## Testing
Tested locally
## 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
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
# 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
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
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:
- N/A or Added/Fixed/Improved ...
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
## Summary
- Add `unconfigure_surface()` and `replace_surface()` methods to
`WgpuRenderer` for mobile platform window lifecycle management
- Prefer `PresentMode::Mailbox` (triple-buffering) over `Fifo` to avoid
blocking during lifecycle transitions
- Early return in `draw()` when surface is unconfigured to prevent
driver hangs
## Motivation
On Android, the native window (`ANativeWindow`) is destroyed when the
app goes to the background and recreated when it returns to the
foreground. The same happens during orientation changes. Without surface
lifecycle methods, the only option is to destroy the entire
`WgpuRenderer` and create a new one on resume.
The problem: GPUI's scene cache holds `AtlasTextureId` references from
the old renderer's atlas. A new renderer has an empty atlas, so those
cached IDs cause index-out-of-bounds panics.
The fix: Keep the renderer (device, queue, atlas, pipelines) alive
across surface destruction. Only the wgpu `Surface` needs to be
replaced.
### `unconfigure_surface()`
Marks the surface as unconfigured so `draw()` skips rendering via the
existing `surface_configured` guard. Drops intermediate textures that
reference the old surface dimensions. The renderer stays fully alive.
### `replace_surface()`
Creates a new `wgpu::Surface` from fresh window handles using the
**same** `wgpu::Instance` that created the original adapter/device.
Reconfigures the surface and marks it as configured so rendering
resumes. All cached atlas textures remain valid.
### PresentMode::Mailbox
`Fifo` (VSync) blocks in `get_current_texture()` and can deadlock if the
compositor is frozen during a lifecycle transition (e.g.
`TerminateWindow` → `InitWindow` on Android). Mailbox (triple-buffering)
avoids this. Falls back to `AutoNoVsync` → `Fifo` if unsupported.
### draw() early return
Some drivers (notably Adreno) block indefinitely when acquiring a
texture from an unconfigured surface. The early return prevents this.
## Context
This is needed by
[gpui-mobile](https://github.com/itsbalamurali/gpui-mobile), a project
bringing GPUI to Android and iOS. The Android implementation needs these
methods to handle:
1. **Background/foreground transitions** — `TerminateWindow` destroys
the native window, `InitWindow` recreates it
2. **Orientation changes** — Surface is destroyed and recreated with new
dimensions
3. **Split-screen transitions** — Similar surface recreation
Without this change, we maintain a local fork of `gpui_wgpu` with just
these additions. Upstreaming them would let mobile platform
implementations use the official crate directly.
## Test plan
- [x] Tested on Android (Motorola, Adreno 720 GPU) — 3 consecutive
background/foreground cycles, zero panics, atlas textures preserved
- [x] Tested orientation changes (portrait→landscape→portrait) — surface
replacement completed in <40ms per rotation
- [x] Verified `draw()` correctly skips rendering when surface is
unconfigured
- [x] Verified no regression on desktop — methods are additive, existing
code paths unchanged
- [x] PresentMode fallback chain works on devices that don't support
Mailbox
## Release Notes
- N/A
Currently the web backend for gpui doesn't send any events to the
`InputHandler`'s like `EntityInputHandler` which are needed for the
input example and the editor crate, among others. This PR makes it pass
those events, in addition to also dealing with composition events so
that IME works. It adds an invisible input element to listen for
composition events, since canvases don't receive them.
Release Notes:
- N/A
Co-Authored-By: Eric Holk <eric@zed.dev>
In app drop we had been calling `.close()` on the executors. This caused
problems with the BackgroundExecutor on Linux because it raced with
concurrent work: If task A was running and about to poll task B, the
poll to task B would panic with "Task polled after completion". This
didn't really matter (because the app was shutting down anyway) but
inflated our panic metrics on Linux.
It turns out that the call to `.close()` is not needed. It was added to
prevent foreground tasks being scheduled after the app was dropped; but
on all platforms the App run method does not return until after the
ForegroundExecutor is stopped (so no further tasks will run anyway).
The background case is more interesting. In test code it didn't matter
(the background executor is simulated on the main thread so tests can't
leak tasks); in app code it also didn't really make a difference. When
`fn main` returns (which it does immediately after the app is dropped)
all the background threads will be cancelled anyway.
Further confounding debugging, it turns out that the App does not get
dropped on macOS and Windows due to a reference cycle; so this was only
happening on Linux where the app quit callback is dropped instead of
retained after being called. (Fix in #50985)
Release Notes:
- N/A
---------
Co-authored-by: Eric Holk <eric@zed.dev>
Fixes a crash which occurred on firefox-linux when resizing (and was
causing blanks during resize in other browsers)
The issue was due to attempting to resize the drawing buffer while it
was being rendered to, which causes a browser crash since firefox
destroys that buffer while it is still being accessed. This essentially
defers the resize to just before drawing, so that it won't be resized
*while* being drawn to.
Release Notes:
- N/A
Implements a basic web platform for the wasm32-unknown-unknown target
for gpui
Release Notes:
- N/A *or* Added/Fixed/Improved ...
---------
Co-authored-by: John Tur <john-tur@outlook.com>