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)