Repository: electron/electron
Pull request: fix: ensure bounds stability in `OnWidgetBoundsChanged`

#### Description of Change

Closes https://github.com/electron/electron/issues/43417.

Fixes an issue where moving a window and not resizing it would cause errant `resize` events to be emitted. This was happening because  `GetWindowBoundsInScreen` has a ~1 pixel margin of error. 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. There's not really a way to test this unfortunately as it occurs as a direct result of user interaction.

Output from logs during testing while I moved the window around:

<details><summary>Sample Logs</summary>
<p>

```
[27956:0822/153943.250:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.258:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.259:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.261:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.261:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.270:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.270:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.277:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.278:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.286:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.287:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.295:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.296:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.298:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.298:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.312:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.313:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.319:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.320:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.328:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.329:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.336:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.337:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.340:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.341:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.348:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.349:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.357:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.358:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.365:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.366:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.373:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.373:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.382:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.383:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.390:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.391:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.399:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.400:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.406:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.408:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.415:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.416:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.424:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.424:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.428:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.429:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.436:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.436:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.444:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.445:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.452:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.453:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.461:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.462:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.469:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.470:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.477:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.478:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.487:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.487:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.490:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.490:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.499:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.500:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.507:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.508:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.515:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.516:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.524:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.525:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.531:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.532:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.540:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.541:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.548:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.550:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.557:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.558:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.565:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.565:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.573:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.574:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.577:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.577:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.586:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.587:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.595:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x600
[27956:0822/153943.595:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.602:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x601
[27956:0822/153943.603:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.611:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.612:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.619:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.620:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.627:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
[27956:0822/153943.628:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.632:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.632:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.645:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=801x601
[27956:0822/153943.646:INFO:native_window_views.cc(1652)] OnWidgetBoundsChanged widget_size_=800x600 new_bounds.size()=800x600
```

</p>
</details> 

#### 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 with `resize` events being emitted on Windows when the window was moved but not resized.


Changed files:
filename: shell/browser/native_window_views.cc
patch: @@ -22,6 +22,7 @@
 
 #include "base/containers/contains.h"
 #include "base/memory/raw_ref.h"
+#include "base/numerics/ranges.h"
 #include "base/strings/utf_string_conversions.h"
 #include "content/public/browser/desktop_media_id.h"
 #include "content/public/common/color_parser.h"
@@ -1637,10 +1638,18 @@ void NativeWindowViews::OnWidgetBoundsChanged(views::Widget* changed_widget,
   if (changed_widget != widget())
     return;
 
-  // Note: We intentionally use `GetBounds()` instead of `bounds` to properly
-  // handle minimized windows on Windows.
+  // |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 {
+    return base::IsApproximatelyEqual(old_size.width(), new_size.width(), 1) &&
+           base::IsApproximatelyEqual(old_size.height(), new_size.height(), 1);
+  };
+
+  // We use |GetBounds| to account for minimized windows on Windows.
   const auto new_bounds = GetBounds();
-  if (widget_size_ != new_bounds.size()) {
+  if (!areWithinOnePixel(widget_size_, new_bounds.size())) {
     NotifyWindowResize();
     widget_size_ = new_bounds.size();
   }