From a8db160c3e8acc3a86164fb00c84295fb7e13138 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:02:05 +0100 Subject: [PATCH] feat: add mirror transforms OS-20201 (#158) --- docs/api/browser-window.md | 4 + docs/api/structures/web-preferences.md | 4 + patches/chromium/.patches | 1 + ...20201_add_mirrored_window_transforms.patch | 203 ++++++++ shell/browser/api/electron_api_base_window.cc | 15 +- shell/browser/native_window_views.cc | 16 + shell/browser/web_contents_preferences.cc | 4 + spec/api-browser-window-spec.ts | 477 +++++------------- 8 files changed, 383 insertions(+), 341 deletions(-) create mode 100644 patches/chromium/os-20201_add_mirrored_window_transforms.patch diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 7aee2e99ce115..42dd91d00180d 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1819,6 +1819,10 @@ with `addBrowserView` or `setBrowserView`. The top-most BrowserView is the last * `rot90` - The widget content is rotated to portrait at 90 degrees (clockwise). * `rot180` - The widget content is rotated to portrait at 180 degrees (clockwise). * `rot270` - The widget content is rotated to portrait at 270 degrees (clockwise). + * `mirror` - The widget content is mirrored horizontally. + * `mirror_rot90` - The widget content is mirrored horizontally and rotated 90 degrees clockwise. + * `mirror_rot180` - The widget content is mirrored horizontally and rotated 180 degrees clockwise. + * `mirror_rot270` - The widget content is mirrored horizontally and rotated 270 degrees clockwise. This method sets the browser window's transform. diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index d41881eab07ec..d6d25c951b0fe 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -167,6 +167,10 @@ * `rot90` - The widget content is rotated to portrait at 90 degrees (clockwise). * `rot180` - The widget content is rotated to portrait at 180 degrees (clockwise). * `rot270` - The widget content is rotated to portrait at 270 degrees (clockwise). + * `mirror` - The widget content is mirrored horizontally. + * `mirror_rot90` - The widget content is mirrored horizontally and rotated 90 degrees clockwise. + * `mirror_rot180` - The widget content is mirrored horizontally and rotated 180 degrees clockwise. + * `mirror_rot270` - The widget content is mirrored horizontally and rotated 270 degrees clockwise. [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5 diff --git a/patches/chromium/.patches b/patches/chromium/.patches index dd7fc8b7657b2..aeabc38bd9c21 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -200,3 +200,4 @@ feat_brightsign_add_setattribute_and_setsyncparams_os-21421.patch os-20711_repaint_on_late_video_size_changed.patch fix_brightsign_fire_timechanged_for_equal-time-seeks_on-seek_os-21551.patch brightsign_guard_against_late_player_callbacks_racing_teardown.patch +os-20201_add_mirrored_window_transforms.patch diff --git a/patches/chromium/os-20201_add_mirrored_window_transforms.patch b/patches/chromium/os-20201_add_mirrored_window_transforms.patch new file mode 100644 index 0000000000000..11e7e828dcabe --- /dev/null +++ b/patches/chromium/os-20201_add_mirrored_window_transforms.patch @@ -0,0 +1,203 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 21 Jul 2026 12:00:00 +0000 +Subject: Add hardware-accelerated mirrored window transforms + +Extend window and overlay transforms with mirrored quarter turns. Propagate +these transforms through Viz and the Linux Wayland, Vulkan, and DRM hardware +presentation paths, and update Aura bounds, popup, and input transforms. + +diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc +index 6e03d616d77d1c9da483b8777142925f78d1427c..6d0bf614341bb142e9aac161b961561ff444d18e 100644 +--- a/content/browser/renderer_host/render_widget_host_view_aura.cc ++++ b/content/browser/renderer_host/render_widget_host_view_aura.cc +@@ -393,6 +393,20 @@ RenderWidgetHostViewAura::RenderWidgetHostViewAura( + case blink::mojom::WindowTransformType::kWindowTransformTypeRotate270: + window_transform_ = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270; + break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeMirror: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL; ++ break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate90: ++ // Since its flip horizontal and rotate 90, it is equivalent to flip vertical and rotate 270. ++ window_transform_ = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270; ++ break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate180: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL; ++ break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate270: ++ // Since its flip horizontal and rotate 270, it is equivalent to flip vertical and rotate 90. ++ window_transform_ = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90; ++ break; + default: + window_transform_ = gfx::OVERLAY_TRANSFORM_NONE; + break; +@@ -3600,6 +3614,14 @@ gfx::Rect RenderWidgetHostViewAura::GetTransformedPopupBounds(const gfx::Rect& n + transformed_bounds.Offset(parent_bounds.OffsetFromOrigin()); + + switch (window_transform_) { ++ case gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL: ++ transformed_bounds.Offset(-original_popup_bounds_.width(), 0); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL: ++ transformed_bounds.Offset(0, -original_popup_bounds_.height()); ++ break; ++ + case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90: + transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); + transformed_bounds.Offset(-original_popup_bounds_.height(), 0); +@@ -3614,6 +3636,16 @@ gfx::Rect RenderWidgetHostViewAura::GetTransformedPopupBounds(const gfx::Rect& n + transformed_bounds.Offset(0, -original_popup_bounds_.width()); + break; + ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270: ++ transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); ++ transformed_bounds.Offset(-original_popup_bounds_.height(), ++ -original_popup_bounds_.width()); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90: ++ transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); ++ break; ++ + default: + break; + } +diff --git a/third_party/blink/public/mojom/webpreferences/web_preferences.mojom b/third_party/blink/public/mojom/webpreferences/web_preferences.mojom +index d877f7c2e908f1f3188b4f499269da5388037285..b0d5e37425ce7a8be36fc36511d9ab13c260f839 100644 +--- a/third_party/blink/public/mojom/webpreferences/web_preferences.mojom ++++ b/third_party/blink/public/mojom/webpreferences/web_preferences.mojom +@@ -120,6 +120,10 @@ enum WindowTransformType { + kWindowTransformTypeRotate90, + kWindowTransformTypeRotate180, + kWindowTransformTypeRotate270, ++ kWindowTransformTypeMirror, ++ kWindowTransformTypeMirrorRotate90, ++ kWindowTransformTypeMirrorRotate180, ++ kWindowTransformTypeMirrorRotate270, + }; + + struct WebPreferences { +diff --git a/ui/aura/window_tree_host.cc b/ui/aura/window_tree_host.cc +index 2d753066243c6cd5f95c3d356b62ef6003b9340b..49e22badb8e725be20b8c513a2c40a637d35f04b 100644 +--- a/ui/aura/window_tree_host.cc ++++ b/ui/aura/window_tree_host.cc +@@ -186,6 +186,7 @@ void WindowTreeHost::SetDisplayTransformHint(gfx::OverlayTransform transform) { + return; + + compositor()->SetDisplayTransformHint(transform); ++ compositor()->ScheduleFullRedraw(); + OnHostResizedInPixels(GetBoundsInPixels().size()); + CalculateInputTransform(transform); + } +@@ -208,7 +209,9 @@ gfx::Rect WindowTreeHost::GetTransformedWindowBounds(const gfx::Rect& new_bounds + + if (IsPopup()) { + if (transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || +- transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) { ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270) { + // Make the window bounds equal the original popup bounds with the width and height swapped. + transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); + } +@@ -216,7 +219,9 @@ gfx::Rect WindowTreeHost::GetTransformedWindowBounds(const gfx::Rect& new_bounds + else { + gfx::Rect window_bounds = GetBoundsInPixels(); + if ((transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || +- transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) && ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270) && + window_bounds.height() && window_bounds.width()) { + float window_aspect_ratio = (float)window_bounds.width() / (float)window_bounds.height(); + float new_aspect_ratio = (float)new_bounds.width() / (float)new_bounds.height(); +@@ -237,7 +242,9 @@ gfx::Rect WindowTreeHost::GetNonTransformedWindowBounds(const gfx::Rect& new_bou + gfx::Rect window_bounds = GetBoundsInPixels(); + + if ((transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || +- transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) && ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270) && + window_bounds.height() && window_bounds.width()) { + float window_aspect_ratio = (float)window_bounds.width() / (float)window_bounds.height(); + float new_aspect_ratio = (float)new_bounds.width() / (float)new_bounds.height(); +@@ -257,6 +264,18 @@ void WindowTreeHost::CalculateInputTransform(gfx::OverlayTransform transform) { + popup_transform_.MakeIdentity(); + + switch (transform) { ++ case gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL: ++ input_transform_ = gfx::Transform::Affine( ++ -1, 0, 0, 1, GetBoundsInPixels().width(), 0); ++ popup_transform_ = input_transform_; ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL: ++ input_transform_ = gfx::Transform::Affine( ++ 1, 0, 0, -1, 0, GetBoundsInPixels().height()); ++ popup_transform_ = input_transform_; ++ break; ++ + case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90: + input_transform_.Translate(0, GetBoundsInPixels().width()); + input_transform_.Rotate(-90); +@@ -281,6 +300,20 @@ void WindowTreeHost::CalculateInputTransform(gfx::OverlayTransform transform) { + popup_transform_.Rotate(-90); + break; + ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270: ++ input_transform_ = gfx::Transform::Affine( ++ 0, -1, -1, 0, GetBoundsInPixels().height(), ++ GetBoundsInPixels().width()); ++ popup_transform_ = gfx::Transform::Affine( ++ 0, -1, -1, 0, GetBoundsInPixels().width(), ++ GetBoundsInPixels().height()); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90: ++ input_transform_ = gfx::Transform::Affine(0, 1, 1, 0, 0, 0); ++ popup_transform_ = input_transform_; ++ break; ++ + default: + break; + } +diff --git a/ui/ozone/platform/drm/gpu/hardware_display_plane_atomic.cc b/ui/ozone/platform/drm/gpu/hardware_display_plane_atomic.cc +index 80bf68d8a25df51811f2d704d21544f22c7458f7..44516d4818c636d0038565f9f5a0d02114ec04b6 100644 +--- a/ui/ozone/platform/drm/gpu/hardware_display_plane_atomic.cc ++++ b/ui/ozone/platform/drm/gpu/hardware_display_plane_atomic.cc +@@ -37,7 +37,9 @@ uint32_t OverlayTransformToDrmRotationPropertyValue( + case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270: + return DRM_MODE_ROTATE_90; + case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90: ++ return DRM_MODE_REFLECT_Y | DRM_MODE_ROTATE_90; + case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270: ++ return DRM_MODE_REFLECT_Y | DRM_MODE_ROTATE_270; + default: + NOTREACHED(); + } +@@ -56,6 +58,8 @@ bool IsRotationTransformSupported(gfx::OverlayTransform transform, + bool is_original_buffer) { + if ((transform == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90) || + (transform == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) || ++ (transform == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90) || ++ (transform == gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270) || + (transform == gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL)) { + if (is_original_buffer && (format_fourcc == DRM_FORMAT_NV12 || + format_fourcc == DRM_FORMAT_P010)) { +diff --git a/ui/ozone/platform/wayland/common/wayland_util.cc b/ui/ozone/platform/wayland/common/wayland_util.cc +index 193888cefc1846d2b3a22fbab9d76c5907c3a24b..a2f99629978bc2fcc6fa969e2d92ad26899185fc 100644 +--- a/ui/ozone/platform/wayland/common/wayland_util.cc ++++ b/ui/ozone/platform/wayland/common/wayland_util.cc +@@ -147,7 +147,9 @@ wl_output_transform ToWaylandTransform(gfx::OverlayTransform transform) { + case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270: + return WL_OUTPUT_TRANSFORM_90; + case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90: ++ return WL_OUTPUT_TRANSFORM_FLIPPED_270; + case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270: ++ return WL_OUTPUT_TRANSFORM_FLIPPED_90; + default: + break; + } diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc index 7565790d3165e..3a599d7e25e06 100644 --- a/shell/browser/api/electron_api_base_window.cc +++ b/shell/browser/api/electron_api_base_window.cc @@ -1153,7 +1153,19 @@ void BaseWindow::SetWindowTransform(const std::string& transform) { blink::mojom::WindowTransformType transform_type = blink::mojom::WindowTransformType::kWindowTransformTypeNone; - if (transform == "rot180") { + if (transform == "mirror") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeMirror; + } else if (transform == "mirror_rot90") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate90; + } else if (transform == "mirror_rot180") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate180; + } else if (transform == "mirror_rot270") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate270; + } else if (transform == "rot180") { transform_type = blink::mojom::WindowTransformType::kWindowTransformTypeRotate180; } else if (transform == "rot270") { @@ -1163,6 +1175,7 @@ void BaseWindow::SetWindowTransform(const std::string& transform) { transform_type = blink::mojom::WindowTransformType::kWindowTransformTypeRotate90; } + window_->SetWindowTransform(transform_type); } #endif diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 352e91c0a0c80..28bb3d18873e6 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -557,6 +557,22 @@ void NativeWindowViews::SetWindowTransform( case blink::mojom::WindowTransformType::kWindowTransformTypeRotate270: window_transform = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270; break; + case blink::mojom::WindowTransformType::kWindowTransformTypeMirror: + window_transform = gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL; + break; + // Since its flip horizontal and rotate 90, it is equivalent to flip + // vertical and rotate 270. + case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate90: + window_transform = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270; + break; + case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate180: + window_transform = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL; + break; + // Since its flip horizontal and rotate 270, it is equivalent to flip + // vertical and rotate 90. + case blink::mojom::WindowTransformType::kWindowTransformTypeMirrorRotate270: + window_transform = gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90; + break; default: window_transform = gfx::OVERLAY_TRANSFORM_NONE; break; diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index fd44dc63cae80..4fa7591126050 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -79,6 +79,10 @@ struct Converter { using Val = blink::mojom::WindowTransformType; static constexpr auto Lookup = base::MakeFixedFlatMap({ + {"mirror", Val::kWindowTransformTypeMirror}, + {"mirror_rot180", Val::kWindowTransformTypeMirrorRotate180}, + {"mirror_rot270", Val::kWindowTransformTypeMirrorRotate270}, + {"mirror_rot90", Val::kWindowTransformTypeMirrorRotate90}, {"none", Val::kWindowTransformTypeNone}, {"rot180", Val::kWindowTransformTypeRotate180}, {"rot270", Val::kWindowTransformTypeRotate270}, diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index 07e031b3b90af..3384fe2a7382d 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -7651,6 +7651,20 @@ describe('BrowserWindow module', () => { const offsetBounds = { x: 100, y: 200, width: 600, height: 400 }; const negativeOffsetBounds = { x: -5, y: -5, width: 600, height: 400 }; const fullScreenBounds = { x: 0, y: 0, width: screenSize.width - 1, height: screenSize.height - 1 }; + const windowBounds = [ + { description: 'landscape', bounds: landscapeBounds }, + { description: 'portrait', bounds: portraitBounds }, + { description: 'square', bounds: squareBounds }, + { description: 'x,y offset', bounds: offsetBounds }, + { description: 'x,y negative offset', bounds: negativeOffsetBounds }, + { description: 'full screen', bounds: fullScreenBounds } + ]; + const rotatedResizeBounds = [ + { description: 'portraitBounds', bounds: portraitBounds }, + { description: 'squareBounds', bounds: squareBounds }, + { description: 'bounds with x,y offsets', bounds: offsetBounds }, + { description: 'bounds with negative x,y offsets', bounds: negativeOffsetBounds } + ]; const SELECT_HEIGHT = 10; const SELECT_WIDTH = 20; const SELECT_OPTION_HEIGHT = 18; @@ -7666,139 +7680,122 @@ describe('BrowserWindow module', () => { const MOUSE_X_OFFSET = 10; const MOUSE_Y_OFFSET = 5; + type WindowTransform = Parameters[0]; + type WindowTransformTest = { + name: WindowTransform; + description: string; + swapsDimensions: boolean; + transformPosition: (bounds: Electron.Rectangle, x: number, y: number) => { x: number; y: number }; + }; + const windowTransforms: readonly WindowTransformTest[] = [ + { + name: 'none', + description: 'identity', + swapsDimensions: false, + transformPosition: (_bounds, x, y) => ({ x, y }) + }, + { + name: 'rot90', + description: 'rot90', + swapsDimensions: true, + transformPosition: (bounds, x, y) => ({ x: bounds.width - y, y: x }) + }, + { + name: 'rot180', + description: 'rot180', + swapsDimensions: false, + transformPosition: (bounds, x, y) => ({ x: bounds.width - x, y: bounds.height - y }) + }, + { + name: 'mirror', + description: 'mirror', + swapsDimensions: false, + transformPosition: (bounds, x, y) => ({ x: bounds.width - x, y }) + }, + { + name: 'mirror_rot90', + description: 'mirror_rot90', + swapsDimensions: true, + transformPosition: (bounds, x, y) => ({ x: bounds.width - y, y: bounds.height - x }) + }, + { + name: 'mirror_rot180', + description: 'mirror_rot180', + swapsDimensions: false, + transformPosition: (bounds, x, y) => ({ x, y: bounds.height - y }) + }, + { + name: 'mirror_rot270', + description: 'mirror_rot270', + swapsDimensions: true, + transformPosition: (_bounds, x, y) => ({ x: y, y: x }) + }, + { + name: 'rot270', + description: 'rot270', + swapsDimensions: true, + transformPosition: (bounds, x, y) => ({ x: y, y: bounds.height - x }) + } + ]; + const identityTransform = windowTransforms.find(({ name }) => name === 'none')!; + const rot270Transform = windowTransforms.find(({ name }) => name === 'rot270')!; let w: BrowserWindow; - const clickMouse = (w: BrowserWindow, pos: {x: number, y: number}) => { + const clickMouse = (w: BrowserWindow, pos: { x: number; y: number }) => { w.webContents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: pos.x, y: pos.y }); w.webContents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: pos.x, y: pos.y }); }; - // Enable to help debug problems. - // const dumpImageForDebug = async (filename: string) => { - // const screen = await captureScreen(); - // const pngImage = screen.toPNG(); - // fs.writeFileSync(filename, pngImage); - // } - - describe('webpreference', () => { - afterEach(closeAllWindows); - after(() => { w = null as unknown as BrowserWindow; }); - - [{ name: 'landscape', bounds: landscapeBounds }, { name: 'portrait', bounds: portraitBounds }, { name: 'square', bounds: squareBounds }, - { name: 'x,y offset', bounds: offsetBounds }, { name: 'x,y negative offset', bounds: negativeOffsetBounds }, - { name: 'full screen', bounds: fullScreenBounds }].forEach((item) => { - it(`identity: ${item.name}`, async () => { - w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'none' } }); - await w.loadURL(url); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(item.bounds.width); - expect(windowHeight).to.be.equal(item.bounds.height); - expectBoundsEqual(w.getBounds(), item.bounds); - - await setTimeout(500); - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: item.bounds.x + SELECT_WIDTH_OFFSET, - y: item.bounds.y + SELECT_HEIGHT_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: item.bounds.x + SELECT_OPTION_WIDTH_OFFSET, - y: item.bounds.y + SELECT_OPTION_HEIGHT_OFFSET - }) - ); - }); + const verifyWindowTransform = async (bounds: Electron.Rectangle, transform: WindowTransformTest) => { + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript( + '({windowWidth: window.innerWidth, windowHeight: window.innerHeight})' + ); + expect(windowWidth).to.be.equal(transform.swapsDimensions ? bounds.height : bounds.width); + expect(windowHeight).to.be.equal(transform.swapsDimensions ? bounds.width : bounds.height); + expectBoundsEqual(w.getBounds(), bounds); - it(`rot90: ${item.name}`, async () => { - w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot90' } }); - await w.loadURL(url); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(item.bounds.height); - expect(windowHeight).to.be.equal(item.bounds.width); - expectBoundsEqual(w.getBounds(), item.bounds); - - await setTimeout(500); - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: item.bounds.x + item.bounds.width - SELECT_HEIGHT_OFFSET, - y: item.bounds.y + SELECT_WIDTH_OFFSET - }) - ); + // Enable to help debug problems (note run test harness with C=1 prefix). + // dumpImageForDebug(`window-transform-${transform.name}-${bounds.width}x${bounds.height}`); - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: item.bounds.x + item.bounds.width - SELECT_OPTION_HEIGHT_OFFSET, - y: item.bounds.y + SELECT_OPTION_WIDTH_OFFSET - }) - ); - }); + const selectPosition = transform.transformPosition(bounds, SELECT_WIDTH_OFFSET, SELECT_HEIGHT_OFFSET); + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches(HexColors.RED, () => ({ + x: bounds.x + selectPosition.x, + y: bounds.y + selectPosition.y + })); - it(`rot180: ${item.name}`, async () => { - w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot180' } }); - await w.loadURL(url); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(item.bounds.width); - expect(windowHeight).to.be.equal(item.bounds.height); - expectBoundsEqual(w.getBounds(), item.bounds); - - await setTimeout(500); - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: item.bounds.x + item.bounds.width - SELECT_WIDTH_OFFSET, - y: item.bounds.y + item.bounds.height - SELECT_HEIGHT_OFFSET - }) - ); + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + // dumpImageForDebug(`window-transform-${transform.name}-${bounds.width}x${bounds.height}-mouse-clicked`); - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: item.bounds.x + item.bounds.width - SELECT_OPTION_WIDTH_OFFSET, - y: item.bounds.y + item.bounds.height - SELECT_OPTION_HEIGHT_OFFSET - }) - ); - }); + const optionPosition = transform.transformPosition( + bounds, + SELECT_OPTION_WIDTH_OFFSET, + SELECT_OPTION_HEIGHT_OFFSET + ); + await screenCapture.expectColorAtPointOnDisplayMatches(HexColors.GREEN, () => ({ + x: bounds.x + optionPosition.x, + y: bounds.y + optionPosition.y + })); + }; + // Enable to help debug problems + // const dumpImageForDebug = async (filePrefix: string) => { + // const screenCapture = new ScreenCapture(display); + // await screenCapture.takeScreenshot(filePrefix); + // }; - it(`rot270: ${item.name}`, async () => { - w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot270' } }); - await w.loadURL(url); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(item.bounds.height); - expect(windowHeight).to.be.equal(item.bounds.width); - expectBoundsEqual(w.getBounds(), item.bounds); - - await setTimeout(500); - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: item.bounds.x + SELECT_HEIGHT_OFFSET, - y: item.bounds.y + item.bounds.height - SELECT_WIDTH_OFFSET - }) - ); + describe('webpreference', () => { + afterEach(closeAllWindows); + after(() => { + w = null as unknown as BrowserWindow; + }); - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: item.bounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: item.bounds.y + item.bounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); + windowBounds.forEach(({ description, bounds }) => { + windowTransforms.forEach((transform) => { + it(`${transform.description}: ${description}`, async () => { + w = new BrowserWindow({ ...bounds, frame: false, webPreferences: { windowTransform: transform.name } }); + await w.loadURL(url); + await verifyWindowTransform(bounds, transform); + }); }); }); }); @@ -7812,230 +7809,30 @@ describe('BrowserWindow module', () => { await closeWindow(w); w = null as unknown as BrowserWindow; }); - afterEach(() => { clickMouse(w, { x: 500, y: 500 }); }); - - it('rot90', async () => { - w.setWindowTransform('rot90'); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(landscapeBounds.height); - expect(windowHeight).to.be.equal(landscapeBounds.width); - expectBoundsEqual(w.getBounds(), landscapeBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: landscapeBounds.x + landscapeBounds.width - SELECT_HEIGHT_OFFSET, - y: landscapeBounds.y + SELECT_WIDTH_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: landscapeBounds.x + landscapeBounds.width - SELECT_OPTION_HEIGHT_OFFSET, - y: landscapeBounds.y + SELECT_OPTION_WIDTH_OFFSET - }) - ); - }); - - it('rot180', async () => { - w.setWindowTransform('rot180'); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(landscapeBounds.width); - expect(windowHeight).to.be.equal(landscapeBounds.height); - expectBoundsEqual(w.getBounds(), landscapeBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: landscapeBounds.x + landscapeBounds.width - SELECT_WIDTH_OFFSET, - y: landscapeBounds.y + landscapeBounds.height - SELECT_HEIGHT_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: landscapeBounds.x + landscapeBounds.width - SELECT_OPTION_WIDTH_OFFSET, - y: landscapeBounds.y + landscapeBounds.height - SELECT_OPTION_HEIGHT_OFFSET - }) - ); - }); - - it('rot270', async () => { - w.setWindowTransform('rot270'); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(landscapeBounds.height); - expect(windowHeight).to.be.equal(landscapeBounds.width); - expectBoundsEqual(w.getBounds(), landscapeBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: landscapeBounds.x + SELECT_HEIGHT_OFFSET, - y: landscapeBounds.y + landscapeBounds.height - SELECT_WIDTH_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: landscapeBounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: landscapeBounds.y + landscapeBounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); - }); - - it('Resize while rotated to portraitBounds', async () => { - w.setBounds(portraitBounds); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(portraitBounds.height); - expect(windowHeight).to.be.equal(portraitBounds.width); - expectBoundsEqual(w.getBounds(), portraitBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: portraitBounds.x + SELECT_HEIGHT_OFFSET, - y: portraitBounds.y + portraitBounds.height - SELECT_WIDTH_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: portraitBounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: portraitBounds.y + portraitBounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); - }); - - it('Resize while rotated to squareBounds', async () => { - w.setBounds(squareBounds); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(squareBounds.height); - expect(windowHeight).to.be.equal(squareBounds.width); - expectBoundsEqual(w.getBounds(), squareBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: squareBounds.x + SELECT_HEIGHT_OFFSET, - y: squareBounds.y + squareBounds.height - SELECT_WIDTH_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: squareBounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: squareBounds.y + squareBounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); - }); - - it('Resize while rotated to bounds with x,y offsets', async () => { - w.setBounds(offsetBounds); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(offsetBounds.height); - expect(windowHeight).to.be.equal(offsetBounds.width); - expectBoundsEqual(w.getBounds(), offsetBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: offsetBounds.x + SELECT_HEIGHT_OFFSET, - y: offsetBounds.y + offsetBounds.height - SELECT_WIDTH_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: offsetBounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: offsetBounds.y + offsetBounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); + afterEach(async () => { + clickMouse(w, { x: 200, y: 200 }); + await setTimeout(100); }); - it('Resize while rotated to bounds with negative x,y offsets', async () => { - w.setBounds(negativeOffsetBounds); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(negativeOffsetBounds.height); - expect(windowHeight).to.be.equal(negativeOffsetBounds.width); - expectBoundsEqual(w.getBounds(), negativeOffsetBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: negativeOffsetBounds.x + SELECT_HEIGHT_OFFSET, - y: negativeOffsetBounds.y + negativeOffsetBounds.height - SELECT_WIDTH_OFFSET - }) - ); + windowTransforms + .filter(({ name }) => name !== 'none') + .forEach((transform) => { + it(transform.description, async () => { + w.setWindowTransform(transform.name); + await verifyWindowTransform(landscapeBounds, transform); + }); + }); - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: negativeOffsetBounds.x + SELECT_OPTION_HEIGHT_OFFSET, - y: negativeOffsetBounds.y + negativeOffsetBounds.height - SELECT_OPTION_WIDTH_OFFSET - }) - ); + rotatedResizeBounds.forEach(({ description, bounds }) => { + it(`Resize while rotated to ${description}`, async () => { + w.setBounds(bounds); + await verifyWindowTransform(bounds, rot270Transform); + }); }); it('identity', async () => { w.setWindowTransform('none'); - await setTimeout(500); - const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); - expect(windowWidth).to.be.equal(negativeOffsetBounds.width); - expect(windowHeight).to.be.equal(negativeOffsetBounds.height); - expectBoundsEqual(w.getBounds(), negativeOffsetBounds); - - const screenCapture = new ScreenCapture(display); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.RED, - () => ({ - x: negativeOffsetBounds.x + SELECT_WIDTH_OFFSET, - y: negativeOffsetBounds.y + SELECT_HEIGHT_OFFSET - }) - ); - - clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); - await setTimeout(500); - await screenCapture.expectColorAtPointOnDisplayMatches( - HexColors.GREEN, - () => ({ - x: negativeOffsetBounds.x + SELECT_OPTION_WIDTH_OFFSET, - y: negativeOffsetBounds.y + SELECT_OPTION_HEIGHT_OFFSET - }) - ); + await verifyWindowTransform(negativeOffsetBounds, identityTransform); }); it('Check fullscreen bottom right option in correct location', async () => {