Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions crates/gpui/examples/rounded_blur_overlay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Rounded native blur overlay.
//!
//! Shows a pill-shaped overlay window whose background is blurred by the OS
//! compositor, with the blur clipped to a rounded rectangle. This is created
//! with `cx.open_overlay_surface` + `OverlaySurfaceOptions` using
//! `WindowBackgroundAppearance::Blurred { corner_radius }`.
//!
//! The window is 360x72 logical px and the corner radius is 36 (half the
//! height), which turns the rounded rect into a perfect pill.
//!
//! Platform degradation matrix for `Blurred { corner_radius }`:
//! - Windows: acrylic accent clipped to a rounded HWND region. GDI regions are
//! aliased, so the region edge looks jagged; the root view is rendered with a
//! matching radius to cover it (see comment below).
//! - macOS 12+: NSVisualEffectView masked to the rounded rect. macOS < 12 has no
//! mask API, so it degrades to unmasked (rectangular) window-background blur.
//! - Linux Wayland: KDE blur protocol with a region approximating the rounded
//! rect; compositors without blur support degrade to plain transparency.
//! - X11 / web: degrade to plain transparency (no blur, no artifact).
//!
//! Quit with Ctrl+C.

use gpui::{
App, Context, OverlayInputMode, OverlaySurfaceOptions, Window, WindowBackgroundAppearance,
WindowBounds, div, hsla, prelude::*, px, rgb, rgba, size,
};
use gpui_platform::application;

/// Window size: the corner radius is half the height so the rounded rect is a pill.
const PILL_WIDTH: gpui::Pixels = px(360.0);
const PILL_HEIGHT: gpui::Pixels = px(72.0);
const CORNER_RADIUS: gpui::Pixels = px(36.0);

struct RoundedBlurOverlay;

impl Render for RoundedBlurOverlay {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
// The element radius MUST exactly match the window `corner_radius`.
// On Windows the compositor blur is clipped by an aliased GDI region;
// drawing our own rounded content at the same radius hides that jagged
// region edge behind the crisp, anti-aliased element border.
.rounded(CORNER_RADIUS)
// Semi-transparent fill lets the blurred desktop show through.
.bg(rgba(0x00000040))
.border_1()
.border_color(hsla(0.0, 0.0, 1.0, 0.20))
.flex()
.items_center()
.justify_center()
.text_color(rgb(0xffffff))
.child("Rounded native blur")
}
}

fn main() {
application().run(|cx: &mut App| {
let bounds = WindowBounds::centered(size(PILL_WIDTH, PILL_HEIGHT), cx);

cx.open_overlay_surface(
OverlaySurfaceOptions {
window_bounds: Some(bounds),
window_background: WindowBackgroundAppearance::Blurred {
corner_radius: CORNER_RADIUS,
},
show: true,
focus: true,
has_shadow: Some(false),
// Interactive so the overlay can be clicked/focused (not click-through).
input_mode: OverlayInputMode::Interactive,
..Default::default()
},
|_, cx| cx.new(|_| RoundedBlurOverlay),
)
.unwrap();

cx.activate(true);
});
}
54 changes: 49 additions & 5 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2175,6 +2175,18 @@ impl Interactivity {
|window| {
window.with_tab_group(tab_group, |window| {
if let Some(hitbox) = hitbox {
let current_a11y_node_id = if window
.a11y
.is_active()
&& self.current_a11y_node
{
global_id.and_then(|global_id| {
window.a11y.node_id_for_existing(global_id)
})
} else {
None
};

#[cfg(debug_assertions)]
self.paint_debug_info(
global_id, hitbox, &style, window, cx,
Expand Down Expand Up @@ -2206,6 +2218,7 @@ impl Interactivity {
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
current_a11y_node_id,
window,
cx,
);
Expand Down Expand Up @@ -2392,6 +2405,7 @@ impl Interactivity {
&mut self,
hitbox: &Hitbox,
element_state: Option<&mut InteractiveElementState>,
a11y_node_id: Option<accesskit::NodeId>,
window: &mut Window,
cx: &mut App,
) {
Expand Down Expand Up @@ -2512,6 +2526,30 @@ impl Interactivity {
let aux_click_listeners = mem::take(&mut self.aux_click_listeners);
let can_drop_predicate = mem::take(&mut self.can_drop_predicate);

let has_explicit_a11y_click_listener = self.a11y_state.as_ref().is_some_and(|a11y_state| {
a11y_state
.action_listeners
.iter()
.any(|(action, _)| *action == accesskit::Action::Click)
});
if let Some(node_id) = a11y_node_id
&& !click_listeners.is_empty()
&& !has_explicit_a11y_click_listener
{
let click_listeners = click_listeners.clone();
let bounds = hitbox.bounds;
window.on_a11y_action(node_id, accesskit::Action::Click, move |_, window, cx| {
let click_event = ClickEvent::Keyboard(KeyboardClickEvent {
button: KeyboardButton::Enter,
bounds,
});

for listener in &click_listeners {
listener(&click_event, window, cx);
}
});
}

if !drop_listeners.is_empty() {
let hitbox = hitbox.clone();
window.on_mouse_event({
Expand Down Expand Up @@ -4194,19 +4232,24 @@ mod tests {
}

#[gpui::test]
fn a11y_translated_clickable_div_updates_bounds_and_clicks_at_translated_center(
fn a11y_translated_clickable_div_invokes_click_listener_without_mouse_event(
cx: &mut TestAppContext,
) {
let clicked_at = Rc::new(Cell::new(None));
let click_count = Rc::new(Cell::new(0));
let mouse_position = Rc::new(Cell::new(None));
let cx = cx.add_empty_window();

let update = draw_accessible(cx, point(px(0.), px(0.)), size(px(100.), px(100.)), {
let clicked_at = clicked_at.clone();
let click_count = click_count.clone();
let mouse_position = mouse_position.clone();
move |_, _| {
div()
.id("translated-button")
.role(accesskit::Role::Button)
.on_click(move |event, _, _| clicked_at.set(Some(event.position())))
.on_click(move |event, _, _| {
click_count.set(click_count.get() + 1);
mouse_position.set(event.mouse_position());
})
.translate(px(10.), px(20.))
.w(px(40.))
.h(px(30.))
Expand Down Expand Up @@ -4244,7 +4287,8 @@ mod tests {
);
});

assert_eq!(clicked_at.get(), Some(point(px(30.), px(35.))));
assert_eq!(click_count.get(), 1);
assert_eq!(mouse_position.get(), None);
}

#[gpui::test]
Expand Down
47 changes: 44 additions & 3 deletions crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,10 @@ pub struct OverlaySurfaceOptions {
/// Whether the overlay should be focused when created.
pub focus: bool,
/// The appearance of the overlay background.
///
/// Use `WindowBackgroundAppearance::Blurred { corner_radius }` to enable
/// native compositor blur clipped to a rounded rect — ideal for pill-shaped
/// overlays that should not show a rectangular blur gutter behind them.
pub window_background: WindowBackgroundAppearance,
/// Whether platform shadow should be enabled.
pub has_shadow: Option<bool>,
Expand Down Expand Up @@ -1608,10 +1612,25 @@ pub enum WindowBackgroundAppearance {
Opaque,
/// Plain alpha transparency.
Transparent,
/// Transparency, but the contents behind the window are blurred.
/// Plain alpha transparency with the desktop/window content behind the
/// window blurred by the OS compositor.
///
/// `corner_radius` (logical pixels) clips the native blur to a rounded
/// rectangle so small pill-shaped overlays do not show a rectangular blur
/// gutter; `px(0.)` keeps the full rectangular window blur (legacy behavior).
///
/// Not always supported.
Blurred,
/// Platform support:
/// - Windows: acrylic accent, clipped via a rounded HWND region (GDI regions
/// are aliased; render your content rounded at the same radius to hide the edge).
/// - macOS 12+: NSVisualEffectView with a rounded mask image; macOS < 12
/// degrades to unmasked window-background blur.
/// - Linux Wayland: KDE blur protocol with a region approximating the rounded
/// rect; compositors without blur support degrade to plain transparency.
/// - X11 / web: degrades to plain transparency (no blur, no artifact).
Blurred {
/// Corner radius of the blur clip region, in logical pixels. `px(0.)` = rectangular.
corner_radius: Pixels,
},
/// The Mica backdrop material, supported on Windows 11.
MicaBackdrop,
/// The Mica Alt backdrop material, supported on Windows 11.
Expand Down Expand Up @@ -2182,3 +2201,25 @@ impl From<String> for ClipboardString {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn overlay_surface_options_preserve_blurred_rounded_background() {
let options = OverlaySurfaceOptions {
window_background: WindowBackgroundAppearance::Blurred {
corner_radius: px(12.),
},
..Default::default()
};
let window_options = options.into_window_options();
assert_eq!(
window_options.window_background,
WindowBackgroundAppearance::Blurred {
corner_radius: px(12.)
}
);
}
}
20 changes: 0 additions & 20 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5414,26 +5414,6 @@ impl Window {
}

match request.action {
accesskit::Action::Click => {
if let Some(bounds) = self.a11y.node_bounds.get(&request.target_node).copied() {
let center = bounds.center();
let mouse_down = PlatformInput::MouseDown(crate::MouseDownEvent {
button: MouseButton::Left,
position: center,
modifiers: Modifiers::default(),
click_count: 1,
first_mouse: false,
});
let mouse_up = PlatformInput::MouseUp(MouseUpEvent {
button: MouseButton::Left,
position: center,
modifiers: Modifiers::default(),
click_count: 1,
});
self.dispatch_event(mouse_down, cx);
self.dispatch_event(mouse_up, cx);
}
}
accesskit::Action::Focus => {
if let Some(focus_id) = self.a11y.focus_ids.get(&request.target_node).copied()
&& let Some(handle) = FocusHandle::for_id(focus_id, &cx.focus_handles)
Expand Down
Loading
Loading