From df67ebeb989b5961d30a69fc552436276a370d63 Mon Sep 17 00:00:00 2001 From: Athul Iddya Date: Wed, 27 May 2026 00:13:30 -0700 Subject: [PATCH] linux: Draw shadows and rounded corners for frameless windows Introduce CaptionlessFrameViewLinux to draw shadows and rounded corners for frameless windows on Linux. This matches the behavior of frameless windows on macOS and Windows. CaptionlessFrameViewLinux extends FrameViewLinux to suppress the titlebar and round all four corners. Corner radii propagate from the frame view to the client view, which rounds the hosted web contents. Overlay positioning is offset by the new frame border insets so overlays stay within the client area. --- libcef/browser/views/overlay_view_host.cc | 11 +- libcef/browser/views/window_view.cc | 192 +++++++++++++++++++++- libcef/browser/views/window_view.h | 11 ++ tests/ceftests/test_util.cc | 35 ++++ tests/ceftests/test_util.h | 4 + tests/ceftests/views/window_unittest.cc | 30 ++++ 6 files changed, 274 insertions(+), 9 deletions(-) diff --git a/libcef/browser/views/overlay_view_host.cc b/libcef/browser/views/overlay_view_host.cc index a78ee0671..ef5bb4741 100644 --- a/libcef/browser/views/overlay_view_host.cc +++ b/libcef/browser/views/overlay_view_host.cc @@ -13,6 +13,7 @@ #include "third_party/skia/include/core/SkColor.h" #include "ui/compositor/compositor.h" #include "ui/compositor/layer.h" +#include "ui/views/view.h" namespace { @@ -288,7 +289,15 @@ void CefOverlayViewHost::SetOverlayBounds(const gfx::Rect& bounds) { if (view_->size() != bounds_.size()) { view_->SetSize(bounds_.size()); } - widget_->SetBounds(bounds_); + + // |bounds_| is in CefWindowView coordinates, but the overlay Widget is + // positioned in host Widget coordinates. These differ by the frame border + // insets when the frame draws client-side decorations. + gfx::Rect widget_bounds = bounds_; + gfx::Point offset; + views::View::ConvertPointToWidget(window_view_, &offset); + widget_bounds.Offset(offset.OffsetFromOrigin()); + widget_->SetBounds(widget_bounds); window_view_->OnOverlayBoundsChanged(); bounds_changing_ = false; diff --git a/libcef/browser/views/window_view.cc b/libcef/browser/views/window_view.cc index 47ff9bf78..7696e3f92 100644 --- a/libcef/browser/views/window_view.cc +++ b/libcef/browser/views/window_view.cc @@ -30,6 +30,16 @@ #include "ui/views/window/native_frame_view.h" #if BUILDFLAG(IS_LINUX) +#include "ui/aura/window_tree_host_platform.h" +#include "ui/compositor/layer.h" +#include "ui/gfx/geometry/rect_f.h" +#include "ui/gfx/geometry/rounded_corners_f.h" +#include "ui/platform_window/platform_window.h" +#include "ui/views/controls/native/native_view_host.h" +#include "ui/views/controls/webview/webview.h" +#include "ui/views/view_utils.h" +#include "ui/views/window/frame_view_layout_linux.h" +#include "ui/views/window/frame_view_linux.h" #if BUILDFLAG(SUPPORTS_OZONE_X11) #include "ui/gfx/x/atom_cache.h" #include "ui/linux/linux_ui_delegate.h" @@ -70,6 +80,27 @@ class ClientViewEx : public views::ClientView { : views::CloseRequestResult::kCannotClose; } +#if BUILDFLAG(IS_LINUX) + // views::ClientView: + void UpdateWindowRoundedCorners( + const gfx::RoundedCornersF& window_radii) override { + // Clip painted views to rounded corners. A layer is required because layers + // ignore the clip of a non-layer parent. + if (!layer()) { + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + } + layer()->SetRoundedCornerRadius(window_radii); + layer()->SetIsFastRoundedCorner(true); + + // Hosted native views are separate compositor surfaces that the layer clip + // above does not reach, so round the web contents directly. + if (view_) { + view_->UpdateWebContentsRoundedCorners(window_radii); + } + } +#endif + private: const base::WeakPtr view_; }; @@ -310,6 +341,128 @@ class CaptionlessFrameView : public views::FrameView { BEGIN_METADATA(CaptionlessFrameView) END_METADATA +#if BUILDFLAG(IS_LINUX) +// Layout for CaptionlessFrameViewLinux. Drops the caption content height so the +// client view fills the window inside the frame border, and rounds all four +// corners instead of only the top corners. +class CaptionlessFrameViewLayoutLinux : public views::FrameViewLayoutLinux { + public: + CaptionlessFrameViewLayoutLinux() = default; + + CaptionlessFrameViewLayoutLinux(const CaptionlessFrameViewLayoutLinux&) = + delete; + CaptionlessFrameViewLayoutLinux& operator=( + const CaptionlessFrameViewLayoutLinux&) = delete; + + // No caption buttons or title, so the top area is just the frame border. + int GetTopAreaHeight() const override { + return ShouldShowTitlebarAndBorder() ? GetFrameBorderInsets().top() : 0; + } + + // Round all four corners using the base top-corner radius. + gfx::RoundedCornersF GetCornerRadii() const override { + return gfx::RoundedCornersF( + views::FrameViewLayoutLinux::GetCornerRadii().upper_left()); + } +}; + +// Linux implementation of a frameless frame view. Extends FrameViewLinux to +// draw client-side shadows and rounded corners without a titlebar or window +// controls, while preserving draggable region handling. +class CaptionlessFrameViewLinux : public views::FrameViewLinux { + METADATA_HEADER(CaptionlessFrameViewLinux, views::FrameViewLinux) + + public: + CaptionlessFrameViewLinux(views::Widget* widget, + base::WeakPtr view) + : views::FrameViewLinux(widget, new CaptionlessFrameViewLayoutLinux()), + view_(std::move(view)) { + // Match DesktopWindowTreeHostLinux::CreateFrameView shadow configuration. + auto* host = static_cast( + widget->GetNativeWindow()->GetHost()); + SetSupportsClientFrameShadow( + host->platform_window()->CanSetDecorationInsets() && + views::Widget::IsWindowCompositingSupported()); + } + + CaptionlessFrameViewLinux(const CaptionlessFrameViewLinux&) = delete; + CaptionlessFrameViewLinux& operator=(const CaptionlessFrameViewLinux&) = + delete; + + // FrameViewLinux: + void CreateCaptionButtons() override { + // No window controls in a frameless window. + } + + bool HasWindowTitle() const override { return false; } + + int NonClientHitTest(const gfx::Point& point) override { + // Mouse clicks within the draggable region drag the window. + if (!frame_widget()->IsFullscreen()) { + SkRegion* draggable_region = view_->draggable_region(); + if (draggable_region) { + const gfx::Point client_point = + point - GetBoundsForClientView().OffsetFromOrigin(); + if (draggable_region->contains(client_point.x(), client_point.y())) { + return HTCAPTION; + } + } + } + + // Resize handles and everything else are handled by the base class. + return views::FrameViewLinux::NonClientHitTest(point); + } + + // FrameView: + void UpdateWindowRoundedCorners() override { + frame_widget()->client_view()->UpdateWindowRoundedCorners(GetCornerRadii()); + } + + // View: + void Layout(views::View::PassKey) override { + LayoutSuperclass(this); + + // Push the current radii down so the client view and web contents stay + // clipped after relayout, including state changes that alter the radii. + UpdateWindowRoundedCorners(); + } + + private: + const base::WeakPtr view_; +}; + +BEGIN_METADATA(CaptionlessFrameViewLinux) +END_METADATA + +// Rounds every WebView surface under |view|, per corner, only where it is flush +// with a corner of |window_bounds|. |window| is the coordinate target for the +// flush test. +void UpdateWebViewRoundedCorners(views::View* view, + const views::View* window, + const gfx::RectF& window_bounds, + const gfx::RoundedCornersF& radii) { + if (auto* web_view = views::AsViewClass(view)) { + gfx::RectF bounds(web_view->GetLocalBounds()); + views::View::ConvertRectToTarget(web_view, window, &bounds); + + const bool left = bounds.x() <= window_bounds.x(); + const bool top = bounds.y() <= window_bounds.y(); + const bool right = bounds.right() >= window_bounds.right(); + const bool bottom = bounds.bottom() >= window_bounds.bottom(); + + web_view->holder()->SetCornerRadii( + gfx::RoundedCornersF(left && top ? radii.upper_left() : 0, + right && top ? radii.upper_right() : 0, + right && bottom ? radii.lower_right() : 0, + left && bottom ? radii.lower_left() : 0)); + } + + for (views::View* child : view->children()) { + UpdateWebViewRoundedCorners(child, window, window_bounds, radii); + } +} +#endif // BUILDFLAG(IS_LINUX) + bool IsWindowBorderHit(int code) { // On Windows HTLEFT = 10 and HTBORDER = 18. Values are not ordered the same // in base/hit_test.h for non-Windows platforms. @@ -654,14 +807,21 @@ void CefWindowView::CreateWidget(gfx::AcceleratedWidget parent_widget) { params.delegate->SetCanResize(can_resize); -#if BUILDFLAG(IS_WIN) +#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) if (is_frameless_) { - // Don't show the native window caption. Setting this value on Linux will - // result in window resize artifacts. + // Don't show the native window frame. params.remove_standard_frame = true; } #endif +#if BUILDFLAG(IS_LINUX) + if (is_frameless_ && !has_native_parent) { + // Use a translucent window so CaptionlessFrameViewLinux can draw + // client-side shadows and rounded corners. + params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; + } +#endif + widget->Init(std::move(params)); widget->AddObserver(this); @@ -682,10 +842,6 @@ void CefWindowView::CreateWidget(gfx::AcceleratedWidget parent_widget) { auto x11window = static_cast(view_util::GetWindowHandle(widget)); CHECK(x11window != x11::Window::None); - if (is_frameless_) { - ui::SetUseOSWindowFrame(x11window, false); - } - if (host_widget) { auto parent = static_cast( view_util::GetWindowHandle(host_widget)); @@ -802,8 +958,16 @@ std::unique_ptr CefWindowView::CreateFrameView( views::Widget* widget) { if (is_frameless_) { // Custom frame type that doesn't render a caption. +#if BUILDFLAG(IS_LINUX) + // Draw client-side shadows and rounded corners to match Windows and macOS. + auto frame_view = std::make_unique( + widget, weak_ptr_factory_.GetWeakPtr()); + frame_view->InitViews(); + return frame_view; +#else return std::make_unique( widget, weak_ptr_factory_.GetWeakPtr()); +#endif } else if (widget->ShouldUseNativeFrame()) { // DesktopNativeWidgetAura::CreateFrameView() returns // NativeFrameView by default. Extend that type. @@ -819,6 +983,7 @@ std::unique_ptr CefWindowView::CreateFrameView( bool CefWindowView::ShouldDescendIntoChildForEventHandling( gfx::NativeView child, const gfx::Point& location) { + gfx::Point content_location = location; if (is_frameless_) { // If the window is resizable it should claim mouse events that fall on the // window border. @@ -828,13 +993,24 @@ bool CefWindowView::ShouldDescendIntoChildForEventHandling( if (IsWindowBorderHit(result)) { return false; } + // |location| is in the host Widget's coordinate space, which the frame + // view offsets from the contents view by the frame border insets. + content_location -= ncfv->GetBoundsForClientView().OffsetFromOrigin(); } } // The window should claim mouse events that fall within the draggable region. return !draggable_region_.get() || - !draggable_region_->contains(location.x(), location.y()); + !draggable_region_->contains(content_location.x(), + content_location.y()); +} + +#if BUILDFLAG(IS_LINUX) +void CefWindowView::UpdateWebContentsRoundedCorners( + const gfx::RoundedCornersF& radii) { + UpdateWebViewRoundedCorners(this, this, gfx::RectF(GetLocalBounds()), radii); } +#endif // BUILDFLAG(IS_LINUX) void CefWindowView::ViewHierarchyChanged( const views::ViewHierarchyChangedDetails& details) { diff --git a/libcef/browser/views/window_view.h b/libcef/browser/views/window_view.h index 68dc821be..7e3b3e8de 100644 --- a/libcef/browser/views/window_view.h +++ b/libcef/browser/views/window_view.h @@ -11,6 +11,7 @@ #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" +#include "build/build_config.h" #include "cef/include/views/cef_window.h" #include "cef/include/views/cef_window_delegate.h" #include "cef/libcef/browser/views/overlay_view_host.h" @@ -21,6 +22,10 @@ #include "ui/views/widget/widget_delegate.h" #include "ui/views/widget/widget_observer.h" +namespace gfx { +class RoundedCornersF; +} + class CefWindowWidgetDelegate; // Owned by the Widget, via CefWindowWidgetDelegate. @@ -71,6 +76,12 @@ class CefWindowView : public CefPanelView, bool ShouldDescendIntoChildForEventHandling(gfx::NativeView child, const gfx::Point& location); +#if BUILDFLAG(IS_LINUX) + // Rounds each web contents surface in the window, per corner, only where it + // is flush with a window corner. |radii| holds the window radius. + void UpdateWebContentsRoundedCorners(const gfx::RoundedCornersF& radii); +#endif + // views::View methods: void ViewHierarchyChanged( const views::ViewHierarchyChangedDetails& details) override; diff --git a/tests/ceftests/test_util.cc b/tests/ceftests/test_util.cc index ef573fc1d..96f45d634 100644 --- a/tests/ceftests/test_util.cc +++ b/tests/ceftests/test_util.cc @@ -8,6 +8,15 @@ #include #include +#include "include/base/cef_build.h" + +#if defined(OS_LINUX) && defined(CEF_X11) +#include +// Definitions conflict with gtest. +#undef None +#undef Bool +#endif + #include "include/base/cef_callback.h" #include "include/cef_base.h" #include "include/cef_command_line.h" @@ -335,6 +344,32 @@ bool IsRunningOnWayland() { #endif } +bool IsRunningOnX11WithCompositor() { +#if defined(OS_LINUX) && defined(CEF_X11) + static bool has_compositor = []() { + if (IsRunningOnWayland()) { + return false; + } + Display* display = XOpenDisplay(nullptr); + if (!display) { + return false; + } + // Probe the EWMH compositor selection. Owned means a compositing window + // manager is running on this screen. Mirrors x11::VisualManager. + const std::string atom_name = + "_NET_WM_CM_S" + std::to_string(DefaultScreen(display)); + Atom cm_atom = + XInternAtom(display, atom_name.c_str(), 0 /*only_if_exists*/); + Window owner = XGetSelectionOwner(display, cm_atom); + XCloseDisplay(display); + return owner != 0; + }(); + return has_compositor; +#else + return false; +#endif +} + std::string ComputeViewsWindowTitle(CefRefPtr window, CefRefPtr browser_view) { std::string title = "CefTest - Views - "; diff --git a/tests/ceftests/test_util.h b/tests/ceftests/test_util.h index 51272811f..6f55d17a2 100644 --- a/tests/ceftests/test_util.h +++ b/tests/ceftests/test_util.h @@ -108,6 +108,10 @@ std::string ComputeNativeWindowTitle(bool use_alloy_style); // Returns true if running on the Wayland platform (Linux only). bool IsRunningOnWayland(); +// Returns true if running on X11 with a compositing window manager (Linux +// only). +bool IsRunningOnX11WithCompositor(); + // Returns true if BFCache is enabled. bool IsBFCacheEnabled(); diff --git a/tests/ceftests/views/window_unittest.cc b/tests/ceftests/views/window_unittest.cc index 6b20bcc6e..d55cb3c6d 100644 --- a/tests/ceftests/views/window_unittest.cc +++ b/tests/ceftests/views/window_unittest.cc @@ -585,6 +585,35 @@ void WindowAcceleratorImpl(CefRefPtr event) { TestWindowDelegate::RunTest(event, std::move(config)); } +// Minimum frame border per edge. Matches the value from +// views::FrameViewLayoutLinux::kResizeBorder. +const int kResizeBorder = 10; + +void RunWindowFramelessClientBounds(CefRefPtr window) { + window->Show(); + + // The frameless client area is inset by at least the resize border on each + // edge. The border requires a compositor, which Wayland always provides and + // X11 provides only when a compositing window manager is running. + if (IsRunningOnWayland() || IsRunningOnX11WithCompositor()) { + CefRect outer = window->GetBoundsInScreen(); + CefRect client = window->GetClientAreaBoundsInScreen(); + EXPECT_GE(client.x - outer.x, kResizeBorder); + EXPECT_GE(client.y - outer.y, kResizeBorder); + EXPECT_GE((outer.x + outer.width) - (client.x + client.width), + kResizeBorder); + EXPECT_GE((outer.y + outer.height) - (client.y + client.height), + kResizeBorder); + } +} + +void WindowFramelessClientBoundsImpl(CefRefPtr event) { + auto config = std::make_unique(); + config->on_window_created = base::BindOnce(RunWindowFramelessClientBounds); + config->frameless = true; + TestWindowDelegate::RunTest(event, std::move(config)); +} + } // namespace // Test window functionality. This is primarily to exercise exposed CEF APIs @@ -606,6 +635,7 @@ WINDOW_TEST_ASYNC(WindowMinimize) WINDOW_TEST_ASYNC(WindowMinimizeFrameless) WINDOW_TEST_ASYNC(WindowFullscreen) WINDOW_TEST_ASYNC(WindowFullscreenFrameless) +WINDOW_TEST_ASYNC(WindowFramelessClientBounds) WINDOW_TEST_ASYNC(WindowIcon) WINDOW_TEST_ASYNC(WindowIconFrameless) WINDOW_TEST_ASYNC(WindowAccelerator)