From 768ece6b54f78e65efbea2c3fd39eea30e48332b Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 6 Feb 2024 11:30:35 +0100 Subject: [PATCH] feat: add `BrowserWindow.isOccluded()` (#38982) feat: add BrowserWindow.isOccluded() --- docs/api/browser-window.md | 8 +++ shell/browser/api/electron_api_base_window.cc | 5 ++ shell/browser/api/electron_api_base_window.h | 1 + shell/browser/native_window.h | 1 + shell/browser/native_window_mac.h | 1 + shell/browser/native_window_mac.mm | 7 ++- shell/browser/native_window_views.cc | 8 +++ shell/browser/native_window_views.h | 1 + spec/api-browser-window-spec.ts | 59 +++++++++++++++++++ 9 files changed, 89 insertions(+), 2 deletions(-) diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 2abb6f1c34..f6a0884bbc 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -644,6 +644,14 @@ Shows the window but doesn't focus on it. Hides the window. +#### `win.isOccluded()` + +Returns `boolean` - Whether the window is covered by other windows. + +On macOS, a `BrowserWindow` is occluded if the entire window is obscured by other windows. A completely transparent window may also be considered non-occluded. See [docs](https://developer.apple.com/documentation/appkit/nswindowocclusionstate?language=objc) for more details. + +On Windows and Linux, a window is occluded if it or one of its descendants is visible, and all visible windows have their bounds completely covered by fully opaque windows. A window is considered "fully opaque" if it is visible, it is not transparent, and its combined opacity is 1. See [Chromium Source](https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/aura/window.h;l=124-151;drc=7d2d8ccab2b68fbbfc5e1611d45bd4ecf87090b8) for more details. + #### `win.isVisible()` Returns `boolean` - Whether the window is visible to the user in the foreground of the app. diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc index 7b040f8cad..2c2dcdde7f 100644 --- a/shell/browser/api/electron_api_base_window.cc +++ b/shell/browser/api/electron_api_base_window.cc @@ -355,6 +355,10 @@ bool BaseWindow::IsVisible() const { return window_->IsVisible(); } +bool BaseWindow::IsOccluded() const { + return window_->IsOccluded(); +} + bool BaseWindow::IsEnabled() const { return window_->IsEnabled(); } @@ -1086,6 +1090,7 @@ void BaseWindow::BuildPrototype(v8::Isolate* isolate, .SetMethod("showInactive", &BaseWindow::ShowInactive) .SetMethod("hide", &BaseWindow::Hide) .SetMethod("isVisible", &BaseWindow::IsVisible) + .SetMethod("isOccluded", &BaseWindow::IsOccluded) .SetMethod("isEnabled", &BaseWindow::IsEnabled) .SetMethod("setEnabled", &BaseWindow::SetEnabled) .SetMethod("maximize", &BaseWindow::Maximize) diff --git a/shell/browser/api/electron_api_base_window.h b/shell/browser/api/electron_api_base_window.h index 7bff94b776..d3c583e2c9 100644 --- a/shell/browser/api/electron_api_base_window.h +++ b/shell/browser/api/electron_api_base_window.h @@ -98,6 +98,7 @@ class BaseWindow : public gin_helper::TrackableObject, void ShowInactive(); void Hide(); bool IsVisible() const; + bool IsOccluded() const; bool IsEnabled() const; void SetEnabled(bool enable); void Maximize(); diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index bc7868cc07..1b907eae7f 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -87,6 +87,7 @@ class NativeWindow : public base::SupportsUserData, virtual void Show() = 0; virtual void ShowInactive() = 0; virtual void Hide() = 0; + virtual bool IsOccluded() const = 0; virtual bool IsVisible() const = 0; virtual bool IsEnabled() const = 0; virtual void SetEnabled(bool enable) = 0; diff --git a/shell/browser/native_window_mac.h b/shell/browser/native_window_mac.h index fd359e9cce..aa76169936 100644 --- a/shell/browser/native_window_mac.h +++ b/shell/browser/native_window_mac.h @@ -44,6 +44,7 @@ class NativeWindowMac : public NativeWindow, void Show() override; void ShowInactive() override; void Hide() override; + bool IsOccluded() const override; bool IsVisible() const override; bool IsEnabled() const override; void SetEnabled(bool enable) override; diff --git a/shell/browser/native_window_mac.mm b/shell/browser/native_window_mac.mm index 14438b7eb2..3d13a2008a 100644 --- a/shell/browser/native_window_mac.mm +++ b/shell/browser/native_window_mac.mm @@ -561,9 +561,12 @@ void NativeWindowMac::Hide() { [window_ orderOut:nil]; } +bool NativeWindowMac::IsOccluded() const { + return !([window_ occlusionState] & NSWindowOcclusionStateVisible); +} + bool NativeWindowMac::IsVisible() const { - bool occluded = [window_ occlusionState] == NSWindowOcclusionStateVisible; - return [window_ isVisible] && !occluded && !IsMinimized(); + return [window_ isVisible] && !IsMinimized(); } bool NativeWindowMac::IsEnabled() const { diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 4292c0cafe..f3afe1d84e 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -562,6 +562,14 @@ void NativeWindowViews::Hide() { #endif } +bool NativeWindowViews::IsOccluded() const { + if (!GetNativeWindow()) + return false; + auto occlusion_state = + GetNativeWindow()->GetHost()->GetNativeWindowOcclusionState(); + return occlusion_state == aura::Window::OcclusionState::OCCLUDED; +} + bool NativeWindowViews::IsVisible() const { #if BUILDFLAG(IS_WIN) // widget()->IsVisible() calls ::IsWindowVisible, which returns non-zero if a diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index e5683cecf0..ee0c13c888 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -56,6 +56,7 @@ class NativeWindowViews : public NativeWindow, void Hide() override; bool IsVisible() const override; bool IsEnabled() const override; + bool IsOccluded() const override; void SetEnabled(bool enable) override; void Maximize() override; void Unmaximize() override; diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index a8a1fc9380..11e73beff1 100644 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -4164,6 +4164,65 @@ describe('BrowserWindow module', () => { }); }); + describe('BrowserWindow.isOccluded()', () => { + afterEach(closeAllWindows); + + it('returns false for a visible window', async () => { + const w = new BrowserWindow({ show: false }); + + const shown = once(w, 'show'); + w.show(); + await shown; + + expect(w.isOccluded()).to.be.false('window is occluded'); + }); + + it('returns false when the window is only partially obscured', async () => { + const w1 = new BrowserWindow({ width: 400, height: 400 }); + const w2 = new BrowserWindow({ show: false, width: 450, height: 450 }); + + const focused = once(w2, 'focus'); + w2.show(); + await focused; + + await setTimeout(1000); + expect(w1.isOccluded()).to.be.true('window is not occluded'); + + const pos = w2.getPosition(); + const move = once(w2, 'move'); + w2.setPosition(pos[0] - 100, pos[1]); + await move; + + await setTimeout(1000); + expect(w1.isOccluded()).to.be.false('window is occluded'); + }); + + // FIXME: this test fails on Linux CI due to windowing issues. + ifit(process.platform !== 'linux')('returns false for a visible window covered by a transparent window', async () => { + const w1 = new BrowserWindow({ width: 200, height: 200 }); + const w2 = new BrowserWindow({ show: false, transparent: true, frame: false }); + + const focused = once(w2, 'focus'); + w2.show(); + await focused; + + await setTimeout(1000); + expect(w1.isOccluded()).to.be.false('window is occluded'); + }); + + it('returns true for an obscured window', async () => { + const w1 = new BrowserWindow({ width: 200, height: 200 }); + const w2 = new BrowserWindow({ show: false }); + + const focused = once(w2, 'focus'); + w2.show(); + await focused; + + await setTimeout(1000); + expect(w1.isOccluded()).to.be.true('visible window'); + }); + }); + // TODO(codebytere): figure out how to make these pass in CI on Windows. ifdescribe(process.platform !== 'win32')('document.visibilityState/hidden', () => { afterEach(closeAllWindows);