| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/platform_window/x11/x11_window.h" |
| |
| #include "base/command_line.h" |
| #include "base/run_loop.h" |
| #include "base/test/task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "ui/base/x/test/x11_property_change_waiter.h" |
| #include "ui/base/x/x11_util.h" |
| #include "ui/display/display_switches.h" |
| #include "ui/display/screen_base.h" |
| #include "ui/events/devices/x11/touch_factory_x11.h" |
| #include "ui/events/event.h" |
| #include "ui/events/platform/x11/x11_event_source.h" |
| #include "ui/events/test/events_test_utils_x11.h" |
| #include "ui/gfx/native_widget_types.h" |
| #include "ui/gfx/transform.h" |
| #include "ui/gfx/x/x11_atom_cache.h" |
| #include "ui/gfx/x/xproto.h" |
| #include "ui/gfx/x/xproto_util.h" |
| #include "ui/platform_window/extensions/x11_extension_delegate.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| constexpr int kPointerDeviceId = 1; |
| |
| class TestPlatformWindowDelegate : public PlatformWindowDelegate { |
| public: |
| TestPlatformWindowDelegate() = default; |
| TestPlatformWindowDelegate(const TestPlatformWindowDelegate&) = delete; |
| TestPlatformWindowDelegate& operator=(const TestPlatformWindowDelegate&) = |
| delete; |
| ~TestPlatformWindowDelegate() override = default; |
| |
| gfx::AcceleratedWidget widget() const { return widget_; } |
| PlatformWindowState state() const { return state_; } |
| |
| void WaitForBoundsSet(const gfx::Rect& expected_bounds) { |
| if (changed_bounds_ == expected_bounds) |
| return; |
| expected_bounds_ = expected_bounds; |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| // PlatformWindowDelegate: |
| void OnBoundsChanged(const gfx::Rect& new_bounds) override { |
| changed_bounds_ = new_bounds; |
| if (!quit_closure_.is_null() && changed_bounds_ == expected_bounds_) |
| std::move(quit_closure_).Run(); |
| } |
| void OnDamageRect(const gfx::Rect& damaged_region) override {} |
| void DispatchEvent(Event* event) override {} |
| void OnCloseRequest() override {} |
| void OnClosed() override {} |
| void OnWindowStateChanged(PlatformWindowState new_state) override { |
| state_ = new_state; |
| } |
| void OnLostCapture() override {} |
| void OnAcceleratedWidgetAvailable(gfx::AcceleratedWidget widget) override { |
| widget_ = widget; |
| } |
| void OnWillDestroyAcceleratedWidget() override {} |
| void OnAcceleratedWidgetDestroyed() override { |
| widget_ = gfx::kNullAcceleratedWidget; |
| } |
| void OnActivationChanged(bool active) override {} |
| void OnMouseEnter() override {} |
| |
| private: |
| gfx::AcceleratedWidget widget_ = gfx::kNullAcceleratedWidget; |
| PlatformWindowState state_ = PlatformWindowState::kUnknown; |
| gfx::Rect changed_bounds_; |
| gfx::Rect expected_bounds_; |
| |
| // Ends the run loop. |
| base::OnceClosure quit_closure_; |
| }; |
| |
| class ShapedX11ExtensionDelegate : public X11ExtensionDelegate { |
| public: |
| ShapedX11ExtensionDelegate() = default; |
| ~ShapedX11ExtensionDelegate() override = default; |
| |
| void OnLostMouseGrab() override {} |
| void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override { |
| int right = size.width(); |
| int bottom = size.height(); |
| |
| window_mask->moveTo(0, 0); |
| window_mask->lineTo(0, bottom); |
| window_mask->lineTo(right, bottom); |
| window_mask->lineTo(right, 10); |
| window_mask->lineTo(right - 10, 10); |
| window_mask->lineTo(right - 10, 0); |
| window_mask->close(); |
| } |
| #if BUILDFLAG(USE_ATK) |
| bool OnAtkKeyEvent(AtkKeyEventStruct* atk_key_event, |
| bool transient) override { |
| return false; |
| } |
| #endif |
| bool IsOverrideRedirect(bool is_tiling_wm) const override { return false; } |
| }; |
| |
| // Blocks till the window state hint, |hint|, is set or unset. |
| class WMStateWaiter : public X11PropertyChangeWaiter { |
| public: |
| WMStateWaiter(x11::Window window, const char* hint, bool wait_till_set) |
| : X11PropertyChangeWaiter(window, "_NET_WM_STATE"), |
| hint_(hint), |
| wait_till_set_(wait_till_set) {} |
| WMStateWaiter(const WMStateWaiter&) = delete; |
| WMStateWaiter& operator=(const WMStateWaiter&) = delete; |
| ~WMStateWaiter() override = default; |
| |
| private: |
| // X11PropertyChangeWaiter: |
| bool ShouldKeepOnWaiting() override { |
| std::vector<x11::Atom> hints; |
| if (GetAtomArrayProperty(xwindow(), "_NET_WM_STATE", &hints)) |
| return base::Contains(hints, x11::GetAtom(hint_)) != wait_till_set_; |
| return true; |
| } |
| |
| // The name of the hint to wait to get set or unset. |
| const char* hint_; |
| |
| // Whether we are waiting for |hint| to be set or unset. |
| bool wait_till_set_; |
| }; |
| |
| class TestScreen : public display::ScreenBase { |
| public: |
| TestScreen() { ProcessDisplayChanged({}, true); } |
| ~TestScreen() override = default; |
| TestScreen(const TestScreen& screen) = delete; |
| TestScreen& operator=(const TestScreen& screen) = delete; |
| |
| void SetScaleAndBoundsForPrimaryDisplay(float scale, |
| const gfx::Rect& bounds_in_pixels) { |
| auto display = GetPrimaryDisplay(); |
| display.SetScaleAndBounds(scale, bounds_in_pixels); |
| ProcessDisplayChanged(display, true); |
| } |
| }; // namespace |
| |
| // Returns the list of rectangles which describe |window|'s bounding region via |
| // the X shape extension. |
| std::vector<gfx::Rect> GetShapeRects(x11::Window window) { |
| std::vector<gfx::Rect> shape_vector; |
| if (auto shape = x11::Connection::Get() |
| ->shape() |
| .GetRectangles({window, x11::Shape::Sk::Bounding}) |
| .Sync()) { |
| for (const auto& rect : shape->rectangles) |
| shape_vector.emplace_back(rect.x, rect.y, rect.width, rect.height); |
| } |
| return shape_vector; |
| } |
| |
| // Returns true if one of |rects| contains point (x,y). |
| bool ShapeRectContainsPoint(const std::vector<gfx::Rect>& shape_rects, |
| int x, |
| int y) { |
| gfx::Point point(x, y); |
| return std::any_of( |
| shape_rects.cbegin(), shape_rects.cend(), |
| [&point](const auto& rect) { return rect.Contains(point); }); |
| } |
| |
| } // namespace |
| |
| class X11WindowTest : public testing::Test { |
| public: |
| X11WindowTest() |
| : task_env_(std::make_unique<base::test::TaskEnvironment>( |
| base::test::TaskEnvironment::MainThreadType::UI)) {} |
| X11WindowTest(const X11WindowTest&) = delete; |
| X11WindowTest& operator=(const X11WindowTest&) = delete; |
| ~X11WindowTest() override = default; |
| |
| void SetUp() override { |
| auto* connection = x11::Connection::Get(); |
| event_source_ = std::make_unique<X11EventSource>(connection); |
| |
| std::vector<int> pointer_devices; |
| pointer_devices.push_back(kPointerDeviceId); |
| ui::TouchFactory::GetInstance()->SetPointerDeviceForTest(pointer_devices); |
| |
| // X11 requires display::Screen instance. |
| test_screen_ = new TestScreen(); |
| display::Screen::SetScreenInstance(test_screen_); |
| |
| // Make X11 synchronous for our display connection. This does not force the |
| // window manager to behave synchronously. |
| connection->SynchronizeForTest(true); |
| } |
| |
| protected: |
| void TearDown() override { |
| x11::Connection::Get()->SynchronizeForTest(false); |
| } |
| |
| std::unique_ptr<X11Window> CreateX11Window( |
| PlatformWindowDelegate* delegate, |
| const gfx::Rect& bounds, |
| X11ExtensionDelegate* x11_extension_delegate) { |
| PlatformWindowInitProperties init_params(bounds); |
| init_params.x11_extension_delegate = x11_extension_delegate; |
| auto window = std::make_unique<X11Window>(delegate); |
| window->Initialize(std::move(init_params)); |
| // Remove native frame so that X server doesn't change our bounds. |
| window->SetUseNativeFrame(false); |
| return window; |
| } |
| |
| void DispatchSingleEventToWidget(x11::Event* x11_event, x11::Window window) { |
| auto* device_event = x11_event->As<x11::Input::DeviceEvent>(); |
| DCHECK(device_event); |
| device_event->event = window; |
| x11::Connection::Get()->DispatchEvent(*x11_event); |
| } |
| |
| private: |
| std::unique_ptr<base::test::TaskEnvironment> task_env_; |
| std::unique_ptr<X11EventSource> event_source_; |
| |
| TestScreen* test_screen_ = nullptr; |
| }; |
| |
| // https://crbug.com/898742: Test might be flaky. Disable again if it is still |
| // flaky after it is moved from views_unittests to x11_unittests. Tests that the |
| // shape is properly set on the x window. |
| TEST_F(X11WindowTest, Shape) { |
| if (!IsShapeExtensionAvailable()) |
| return; |
| |
| // 1) Test setting the window shape via the ShapedX11ExtensionDelegate. This |
| // technique is used to get rounded corners on Chrome windows when not using |
| // the native window frame. |
| TestPlatformWindowDelegate delegate; |
| ShapedX11ExtensionDelegate x11_extension_delegate; |
| constexpr gfx::Rect bounds(100, 100, 100, 100); |
| auto window = CreateX11Window(&delegate, bounds, &x11_extension_delegate); |
| window->Show(false); |
| |
| const x11::Window x11_window = window->window(); |
| ASSERT_TRUE(x11_window != x11::Window::None); |
| |
| // Force update the window region. |
| window->ResetWindowRegion(); |
| |
| auto* connection = x11::Connection::Get(); |
| connection->DispatchAll(); |
| |
| std::vector<gfx::Rect> shape_rects = GetShapeRects(x11_window); |
| ASSERT_FALSE(shape_rects.empty()); |
| |
| // The widget was supposed to be 100x100, but the WM might have ignored this |
| // suggestion. |
| int window_width = window->GetBounds().width(); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, window_width - 15, 5)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, window_width - 5, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, window_width - 5, 15)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, window_width + 5, 15)); |
| |
| // Changing window's size should update the shape. |
| window->SetBounds(gfx::Rect(100, 100, 200, 200)); |
| connection->DispatchAll(); |
| |
| if (window->GetBounds().width() == 200) { |
| shape_rects = GetShapeRects(x11_window); |
| ASSERT_FALSE(shape_rects.empty()); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 85, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 185, 5)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 195, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 195, 15)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 205, 15)); |
| } |
| |
| if (WmSupportsHint(x11::GetAtom("_NET_WM_STATE_MAXIMIZED_VERT"))) { |
| // The shape should be changed to a rectangle which fills the entire screen |
| // when |widget1| is maximized. |
| { |
| WMStateWaiter waiter(x11_window, "_NET_WM_STATE_MAXIMIZED_VERT", true); |
| window->Maximize(); |
| waiter.Wait(); |
| } |
| |
| // Ensure that the task which is posted when a window is resized is run. |
| base::RunLoop().RunUntilIdle(); |
| |
| // xvfb does not support Xrandr so we cannot check the maximized window's |
| // bounds. |
| gfx::Rect maximized_bounds; |
| GetOuterWindowBounds(x11_window, &maximized_bounds); |
| |
| shape_rects = GetShapeRects(x11_window); |
| ASSERT_FALSE(shape_rects.empty()); |
| EXPECT_TRUE( |
| ShapeRectContainsPoint(shape_rects, maximized_bounds.width() - 1, 5)); |
| EXPECT_TRUE( |
| ShapeRectContainsPoint(shape_rects, maximized_bounds.width() - 1, 15)); |
| } |
| |
| // 2) Test setting the window shape via PlatformWindow::SetShape(). |
| auto shape_region = std::make_unique<std::vector<gfx::Rect>>(); |
| shape_region->emplace_back(10, 0, 90, 10); |
| shape_region->emplace_back(0, 10, 10, 90); |
| shape_region->emplace_back(10, 10, 90, 90); |
| |
| TestPlatformWindowDelegate delegate2; |
| constexpr gfx::Rect bounds2(30, 80, 800, 600); |
| auto window2 = CreateX11Window(&delegate2, bounds2, nullptr); |
| window2->Show(false); |
| |
| const x11::Window x11_window2 = window2->window(); |
| ASSERT_TRUE(x11_window2 != x11::Window::None); |
| |
| gfx::Transform transform; |
| transform.Scale(1.0f, 1.0f); |
| window2->SetShape(std::move(shape_region), transform); |
| |
| connection->DispatchAll(); |
| |
| shape_rects = GetShapeRects(x11_window2); |
| ASSERT_FALSE(shape_rects.empty()); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 5, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 105, 15)); |
| |
| // Changing the windows's size should not affect the shape. |
| window2->SetBounds(gfx::Rect(100, 100, 200, 200)); |
| shape_rects = GetShapeRects(x11_window2); |
| ASSERT_FALSE(shape_rects.empty()); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 5, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 105, 15)); |
| |
| // Setting the shape to nullptr resets the shape back to the entire |
| // window bounds. |
| window2->SetShape(nullptr, transform); |
| shape_rects = GetShapeRects(x11_window2); |
| ASSERT_FALSE(shape_rects.empty()); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 5, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); |
| EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 105, 15)); |
| EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 500, 500)); |
| } |
| |
| // Test that the widget reacts on changes in fullscreen state initiated by the |
| // window manager (e.g. via a window manager accelerator key). |
| TEST_F(X11WindowTest, WindowManagerTogglesFullscreen) { |
| if (!WmSupportsHint(x11::GetAtom("_NET_WM_STATE_FULLSCREEN"))) |
| return; |
| |
| auto* connection = x11::Connection::Get(); |
| |
| TestPlatformWindowDelegate delegate; |
| ShapedX11ExtensionDelegate x11_extension_delegate; |
| constexpr gfx::Rect bounds(100, 100, 100, 100); |
| auto window = CreateX11Window(&delegate, bounds, &x11_extension_delegate); |
| x11::Window x11_window = window->window(); |
| window->Show(false); |
| |
| connection->DispatchAll(); |
| |
| EXPECT_NE(window->GetPlatformWindowState(), PlatformWindowState::kFullScreen); |
| |
| gfx::Rect initial_bounds = window->GetBounds(); |
| { |
| WMStateWaiter waiter(x11_window, "_NET_WM_STATE_FULLSCREEN", true); |
| window->ToggleFullscreen(); |
| waiter.Wait(); |
| } |
| EXPECT_EQ(window->GetPlatformWindowState(), PlatformWindowState::kFullScreen); |
| |
| // Emulate the window manager exiting fullscreen via a window manager |
| // accelerator key. |
| { |
| ui::SendClientMessage( |
| x11_window, ui::GetX11RootWindow(), x11::GetAtom("_NET_WM_STATE"), |
| {0, static_cast<uint32_t>(x11::GetAtom("_NET_WM_STATE_FULLSCREEN")), 0, |
| 1, 0}); |
| |
| WMStateWaiter waiter(x11_window, "_NET_WM_STATE_FULLSCREEN", false); |
| waiter.Wait(); |
| } |
| |
| // Ensure it continues in browser fullscreen mode and bounds are restored to |
| // |initial_bounds|. |
| EXPECT_EQ(window->GetPlatformWindowState(), PlatformWindowState::kFullScreen); |
| delegate.WaitForBoundsSet(initial_bounds); |
| EXPECT_EQ(initial_bounds, window->GetBounds()); |
| |
| // Emulate window resize (through X11 configure events) while in browser |
| // fullscreen mode and ensure bounds are tracked correctly. |
| initial_bounds.set_size({400, 400}); |
| { |
| connection->ConfigureWindow({ |
| .window = x11_window, |
| .width = initial_bounds.width(), |
| .height = initial_bounds.height(), |
| }); |
| // Ensure that the task which is posted when a window is resized is run. |
| base::RunLoop().RunUntilIdle(); |
| } |
| EXPECT_EQ(window->GetPlatformWindowState(), PlatformWindowState::kFullScreen); |
| delegate.WaitForBoundsSet(initial_bounds); |
| EXPECT_EQ(initial_bounds, window->GetBounds()); |
| |
| // Calling Widget::SetFullscreen(false) should clear the widget's fullscreen |
| // state and clean things up. |
| window->ToggleFullscreen(); |
| EXPECT_NE(window->GetPlatformWindowState(), PlatformWindowState::kFullScreen); |
| delegate.WaitForBoundsSet(initial_bounds); |
| EXPECT_EQ(initial_bounds, window->GetBounds()); |
| } |
| |
| // Tests that the minimization information is propagated to the |
| // PlatformWindowDelegate. |
| TEST_F(X11WindowTest, ToggleMinimizePropogateToPlatformWindowDelegate) { |
| TestPlatformWindowDelegate delegate; |
| constexpr gfx::Rect bounds(10, 10, 100, 100); |
| auto window = CreateX11Window(&delegate, bounds, nullptr); |
| window->Show(false); |
| window->Activate(); |
| |
| x11::Connection::Get()->DispatchAll(); |
| |
| x11::Window x11_window = window->window(); |
| |
| // Minimize by sending _NET_WM_STATE_HIDDEN |
| { |
| std::vector<x11::Atom> atom_list; |
| atom_list.push_back(x11::GetAtom("_NET_WM_STATE_HIDDEN")); |
| ui::SetAtomArrayProperty(x11_window, "_NET_WM_STATE", "ATOM", atom_list); |
| |
| x11::PropertyNotifyEvent xevent{ |
| .send_event = true, |
| .window = x11_window, |
| .atom = x11::GetAtom("_NET_WM_STATE"), |
| }; |
| x11::SendEvent(xevent, ui::GetX11RootWindow(), |
| x11::EventMask::SubstructureNotify | |
| x11::EventMask::SubstructureRedirect); |
| |
| WMStateWaiter waiter(x11_window, "_NET_WM_STATE_HIDDEN", true); |
| waiter.Wait(); |
| } |
| EXPECT_TRUE(window->IsMinimized()); |
| EXPECT_EQ(delegate.state(), PlatformWindowState::kMinimized); |
| |
| // Show from minimized by sending _NET_WM_STATE_FOCUSED |
| { |
| std::vector<x11::Atom> atom_list; |
| atom_list.push_back(x11::GetAtom("_NET_WM_STATE_FOCUSED")); |
| ui::SetAtomArrayProperty(x11_window, "_NET_WM_STATE", "ATOM", atom_list); |
| |
| x11::PropertyNotifyEvent xevent{ |
| .send_event = true, |
| .window = x11_window, |
| .atom = x11::GetAtom("_NET_WM_STATE"), |
| }; |
| x11::SendEvent(xevent, ui::GetX11RootWindow(), |
| x11::EventMask::SubstructureNotify | |
| x11::EventMask::SubstructureRedirect); |
| |
| WMStateWaiter waiter(x11_window, "_NET_WM_STATE_FOCUSED", true); |
| waiter.Wait(); |
| } |
| EXPECT_FALSE(window->IsMinimized()); |
| EXPECT_EQ(delegate.state(), PlatformWindowState::kNormal); |
| } |
| |
| } // namespace ui |