From 729d808f6240c3590ec0d1669598f3a48214f43e Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Mon, 8 Mar 2021 16:42:29 -0600 Subject: [PATCH] macOS: make conversation and main header draggable --- stylesheets/_global.scss | 2 + stylesheets/_modules.scss | 8 ++ .../components/ConversationHeader.scss | 4 +- ts/background.ts | 20 ++--- ts/components/MainHeader.tsx | 1 + .../util/isWindowDragElement_test.ts | 74 +++++++++++++++++++ ts/util/isWindowDragElement.ts | 21 ++++++ 7 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 ts/test-electron/util/isWindowDragElement_test.ts create mode 100644 ts/util/isWindowDragElement.ts diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index f373f55a66..1513e15ae7 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -21,8 +21,10 @@ body { // It'd be great if we could use the `:fullscreen` selector here, but that does not seem // to work with Electron, at least on macOS. --title-bar-drag-area-height: 0px; // Needs to have a unit to work with `calc()`. + --draggable-app-region: initial; &.os-macos:not(.full-screen) { --title-bar-drag-area-height: 28px; + --draggable-app-region: drag; } } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 1c552f7098..0d96dee1b2 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -4217,6 +4217,8 @@ button.module-conversation-details__action-button { // Module: Main Header .module-main-header { + -webkit-app-region: var(--draggable-app-region); + height: calc(#{$header-height} + var(--title-bar-drag-area-height)); width: 100%; @@ -4236,11 +4238,16 @@ button.module-conversation-details__action-button { } } + &__avatar { + -webkit-app-region: no-drag; + } + &__search { flex-grow: 1; position: relative; display: flex; flex-direction: row; + -webkit-app-region: no-drag; &__input { flex-grow: 1; @@ -4388,6 +4395,7 @@ button.module-conversation-details__action-button { width: 24px; height: 24px; + -webkit-app-region: no-drag; @include light-theme { @include color-svg($icon, $color-gray-90); diff --git a/stylesheets/components/ConversationHeader.scss b/stylesheets/components/ConversationHeader.scss index bdb17adfe1..83f3b238c0 100644 --- a/stylesheets/components/ConversationHeader.scss +++ b/stylesheets/components/ConversationHeader.scss @@ -8,11 +8,11 @@ --button-spacing: 16px; } + -webkit-app-region: var(--draggable-app-region); padding-top: var(--title-bar-drag-area-height); display: flex; flex-direction: row; align-items: center; - height: calc(#{$header-height} + var(--title-bar-drag-area-height)); @include light-theme { @@ -78,6 +78,7 @@ &--clickable { @include button-reset; border-radius: 4px; + -webkit-app-region: no-drag; // These are clobbered by button-reset: margin-left: 4px; @@ -171,6 +172,7 @@ &__button { $icon-size: 32px; + -webkit-app-region: no-drag; @include button-reset; align-items: stretch; border-radius: 4px; diff --git a/ts/background.ts b/ts/background.ts index f470923719..78842ca2d1 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -4,6 +4,8 @@ import { DataMessageClass } from './textsecure.d'; import { MessageAttributesType } from './model-types.d'; import { WhatIsThis } from './window.d'; +import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; +import { isWindowDragElement } from './util/isWindowDragElement'; import { assert } from './util/assert'; export async function startApp(): Promise { @@ -78,16 +80,14 @@ export async function startApp(): Promise { }, }); - window.addEventListener('dblclick', (event: Event) => { - const target = event.target as HTMLElement; - const isDoubleClickOnTitleBar = Boolean( - target.classList.contains('module-title-bar-drag-area') || - target.closest('module-title-bar-drag-area') - ); - if (isDoubleClickOnTitleBar) { - window.titleBarDoubleClick(); - } - }); + if (getTitleBarVisibility() === TitleBarVisibility.Hidden) { + window.addEventListener('dblclick', (event: Event) => { + const target = event.target as HTMLElement; + if (isWindowDragElement(target)) { + window.titleBarDoubleClick(); + } + }); + } // Globally disable drag and drop document.body.addEventListener( diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index f88550dd06..b1a5a4f57c 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -367,6 +367,7 @@ export class MainHeader extends React.Component { {({ ref }) => ( { + const crel = (tagName: string, appRegion?: string): Element => { + const result = document.createElement(tagName); + if (appRegion) { + result.style.cssText = `-webkit-app-region: ${appRegion}`; + } + return result; + }; + + let sandboxEl: HTMLElement; + + beforeEach(() => { + sandboxEl = document.createElement('div'); + document.body.appendChild(sandboxEl); + }); + + afterEach(() => { + sandboxEl.remove(); + }); + + it('returns false for elements with no -webkit-app-region property in the heirarchy', () => { + const root = crel('div'); + const outer = crel('span'); + const inner = crel('div'); + root.appendChild(outer); + outer.appendChild(inner); + sandboxEl.appendChild(root); + + assert.isFalse(isWindowDragElement(root)); + assert.isFalse(isWindowDragElement(outer)); + assert.isFalse(isWindowDragElement(inner)); + }); + + it('returns false for elements with -webkit-app-region: drag on a sub-element', () => { + const parent = crel('div'); + const child = crel('div', 'drag'); + parent.appendChild(child); + sandboxEl.appendChild(parent); + + assert.isFalse(isWindowDragElement(parent)); + }); + + it('returns false if any element up the chain is found to be -webkit-app-region: no-drag', () => { + const root = crel('div', 'drag'); + const outer = crel('div', 'no-drag'); + const inner = crel('div'); + root.appendChild(outer); + outer.appendChild(inner); + sandboxEl.appendChild(root); + + assert.isFalse(isWindowDragElement(outer)); + assert.isFalse(isWindowDragElement(inner)); + }); + + it('returns true if any element up the chain is found to be -webkit-app-region: drag', () => { + const root = crel('div', 'drag'); + const outer = crel('div'); + const inner = crel('div'); + root.appendChild(outer); + outer.appendChild(inner); + sandboxEl.appendChild(root); + + assert.isTrue(isWindowDragElement(root)); + assert.isTrue(isWindowDragElement(outer)); + assert.isTrue(isWindowDragElement(inner)); + }); +}); diff --git a/ts/util/isWindowDragElement.ts b/ts/util/isWindowDragElement.ts new file mode 100644 index 0000000000..174a0a5e17 --- /dev/null +++ b/ts/util/isWindowDragElement.ts @@ -0,0 +1,21 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function isWindowDragElement(el: Readonly): boolean { + let currentEl: Element | null = el; + do { + const appRegion = getComputedStyle(currentEl).getPropertyValue( + '-webkit-app-region' + ); + switch (appRegion) { + case 'no-drag': + return false; + case 'drag': + return true; + default: + currentEl = currentEl.parentElement; + break; + } + } while (currentEl); + return false; +}