fix(shell): clip native_surface overlay to its panel's corner radius
The web canvas is a GPUI `native_surface` element, which on macOS attaches an `NSView` (with a Metal-backed `CALayer`) as an AppKit overlay on top of the GPUI render layer. GPUI's `overflow: hidden` clips its own children but cannot reach into AppKit, so the canvas painted square corners on top of the main panel's rounded edge — the white surface visibly overshot the panel's rounded bottom. Thread a per-corner radius through the GPUI patch: - `Window::sync_native_surface` and the `PlatformWindow` trait carry a `Corners<Pixels>`. - The macOS implementation sets `CALayer.cornerRadius` to the maximum requested radius and `maskedCorners` to the bitmask of corners that actually requested rounding (GPUI is Y-down while NSView is Y-up, so a GPUI "top" maps to a CALayer "MaxY" corner — comment locks the mapping). `setMasksToBounds: YES` lets Core Animation actually honor the corner radius for layered content. - The `NativeSurface` element resolves all four corner radii from its own `StyleRefinement` (so callers attach `.rounded_bl(...)` / `.rounded_br(...)` exactly as on any other GPUI element). Application side wires the right radius per context: - The main canvas asks for `RADIUS_CARD` (18 px) on its bottom corners so it follows the surrounding panel's rounded bottom edge while the top stays flush against the toolbar. - A split pane's canvas asks for the pane's own `SPLIT_PANE_RADIUS` (10 px) so the canvas hugs the rounded frame the pane already paints. `render_failed_web_surface` doesn't need rounding wiring — the error page is a GPUI div and is clipped normally by the surrounding `overflow: hidden`.
This commit is contained in:
@@ -40,7 +40,7 @@ use ely_browser_core::BrowserSnapshot;
|
|||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::{ArchivedTab, BrowserTab, TabState};
|
use ely_domain::{ArchivedTab, BrowserTab, TabState};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString,
|
||||||
StatefulInteractiveElement, Styled, div, px, rgb,
|
StatefulInteractiveElement, Styled, div, px, rgb,
|
||||||
};
|
};
|
||||||
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
||||||
@@ -54,6 +54,7 @@ impl ElyShell {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
|
bottom_corner_radius: Pixels,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
if tab.state() == &TabState::Crashed {
|
if tab.state() == &TabState::Crashed {
|
||||||
@@ -145,7 +146,7 @@ impl ElyShell {
|
|||||||
render_settings_shell(snapshot, "ely://settings/sync", content, cx)
|
render_settings_shell(snapshot, "ely://settings/sync", content, cx)
|
||||||
}
|
}
|
||||||
url if super::web_surface::is_external_web_url(url) => {
|
url if super::web_surface::is_external_web_url(url) => {
|
||||||
self.render_external_web_canvas(tab, snapshot, cx)
|
self.render_external_web_canvas(tab, snapshot, bottom_corner_radius, cx)
|
||||||
}
|
}
|
||||||
_ => render_default_page(tab),
|
_ => render_default_page(tab),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ use super::chrome::{
|
|||||||
use super::{ElyShell, ShellState};
|
use super::{ElyShell, ShellState};
|
||||||
use crate::SplitRight;
|
use crate::SplitRight;
|
||||||
|
|
||||||
|
/// Bottom-corner radius applied to a split pane's canvas overlay so it
|
||||||
|
/// follows the pane's rounded frame. Kept in lock-step with the
|
||||||
|
/// `.rounded(px(10.0))` used in `render_pane`; bumping one without the
|
||||||
|
/// other re-introduces the panel-bg sliver between the canvas and the
|
||||||
|
/// pane's rounded edge.
|
||||||
|
const SPLIT_PANE_RADIUS: f32 = 10.0;
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub(super) fn render_content_area(
|
pub(super) fn render_content_area(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -24,11 +31,11 @@ impl ElyShell {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let Some(layout) = active_split_layout(snapshot, active_tab) else {
|
let Some(layout) = active_split_layout(snapshot, active_tab) else {
|
||||||
return self.render_web_canvas(active_tab, snapshot, cx);
|
return self.render_web_canvas(active_tab, snapshot, px(spacing::RADIUS_CARD), cx);
|
||||||
};
|
};
|
||||||
|
|
||||||
if layout.pane_count() < 2 {
|
if layout.pane_count() < 2 {
|
||||||
return self.render_web_canvas(active_tab, snapshot, cx);
|
return self.render_web_canvas(active_tab, snapshot, px(spacing::RADIUS_CARD), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render_split_canvas(snapshot, active_tab, layout, cx)
|
self.render_split_canvas(snapshot, active_tab, layout, cx)
|
||||||
@@ -126,7 +133,7 @@ impl ElyShell {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if panes.len() < 2 {
|
if panes.len() < 2 {
|
||||||
return self.render_web_canvas(active_tab, snapshot, cx);
|
return self.render_web_canvas(active_tab, snapshot, px(spacing::RADIUS_CARD), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = div()
|
let body = div()
|
||||||
@@ -217,7 +224,11 @@ impl ElyShell {
|
|||||||
.child(div().flex_1().min_h_0().overflow_hidden().child(if compact_canvas {
|
.child(div().flex_1().min_h_0().overflow_hidden().child(if compact_canvas {
|
||||||
render_compact_split_canvas(tab)
|
render_compact_split_canvas(tab)
|
||||||
} else {
|
} else {
|
||||||
self.render_web_canvas(tab, snapshot, cx)
|
// Split pane wraps the canvas in a `rounded(SPLIT_PANE_RADIUS)`
|
||||||
|
// container with its own header — the canvas's bottom
|
||||||
|
// corners follow the pane's rounded edge, top stays
|
||||||
|
// flush against the pane header.
|
||||||
|
self.render_web_canvas(tab, snapshot, px(SPLIT_PANE_RADIUS), cx)
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ impl ElyShell {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
|
bottom_corner_radius: Pixels,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let state_entity = cx.entity().clone();
|
let state_entity = cx.entity().clone();
|
||||||
@@ -31,16 +32,16 @@ impl ElyShell {
|
|||||||
|
|
||||||
match self.web_surfaces.state(tab.id()) {
|
match self.web_surfaces.state(tab.id()) {
|
||||||
Some(WebSurfaceState::Ready(frame)) => {
|
Some(WebSurfaceState::Ready(frame)) => {
|
||||||
render_ready_web_surface(frame, tab, state_entity)
|
render_ready_web_surface(frame, tab, state_entity, bottom_corner_radius)
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Failed { message, .. }) => {
|
Some(WebSurfaceState::Failed { message, .. }) => {
|
||||||
render_failed_web_surface(tab, message.as_str(), state_entity)
|
render_failed_web_surface(tab, message.as_str(), state_entity)
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Loading { previous_frame: Some(frame), .. }) => {
|
Some(WebSurfaceState::Loading { previous_frame: Some(frame), .. }) => {
|
||||||
render_ready_web_surface(frame, tab, state_entity)
|
render_ready_web_surface(frame, tab, state_entity, bottom_corner_radius)
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Loading { previous_frame: None, .. }) | None => {
|
Some(WebSurfaceState::Loading { previous_frame: None, .. }) | None => {
|
||||||
render_loading_web_surface(tab, state_entity)
|
render_loading_web_surface(tab, state_entity, bottom_corner_radius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use ely_domain::{BrowserTab, TabId};
|
use ely_domain::{BrowserTab, TabId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, Entity, InteractiveElement, IntoElement, MouseButton,
|
AnyElement, App, ElementId, Entity, InteractiveElement, IntoElement, MouseButton,
|
||||||
ParentElement, Styled, Window, canvas, div, native_surface, px, rgb,
|
ParentElement, Pixels, Styled, Window, canvas, div, native_surface, px, rgb,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -14,15 +14,25 @@ pub(super) fn render_ready_web_surface(
|
|||||||
_frame: &WebSurfaceFrame,
|
_frame: &WebSurfaceFrame,
|
||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
state_entity: Entity<ElyShell>,
|
state_entity: Entity<ElyShell>,
|
||||||
|
bottom_corner_radius: Pixels,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
render_web_surface(tab, state_entity.clone(), render_native_web_surface(tab, state_entity))
|
render_web_surface(
|
||||||
|
tab,
|
||||||
|
state_entity.clone(),
|
||||||
|
render_native_web_surface(tab, state_entity, bottom_corner_radius),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn render_loading_web_surface(
|
pub(super) fn render_loading_web_surface(
|
||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
state_entity: Entity<ElyShell>,
|
state_entity: Entity<ElyShell>,
|
||||||
|
bottom_corner_radius: Pixels,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
render_web_surface(tab, state_entity.clone(), render_native_web_surface(tab, state_entity))
|
render_web_surface(
|
||||||
|
tab,
|
||||||
|
state_entity.clone(),
|
||||||
|
render_native_web_surface(tab, state_entity, bottom_corner_radius),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn render_failed_web_surface(
|
pub(super) fn render_failed_web_surface(
|
||||||
@@ -75,9 +85,18 @@ fn render_web_surface(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_native_web_surface(tab: &BrowserTab, state_entity: Entity<ElyShell>) -> impl IntoElement {
|
fn render_native_web_surface(
|
||||||
|
tab: &BrowserTab,
|
||||||
|
state_entity: Entity<ElyShell>,
|
||||||
|
bottom_corner_radius: Pixels,
|
||||||
|
) -> impl IntoElement {
|
||||||
let tab_id = tab.id().clone();
|
let tab_id = tab.id().clone();
|
||||||
let element_id = ElementId::Name(format!("web-surface-{}", tab_id.as_str()).into());
|
let element_id = ElementId::Name(format!("web-surface-{}", tab_id.as_str()).into());
|
||||||
|
// `bottom_corner_radius` is wired through the GPUI `native_surface`
|
||||||
|
// patch to the AppKit overlay's `CALayer.cornerRadius`, so the
|
||||||
|
// canvas follows the same rounded edge as its containing panel
|
||||||
|
// instead of painting past it. The top corners stay flat because
|
||||||
|
// the topbar / pane header sits flush above the canvas.
|
||||||
native_surface(element_id, move |surface, bounds, window: &mut Window, cx: &mut App| {
|
native_surface(element_id, move |surface, bounds, window: &mut Window, cx: &mut App| {
|
||||||
let scale_factor = window.scale_factor();
|
let scale_factor = window.scale_factor();
|
||||||
state_entity.update(cx, |shell, cx| {
|
state_entity.update(cx, |shell, cx| {
|
||||||
@@ -85,6 +104,8 @@ fn render_native_web_surface(tab: &BrowserTab, state_entity: Entity<ElyShell>) -
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.rounded_bl(bottom_corner_radius)
|
||||||
|
.rounded_br(bottom_corner_radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_input_overlay(
|
fn render_input_overlay(
|
||||||
|
|||||||
+17
-3
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
App, Bounds, Corners, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
|
||||||
NativeSurfaceHandle, Pixels, Style, StyleRefinement, Styled, Window,
|
LayoutId, NativeSurfaceHandle, Pixels, Style, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
|
|
||||||
@@ -71,6 +71,20 @@ impl Element for NativeSurface {
|
|||||||
let Some(on_surface) = self.on_surface.as_mut() else {
|
let Some(on_surface) = self.on_surface.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// The platform overlay sits on its own AppKit/Compositor layer
|
||||||
|
// and is not clipped by GPUI's rounded `overflow: hidden`.
|
||||||
|
// Resolve every corner radius from the element's style so a
|
||||||
|
// caller that attaches `.rounded_bl(...)` / `.rounded_br(...)`
|
||||||
|
// gets exactly those corners rounded on the platform surface.
|
||||||
|
let rem_size = window.rem_size();
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.refine(&self.style);
|
||||||
|
let corner_radii = Corners {
|
||||||
|
top_left: style.corner_radii.top_left.to_pixels(rem_size),
|
||||||
|
top_right: style.corner_radii.top_right.to_pixels(rem_size),
|
||||||
|
bottom_left: style.corner_radii.bottom_left.to_pixels(rem_size),
|
||||||
|
bottom_right: style.corner_radii.bottom_right.to_pixels(rem_size),
|
||||||
|
};
|
||||||
let Some(surface) = window.with_element_state::<NativeSurfaceState, _>(
|
let Some(surface) = window.with_element_state::<NativeSurfaceState, _>(
|
||||||
global_id,
|
global_id,
|
||||||
|state, window| {
|
|state, window| {
|
||||||
@@ -78,7 +92,7 @@ impl Element for NativeSurface {
|
|||||||
.and_then(|state| state.surface)
|
.and_then(|state| state.surface)
|
||||||
.or_else(|| window.create_native_surface());
|
.or_else(|| window.create_native_surface());
|
||||||
if let Some(surface) = surface {
|
if let Some(surface) = surface {
|
||||||
window.sync_native_surface(&surface, bounds);
|
window.sync_native_surface(&surface, bounds, corner_radii);
|
||||||
return (Some(surface.clone()), NativeSurfaceState { surface: Some(surface) });
|
return (Some(surface.clone()), NativeSurfaceState { surface: Some(surface) });
|
||||||
}
|
}
|
||||||
(None, NativeSurfaceState { surface: None })
|
(None, NativeSurfaceState { surface: None })
|
||||||
|
|||||||
Vendored
+7
-1
@@ -747,7 +747,13 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||||||
fn create_native_surface(&self) -> Option<NativeSurfaceHandle> {
|
fn create_native_surface(&self) -> Option<NativeSurfaceHandle> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn sync_native_surface(&self, _surface: &NativeSurfaceHandle, _bounds: Bounds<Pixels>) {}
|
fn sync_native_surface(
|
||||||
|
&self,
|
||||||
|
_surface: &NativeSurfaceHandle,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_corner_radii: crate::Corners<Pixels>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
// macOS specific methods
|
// macOS specific methods
|
||||||
fn get_title(&self) -> String {
|
fn get_title(&self) -> String {
|
||||||
|
|||||||
+57
-1
@@ -1539,7 +1539,12 @@ impl PlatformWindow for MacWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_native_surface(&self, surface: &NativeSurfaceHandle, bounds: Bounds<Pixels>) {
|
fn sync_native_surface(
|
||||||
|
&self,
|
||||||
|
surface: &NativeSurfaceHandle,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
corner_radii: crate::Corners<Pixels>,
|
||||||
|
) {
|
||||||
let this = self.0.lock();
|
let this = self.0.lock();
|
||||||
unsafe {
|
unsafe {
|
||||||
let parent = this.native_view.as_ptr() as id;
|
let parent = this.native_view.as_ptr() as id;
|
||||||
@@ -1552,6 +1557,57 @@ impl PlatformWindow for MacWindow {
|
|||||||
let frame = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height));
|
let frame = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height));
|
||||||
let _: () = msg_send![view, setFrame: frame];
|
let _: () = msg_send![view, setFrame: frame];
|
||||||
let _: () = msg_send![view, setHidden: NO];
|
let _: () = msg_send![view, setHidden: NO];
|
||||||
|
|
||||||
|
// Clip the AppKit overlay to the same shape the surrounding
|
||||||
|
// GPUI layout uses for its rounded panel. Without this the
|
||||||
|
// overlay's CALayer paints square corners on top of GPUI's
|
||||||
|
// rounded mask — the canvas visibly overshoots the panel's
|
||||||
|
// rounded bottom edge.
|
||||||
|
//
|
||||||
|
// Core Animation's `cornerRadius` is a single scalar applied
|
||||||
|
// to all four corners in `maskedCorners`; we therefore use
|
||||||
|
// the *maximum* requested radius across non-zero corners and
|
||||||
|
// intersect the corner mask to the corners that asked for
|
||||||
|
// any rounding. That covers the common cases:
|
||||||
|
//
|
||||||
|
// - Uniform `rounded_*` : all four corners get the same radius.
|
||||||
|
// - Bottom-only `rounded_b_*`: top stays flush against a toolbar.
|
||||||
|
//
|
||||||
|
// GPUI is Y-down while NSView (with the default `isFlipped:
|
||||||
|
// NO`) is Y-up, so a GPUI *top* corner maps to a CALayer
|
||||||
|
// *MaxY* corner and a GPUI *bottom* corner maps to a CALayer
|
||||||
|
// *MinY* corner.
|
||||||
|
let layer: id = msg_send![view, layer];
|
||||||
|
if !layer.is_null() {
|
||||||
|
const LAYER_MIN_X_MIN_Y: NSUInteger = 1 << 0; // bottom-left in NSView
|
||||||
|
const LAYER_MAX_X_MIN_Y: NSUInteger = 1 << 1; // bottom-right
|
||||||
|
const LAYER_MIN_X_MAX_Y: NSUInteger = 1 << 2; // top-left
|
||||||
|
const LAYER_MAX_X_MAX_Y: NSUInteger = 1 << 3; // top-right
|
||||||
|
|
||||||
|
let tl = corner_radii.top_left.0.max(0.0);
|
||||||
|
let tr = corner_radii.top_right.0.max(0.0);
|
||||||
|
let bl = corner_radii.bottom_left.0.max(0.0);
|
||||||
|
let br = corner_radii.bottom_right.0.max(0.0);
|
||||||
|
let max_radius = tl.max(tr).max(bl).max(br) as f64;
|
||||||
|
|
||||||
|
let mut mask: NSUInteger = 0;
|
||||||
|
if tl > 0.0 {
|
||||||
|
mask |= LAYER_MIN_X_MAX_Y;
|
||||||
|
}
|
||||||
|
if tr > 0.0 {
|
||||||
|
mask |= LAYER_MAX_X_MAX_Y;
|
||||||
|
}
|
||||||
|
if bl > 0.0 {
|
||||||
|
mask |= LAYER_MIN_X_MIN_Y;
|
||||||
|
}
|
||||||
|
if br > 0.0 {
|
||||||
|
mask |= LAYER_MAX_X_MIN_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _: () = msg_send![layer, setCornerRadius: max_radius];
|
||||||
|
let _: () = msg_send![layer, setMaskedCorners: mask];
|
||||||
|
let _: () = msg_send![layer, setMasksToBounds: YES];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+14
-2
@@ -3208,8 +3208,20 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronizes a native child surface to a GPUI layout box.
|
/// Synchronizes a native child surface to a GPUI layout box.
|
||||||
pub fn sync_native_surface(&self, surface: &NativeSurfaceHandle, bounds: Bounds<Pixels>) {
|
///
|
||||||
self.platform_window.sync_native_surface(surface, bounds);
|
/// `corner_radii` carries each rounded-corner radius (top-left,
|
||||||
|
/// top-right, bottom-left, bottom-right) so the platform surface
|
||||||
|
/// can match the surrounding GPUI layout's clipping shape — e.g.
|
||||||
|
/// a browser canvas whose top edge butts flat against a toolbar
|
||||||
|
/// while its bottom edge follows the panel's rounded corners.
|
||||||
|
/// Pass `Corners::default()` for a flat-edged surface.
|
||||||
|
pub fn sync_native_surface(
|
||||||
|
&self,
|
||||||
|
surface: &NativeSurfaceHandle,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
corner_radii: crate::Corners<Pixels>,
|
||||||
|
) {
|
||||||
|
self.platform_window.sync_native_surface(surface, bounds, corner_radii);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes an image from the sprite atlas.
|
/// Removes an image from the sprite atlas.
|
||||||
|
|||||||
Reference in New Issue
Block a user