Browse Source

fix: menus on Linux after window modification (#37906)

* fix: menus on Linux after window modification

Co-authored-by: David Sanders <[email protected]>

* test: don't run on CI

Co-authored-by: David Sanders <[email protected]>

* chore: fix .patches

* test: refactor since types are off

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <[email protected]>
Co-authored-by: electron-patch-conflict-fixer[bot] <83340002+electron-patch-conflict-fixer[bot]@users.noreply.github.com>
trop[bot] 2 years ago
parent
commit
3a027a743b

+ 1 - 0
patches/chromium/.patches

@@ -126,3 +126,4 @@ expose_v8initializer_codegenerationcheckcallbackinmainthread.patch
 chore_patch_out_profile_methods_in_profile_selections_cc.patch
 fix_x11_window_restore_minimized_maximized_window.patch
 chore_defer_usb_service_getdevices_request_until_usb_service_is.patch
+revert_x11_keep_windowcache_alive_for_a_time_interval.patch

+ 240 - 0
patches/chromium/revert_x11_keep_windowcache_alive_for_a_time_interval.patch

@@ -0,0 +1,240 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: David Sanders <[email protected]>
+Date: Sun, 2 Apr 2023 05:52:30 -0700
+Subject: Revert "[X11] Keep WindowCache alive for a time interval"
+
+This reverts commit bbc20a9c1b91ac6d6035408748091369cc96d4d7.
+
+While intended as a performance improvement, the commit breaks
+Views menus on X11 after certain window events such as resizing,
+or maximizing and unmaximizing.
+
+The patch can be removed once the upstream issue is fixed. That
+was reported in https://crbug.com/1429935.
+
+diff --git a/ui/base/x/x11_whole_screen_move_loop.cc b/ui/base/x/x11_whole_screen_move_loop.cc
+index 08e1c09749c5c39d99deec75c1a914c43936a6a5..ccd4785415a743a9519e750d5f0e334632058654 100644
+--- a/ui/base/x/x11_whole_screen_move_loop.cc
++++ b/ui/base/x/x11_whole_screen_move_loop.cc
+@@ -25,6 +25,7 @@
+ #include "ui/events/x/x11_event_translation.h"
+ #include "ui/gfx/x/connection.h"
+ #include "ui/gfx/x/keysyms/keysyms.h"
++#include "ui/gfx/x/window_cache.h"
+ #include "ui/gfx/x/x11_window_event_manager.h"
+ #include "ui/gfx/x/xproto.h"
+ 
+@@ -150,6 +151,10 @@ bool X11WholeScreenMoveLoop::RunMoveLoop(
+   auto* connection = x11::Connection::Get();
+   CreateDragInputWindow(connection);
+ 
++  // Keep a window cache alive for the duration of the drag so that the drop
++  // target under the drag window can be quickly determined.
++  x11::WindowCache cache(connection, connection->default_root(), true);
++
+   // Only grab mouse capture of |grab_input_window_| if |can_grab_pointer| is
+   // true aka the source that initiated the move loop doesn't have explicit
+   // grab.
+diff --git a/ui/gfx/x/window_cache.cc b/ui/gfx/x/window_cache.cc
+index 9c603366a657954b4be44d0a30fcf428265f95e7..03885b55354c37e04bc016b509ac2ae8bd6191c2 100644
+--- a/ui/gfx/x/window_cache.cc
++++ b/ui/gfx/x/window_cache.cc
+@@ -11,8 +11,6 @@
+ #include "base/notreached.h"
+ #include "base/ranges/algorithm.h"
+ #include "base/run_loop.h"
+-#include "base/task/single_thread_task_runner.h"
+-#include "base/time/time.h"
+ #include "ui/gfx/geometry/insets.h"
+ #include "ui/gfx/geometry/rect.h"
+ #include "ui/gfx/geometry/vector2d.h"
+@@ -24,23 +22,19 @@
+ 
+ namespace x11 {
+ 
+-const base::TimeDelta kDestroyTimerInterval = base::Seconds(3);
+-
+ Window GetWindowAtPoint(const gfx::Point& point_px,
+                         const base::flat_set<Window>* ignore) {
+   auto* connection = Connection::Get();
+   Window root = connection->default_root();
+ 
+-  if (!WindowCache::instance()) {
+-    auto instance =
+-        std::make_unique<WindowCache>(connection, connection->default_root());
+-    auto* cache = instance.get();
+-    cache->BeginDestroyTimer(std::move(instance));
++  if (auto* instance = WindowCache::instance()) {
++    instance->WaitUntilReady();
++    return instance->GetWindowAtPoint(point_px, root, ignore);
+   }
+ 
+-  auto* instance = WindowCache::instance();
+-  instance->WaitUntilReady();
+-  return instance->GetWindowAtPoint(point_px, root, ignore);
++  WindowCache cache(connection, connection->default_root(), false);
++  cache.WaitUntilReady();
++  return cache.GetWindowAtPoint(point_px, root, ignore);
+ }
+ 
+ ScopedShapeEventSelector::ScopedShapeEventSelector(Connection* connection,
+@@ -62,21 +56,24 @@ WindowCache::WindowInfo::~WindowInfo() = default;
+ // static
+ WindowCache* WindowCache::instance_ = nullptr;
+ 
+-WindowCache::WindowCache(Connection* connection, Window root)
++WindowCache::WindowCache(Connection* connection, Window root, bool track_events)
+     : connection_(connection),
+       root_(root),
++      track_events_(track_events),
+       gtk_frame_extents_(GetAtom("_GTK_FRAME_EXTENTS")) {
+   DCHECK(!instance_) << "Only one WindowCache should be active at a time";
+   instance_ = this;
+ 
+   connection_->AddEventObserver(this);
+ 
+-  // We select for SubstructureNotify events on all windows (to receive
+-  // CreateNotify events), which will cause events to be sent for all child
+-  // windows.  This means we need to additionally select for StructureNotify
+-  // changes for the root window.
+-  root_events_ =
+-      std::make_unique<XScopedEventSelector>(root_, EventMask::StructureNotify);
++  if (track_events_) {
++    // We select for SubstructureNotify events on all windows (to receive
++    // CreateNotify events), which will cause events to be sent for all child
++    // windows.  This means we need to additionally select for StructureNotify
++    // changes for the root window.
++    root_events_ = std::make_unique<XScopedEventSelector>(
++        root_, EventMask::StructureNotify);
++  }
+   AddWindow(root_, Window::None);
+ }
+ 
+@@ -106,16 +103,6 @@ void WindowCache::WaitUntilReady() {
+     last_processed_event_ = events[event - 1].sequence();
+ }
+ 
+-void WindowCache::BeginDestroyTimer(std::unique_ptr<WindowCache> self) {
+-  DCHECK_EQ(this, self.get());
+-  delete_when_destroy_timer_fires_ = false;
+-  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+-      FROM_HERE,
+-      base::BindOnce(&WindowCache::OnDestroyTimerExpired,
+-                     base::Unretained(this), std::move(self)),
+-      kDestroyTimerInterval);
+-}
+-
+ void WindowCache::SyncForTest() {
+   do {
+     // Perform a blocking sync to prevent spinning while waiting for replies.
+@@ -127,7 +114,6 @@ void WindowCache::SyncForTest() {
+ Window WindowCache::GetWindowAtPoint(gfx::Point point_px,
+                                      Window window,
+                                      const base::flat_set<Window>* ignore) {
+-  delete_when_destroy_timer_fires_ = true;
+   if (ignore && ignore->contains(window))
+     return Window::None;
+   auto* info = GetInfo(window);
+@@ -265,10 +251,12 @@ void WindowCache::AddWindow(Window window, Window parent) {
+     return;
+   WindowInfo& info = windows_[window];
+   info.parent = parent;
+-  // Events must be selected before getting the initial window info to
+-  // prevent race conditions.
+-  info.events = std::make_unique<XScopedEventSelector>(
+-      window, EventMask::SubstructureNotify | EventMask::PropertyChange);
++  if (track_events_) {
++    // Events must be selected before getting the initial window info to
++    // prevent race conditions.
++    info.events = std::make_unique<XScopedEventSelector>(
++        window, EventMask::SubstructureNotify | EventMask::PropertyChange);
++  }
+ 
+   AddRequest(connection_->GetWindowAttributes(window),
+              &WindowCache::OnGetWindowAttributesResponse, window);
+@@ -282,8 +270,10 @@ void WindowCache::AddWindow(Window window, Window parent) {
+ 
+   auto& shape = connection_->shape();
+   if (shape.present()) {
+-    info.shape_events =
+-        std::make_unique<ScopedShapeEventSelector>(connection_, window);
++    if (track_events_) {
++      info.shape_events =
++          std::make_unique<ScopedShapeEventSelector>(connection_, window);
++    }
+ 
+     for (auto kind : {Shape::Sk::Bounding, Shape::Sk::Input}) {
+       AddRequest(shape.GetRectangles(window, kind),
+@@ -391,11 +381,4 @@ void WindowCache::OnGetRectanglesResponse(
+   }
+ }
+ 
+-void WindowCache::OnDestroyTimerExpired(std::unique_ptr<WindowCache> self) {
+-  if (!delete_when_destroy_timer_fires_)
+-    return;  // destroy `this`
+-
+-  BeginDestroyTimer(std::move(self));
+-}
+-
+ }  // namespace x11
+diff --git a/ui/gfx/x/window_cache.h b/ui/gfx/x/window_cache.h
+index f241d6c23855fad478813ff3029fa6a17d084d34..ebc05d311ed3719be98180086baae8230ec9c58e 100644
+--- a/ui/gfx/x/window_cache.h
++++ b/ui/gfx/x/window_cache.h
+@@ -78,7 +78,7 @@ class COMPONENT_EXPORT(X11) WindowCache : public EventObserver {
+   // If `track_events` is true, the WindowCache will keep the cache state synced
+   // with the server's state over time. It may be set to false if the cache is
+   // short-lived, if only a single GetWindowAtPoint call is made.
+-  WindowCache(Connection* connection, Window root);
++  WindowCache(Connection* connection, Window root, bool track_events);
+   WindowCache(const WindowCache&) = delete;
+   WindowCache& operator=(const WindowCache&) = delete;
+   ~WindowCache() override;
+@@ -92,10 +92,6 @@ class COMPONENT_EXPORT(X11) WindowCache : public EventObserver {
+   // Blocks until all outstanding requests are processed.
+   void WaitUntilReady();
+ 
+-  // Destroys |self| if no calls to GetWindowAtPoint() are made within
+-  // a time window.
+-  void BeginDestroyTimer(std::unique_ptr<WindowCache> self);
+-
+   void SyncForTest();
+ 
+   const std::unordered_map<Window, WindowInfo>& windows() const {
+@@ -147,12 +143,11 @@ class COMPONENT_EXPORT(X11) WindowCache : public EventObserver {
+                                Shape::Sk kind,
+                                Shape::GetRectanglesResponse response);
+ 
+-  void OnDestroyTimerExpired(std::unique_ptr<WindowCache> self);
+-
+   static WindowCache* instance_;
+ 
+   const raw_ptr<Connection> connection_;
+   const Window root_;
++  const bool track_events_;
+   const Atom gtk_frame_extents_;
+   std::unique_ptr<XScopedEventSelector> root_events_;
+ 
+@@ -164,9 +159,6 @@ class COMPONENT_EXPORT(X11) WindowCache : public EventObserver {
+   // processed in order.
+   absl::optional<uint32_t> last_processed_event_;
+ 
+-  // True iff GetWindowAtPoint() was called since the last timer interval.
+-  bool delete_when_destroy_timer_fires_ = false;
+-
+   // Although only one instance of WindowCache may be created at a time, the
+   // instance will be created and destroyed as needed, so WeakPtrs are still
+   // necessary.
+diff --git a/ui/gfx/x/window_cache_unittest.cc b/ui/gfx/x/window_cache_unittest.cc
+index 2199ddac2577a33ff7a42f3d3752613cef00dd32..af0a2d3737c132b596096514b5ca4f572d6c9d64 100644
+--- a/ui/gfx/x/window_cache_unittest.cc
++++ b/ui/gfx/x/window_cache_unittest.cc
+@@ -21,7 +21,7 @@ class WindowCacheTest : public testing::Test {
+  protected:
+   void ResetCache() {
+     cache_.reset();
+-    cache_ = std::make_unique<WindowCache>(connection_, root_);
++    cache_ = std::make_unique<WindowCache>(connection_, root_, true);
+     cache_->SyncForTest();
+   }
+ 

+ 43 - 1
spec/api-menu-spec.ts

@@ -1,6 +1,6 @@
 import * as cp from 'child_process';
 import * as path from 'path';
-import { expect } from 'chai';
+import { assert, expect } from 'chai';
 import { BrowserWindow, Menu, MenuItem } from 'electron/main';
 import { sortMenuItems } from '../lib/browser/api/menu-utils';
 import { emittedOnce } from './lib/events-helpers';
@@ -877,6 +877,48 @@ describe('Menu module', function () {
         throw new Error('Menu is garbage-collected while popuping');
       }
     });
+
+    // https://github.com/electron/electron/issues/35724
+    // Maximizing window is enough to trigger the bug
+    // FIXME(dsanders11): Test always passes on CI, even pre-fix
+    ifit(process.platform === 'linux' && !process.env.CI)('does not trigger issue #35724', (done) => {
+      const showAndCloseMenu = async () => {
+        await delay(1000);
+        menu.popup({ window: w, x: 50, y: 50 });
+        await delay(500);
+        const closed = new Promise((resolve) => {
+          menu.once('menu-will-close', resolve);
+        });
+        menu.closePopup();
+        await closed;
+      };
+
+      const failOnEvent = () => { done(new Error('Menu closed prematurely')); };
+
+      assert(!w.isVisible());
+      w.on('show', async () => {
+        assert(!w.isMaximized());
+        // Show the menu once, then maximize window
+        await showAndCloseMenu();
+        // NOTE - 'maximize' event never fires on CI for Linux
+        const maximized = emittedOnce(w, 'maximize');
+        w.maximize();
+        await maximized;
+
+        // Bug only seems to trigger programmatically after showing the menu once more
+        await showAndCloseMenu();
+
+        // Now ensure the menu stays open until we close it
+        await delay(500);
+        menu.once('menu-will-close', failOnEvent);
+        menu.popup({ window: w, x: 50, y: 50 });
+        await delay(1500);
+        menu.removeListener('menu-will-close', failOnEvent);
+        menu.once('menu-will-close', () => done());
+        menu.closePopup();
+      });
+      w.show();
+    });
   });
 
   describe('Menu.setApplicationMenu', () => {