diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index fa935ac..0f6ac47 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -40,7 +40,7 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; use ely_domain::{ArchivedTab, BrowserTab, TabState}; use gpui::{ - AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, + AnyElement, Context, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, div, px, rgb, }; use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; @@ -54,6 +54,7 @@ impl ElyShell { &mut self, tab: &BrowserTab, snapshot: &BrowserSnapshot, + bottom_corner_radius: Pixels, cx: &mut Context, ) -> AnyElement { if tab.state() == &TabState::Crashed { @@ -145,7 +146,7 @@ impl ElyShell { render_settings_shell(snapshot, "ely://settings/sync", content, cx) } 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), } diff --git a/crates/ely_app/src/shell/splits.rs b/crates/ely_app/src/shell/splits.rs index 3bf17c4..8ef6be3 100644 --- a/crates/ely_app/src/shell/splits.rs +++ b/crates/ely_app/src/shell/splits.rs @@ -16,6 +16,13 @@ use super::chrome::{ use super::{ElyShell, ShellState}; 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 { pub(super) fn render_content_area( &mut self, @@ -24,11 +31,11 @@ impl ElyShell { cx: &mut Context, ) -> AnyElement { 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 { - 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) @@ -126,7 +133,7 @@ impl ElyShell { .collect::>(); 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() @@ -217,7 +224,11 @@ impl ElyShell { .child(div().flex_1().min_h_0().overflow_hidden().child(if compact_canvas { render_compact_split_canvas(tab) } 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() } diff --git a/crates/ely_app/src/shell/web_surface_controller.rs b/crates/ely_app/src/shell/web_surface_controller.rs index 9a53371..4eb20df 100644 --- a/crates/ely_app/src/shell/web_surface_controller.rs +++ b/crates/ely_app/src/shell/web_surface_controller.rs @@ -22,6 +22,7 @@ impl ElyShell { &mut self, tab: &BrowserTab, snapshot: &BrowserSnapshot, + bottom_corner_radius: Pixels, cx: &mut Context, ) -> AnyElement { let state_entity = cx.entity().clone(); @@ -31,16 +32,16 @@ impl ElyShell { match self.web_surfaces.state(tab.id()) { 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, .. }) => { render_failed_web_surface(tab, message.as_str(), state_entity) } 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 => { - render_loading_web_surface(tab, state_entity) + render_loading_web_surface(tab, state_entity, bottom_corner_radius) } } } diff --git a/crates/ely_app/src/shell/web_surface_view.rs b/crates/ely_app/src/shell/web_surface_view.rs index 0c91e18..bdc93ef 100644 --- a/crates/ely_app/src/shell/web_surface_view.rs +++ b/crates/ely_app/src/shell/web_surface_view.rs @@ -1,7 +1,7 @@ use ely_domain::{BrowserTab, TabId}; use gpui::{ 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::{ @@ -14,15 +14,25 @@ pub(super) fn render_ready_web_surface( _frame: &WebSurfaceFrame, tab: &BrowserTab, state_entity: Entity, + bottom_corner_radius: Pixels, ) -> 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( tab: &BrowserTab, state_entity: Entity, + bottom_corner_radius: Pixels, ) -> 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( @@ -75,9 +85,18 @@ fn render_web_surface( .into_any_element() } -fn render_native_web_surface(tab: &BrowserTab, state_entity: Entity) -> impl IntoElement { +fn render_native_web_surface( + tab: &BrowserTab, + state_entity: Entity, + bottom_corner_radius: Pixels, +) -> impl IntoElement { let tab_id = tab.id().clone(); 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| { let scale_factor = window.scale_factor(); state_entity.update(cx, |shell, cx| { @@ -85,6 +104,8 @@ fn render_native_web_surface(tab: &BrowserTab, state_entity: Entity) - }); }) .size_full() + .rounded_bl(bottom_corner_radius) + .rounded_br(bottom_corner_radius) } fn render_input_overlay( diff --git a/third_party/gpui/src/elements/native_surface.rs b/third_party/gpui/src/elements/native_surface.rs index 28f9dd3..1c57ace 100644 --- a/third_party/gpui/src/elements/native_surface.rs +++ b/third_party/gpui/src/elements/native_surface.rs @@ -1,6 +1,6 @@ use crate::{ - App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, - NativeSurfaceHandle, Pixels, Style, StyleRefinement, Styled, Window, + App, Bounds, Corners, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, + LayoutId, NativeSurfaceHandle, Pixels, Style, StyleRefinement, Styled, Window, }; use refineable::Refineable; @@ -71,6 +71,20 @@ impl Element for NativeSurface { let Some(on_surface) = self.on_surface.as_mut() else { 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::( global_id, |state, window| { @@ -78,7 +92,7 @@ impl Element for NativeSurface { .and_then(|state| state.surface) .or_else(|| window.create_native_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) }); } (None, NativeSurfaceState { surface: None }) diff --git a/third_party/gpui/src/platform.rs b/third_party/gpui/src/platform.rs index 1f89980..c151123 100644 --- a/third_party/gpui/src/platform.rs +++ b/third_party/gpui/src/platform.rs @@ -747,7 +747,13 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn create_native_surface(&self) -> Option { None } - fn sync_native_surface(&self, _surface: &NativeSurfaceHandle, _bounds: Bounds) {} + fn sync_native_surface( + &self, + _surface: &NativeSurfaceHandle, + _bounds: Bounds, + _corner_radii: crate::Corners, + ) { + } // macOS specific methods fn get_title(&self) -> String { diff --git a/third_party/gpui/src/platform/mac/window.rs b/third_party/gpui/src/platform/mac/window.rs index c2f31af..aa3f465 100644 --- a/third_party/gpui/src/platform/mac/window.rs +++ b/third_party/gpui/src/platform/mac/window.rs @@ -1539,7 +1539,12 @@ impl PlatformWindow for MacWindow { } } - fn sync_native_surface(&self, surface: &NativeSurfaceHandle, bounds: Bounds) { + fn sync_native_surface( + &self, + surface: &NativeSurfaceHandle, + bounds: Bounds, + corner_radii: crate::Corners, + ) { let this = self.0.lock(); unsafe { 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 _: () = msg_send![view, setFrame: frame]; 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]; + } } } diff --git a/third_party/gpui/src/window.rs b/third_party/gpui/src/window.rs index e55f2c3..f5a6eff 100644 --- a/third_party/gpui/src/window.rs +++ b/third_party/gpui/src/window.rs @@ -3208,8 +3208,20 @@ impl Window { } /// Synchronizes a native child surface to a GPUI layout box. - pub fn sync_native_surface(&self, surface: &NativeSurfaceHandle, bounds: Bounds) { - 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, + corner_radii: crate::Corners, + ) { + self.platform_window.sync_native_surface(surface, bounds, corner_radii); } /// Removes an image from the sprite atlas.