Repository: electron/electron
Pull request: fix: improve single-pixel resize handling on Windows

#### Description of Change

Refs https://github.com/electron/electron/pull/43431
Closes https://github.com/electron/electron/issues/44521

This PR follows up a previous PR to prevent overactive emission of `resize` events when the Window is resized and not moved. However, that PR did not distinguish between `WidgetObserver::OnWidgetBoundsChanged` being called from
`Widget::OnNativeWidgetMove()` and `Widget::OnNativeWidgetSizeChanged`. This led to a new issue where single-pixel resizes done by the user no longer caused `resize` events to be emitted where they should have been.

To handle this more effectively, we should check _both_ whether the window is moving at the time `OnWidgetBoundsChanged` is called via `is_moving_` as well as whether it's within the single-pixel tolerance zone. `is_moving_` is set in `WM_MOVING`, which is emitted only when a user is actively moving a window (e.g. it wouldn't be emitted when the window is being moved programmatically, that would be `WM_WINDOWPOSCHANGING` or `WM_WINDOWPOSCHANGED`).

cc @nikwen

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where `resize` wasn't being emitted for single-pixel resizes on Windows.


Changed files:
filename: shell/browser/native_window_views.cc
patch: @@ -1661,18 +1661,28 @@ void NativeWindowViews::OnWidgetBoundsChanged(views::Widget* changed_widget,
   if (changed_widget != widget())
     return;
 
-  // |GetWindowBoundsInScreen| has a ~1 pixel margin of error, so if we check
-  // existing bounds directly against the new bounds without accounting for that
-  // we'll have constant false positives when the window is moving but the user
-  // hasn't changed its size at all.
-  auto areWithinOnePixel = [](gfx::Size old_size, gfx::Size new_size) -> bool {
+#if BUILDFLAG(IS_WIN)
+  // OnWidgetBoundsChanged is emitted both when a window is moved and when a
+  // window is resized. If the window is moving, then
+  // WidgetObserver::OnWidgetBoundsChanged is being called from
+  // Widget::OnNativeWidgetMove() and not Widget::OnNativeWidgetSizeChanged.
+  // |GetWindowBoundsInScreen| has a ~1 pixel margin
+  // of error because it converts from floats to integers between calculations,
+  // so if we check existing bounds directly against the new bounds without
+  // accounting for that we'll have constant false positives when the window is
+  // moving but the user hasn't changed its size at all.
+  auto isWithinOnePixel = [](gfx::Size old_size, gfx::Size new_size) -> bool {
     return base::IsApproximatelyEqual(old_size.width(), new_size.width(), 1) &&
            base::IsApproximatelyEqual(old_size.height(), new_size.height(), 1);
   };
 
+  if (is_moving_ && isWithinOnePixel(widget_size_, bounds.size()))
+    return;
+#endif
+
   // We use |GetBounds| to account for minimized windows on Windows.
   const auto new_bounds = GetBounds();
-  if (!areWithinOnePixel(widget_size_, new_bounds.size())) {
+  if (widget_size_ != new_bounds.size()) {
     NotifyWindowResize();
     widget_size_ = new_bounds.size();
   }

filename: spec/api-browser-window-spec.ts
patch: @@ -1544,6 +1544,16 @@ describe('BrowserWindow module', () => {
           await expect(once(w, 'resized')).to.eventually.be.fulfilled();
         });
       });
+
+      it('does not emits the resize event for move-only changes', async () => {
+        const [x, y] = w.getPosition();
+
+        w.once('resize', () => {
+          expect.fail('resize event should not be emitted');
+        });
+
+        w.setBounds({ x: x + 10, y: y + 10 });
+      });
     });
 
     describe('BrowserWindow.setSize(width, height)', () => {
@@ -1557,6 +1567,17 @@ describe('BrowserWindow module', () => {
         expectBoundsEqual(w.getSize(), size);
       });
 
+      it('emits the resize event for single-pixel size changes', async () => {
+        const [width, height] = w.getSize();
+        const size = [width + 1, height - 1];
+
+        const resized = once(w, 'resize');
+        w.setSize(size[0], size[1]);
+        await resized;
+
+        expectBoundsEqual(w.getSize(), size);
+      });
+
       ifit(process.platform === 'darwin')('on macOS', () => {
         it('emits \'resized\' event after animating', async () => {
           const size = [300, 400];