Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Tabs transitions / drag & drop / realtime tear-off #11720

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
574 changes: 574 additions & 0 deletions app/browser/reducers/tabDraggingReducer.js

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion app/browser/reducers/tabsReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ const tabsReducer = (state, action, immutableAction) => {
const isPinned = tabState.isTabPinned(state, tabId)
const nonPinnedTabs = tabState.getNonPinnedTabsByWindowId(state, windowId)
const pinnedTabs = tabState.getPinnedTabsByWindowId(state, windowId)

// if there is 2 or more non-pinned tabs (including the current one)
// or 1 or more non-pinned tab as well as 1 or more pinned tab
// just close the tab
if (nonPinnedTabs.size > 1 ||
(nonPinnedTabs.size > 0 && pinnedTabs.size > 0)) {
setImmediate(() => {
Expand All @@ -311,6 +313,7 @@ const tabsReducer = (state, action, immutableAction) => {
tabs.closeTab(tabId, action.get('forceClosePinned'))
})
} else {
// there would be no pinned or unpinned tabs remaining, close the window
windows.closeWindow(windowId)
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/browser/reducers/windowsReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ const setWindowPosition = (browserOpts, defaults, immutableWindowState) => {
browserOpts.x = firstDefinedValue(browserOpts.x, browserOpts.left, browserOpts.screenX)
browserOpts.y = firstDefinedValue(browserOpts.y, browserOpts.top, browserOpts.screenY)
}
if (browserOpts.offsetX) {
browserOpts.x += browserOpts.offsetX
}
if (browserOpts.offsetY) {
browserOpts.y += browserOpts.offsetY
}
return browserOpts
}

Expand Down
3 changes: 2 additions & 1 deletion app/browser/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ const api = {
})
},

moveTo: (state, tabId, frameOpts, browserOpts, toWindowId) => {
moveTo: (state, tabId, frameOpts, browserOpts, toWindowId, onReady) => {
frameOpts = makeImmutable(frameOpts)
browserOpts = makeImmutable(browserOpts)
if (shouldDebugTabEvents) {
Expand Down Expand Up @@ -1116,6 +1116,7 @@ const api = {
win.webContents.emit('detached-tab-new-window')
}
})
tab.once('tab-inserted-at', onReady)
// make sure frame has latest guestinstanceid
const guestInstanceId = tabValue && tabValue.get('guestInstanceId')
if (guestInstanceId != null) {
Expand Down
6 changes: 3 additions & 3 deletions app/browser/windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,9 +566,9 @@ const api = {
},

/** Specialist function for providing an existing window for
* Buffer Window. Normally this should not be used as one
* will automatically be created with `getOrCreateBufferWindow`
*/
* Buffer Window. Normally this should not be used as one
* will automatically be created with `getOrCreateBufferWindow`
*/
setWindowIsBufferWindow: (dragBufferWindowId) => {
// close existing buffer window if it exists
const existingBufferWindow = api.getBufferWindow()
Expand Down
193 changes: 191 additions & 2 deletions app/common/lib/browserWindowUtil.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,187 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/

const { nativeImage } = require('electron')
const electron = require('electron')
const { isDarwin, isLinux } = require('./platformUtil.js')

const { app, remote, nativeImage, BrowserWindow } = electron
let screen
function initScreen () {
// work with both in-process api and inter-process api
screen = electron.screen
if (!screen && remote) {
screen = remote.screen
}
}
if (app && !app.isReady()) {
app.on('ready', initScreen)
} else {
initScreen()
}

// assume Windows and macOS have 0px frame sizes
let _getWindowFrameSize = !isLinux() ? Promise.resolve({ left: 0, top: 0 }) : null

function calculateWindowFrameSize () {
return new Promise(function (resolve, reject) {
const testWindow = new BrowserWindow({
width: 0,
height: 0,
minimumWidth: 0,
minimumHeight: 0,
x: 100,
y: 100,
show: false,
frame: true
})
const initial = testWindow.getPosition()
testWindow.once('show', () => {
testWindow.once('move', () => {
console.log('getFrameSize move', initial, testWindow.getPosition())
const afterShown = testWindow.getPosition()
testWindow.hide()
testWindow.close()
resolve({
left: afterShown[0] - initial[0],
top: afterShown[1] - initial[1]
})
})
})
testWindow.showInactive()
})
}

function getWindowFrameSize () {
// do not start calculating until something has asked for the data,
// but cache the result (only perform the calculation once)
_getWindowFrameSize = _getWindowFrameSize || calculateWindowFrameSize()
return _getWindowFrameSize
}

/**
* Determines the screen point for a window's client point
*/
function getScreenPointAtWindowClientPoint (browserWindow, clientPoint) {
const contentPosition = browserWindow.getContentBounds()
const x = Math.floor(contentPosition.x + clientPoint.x)
const y = Math.floor(contentPosition.y + clientPoint.y)
return { x, y }
}

function getWindowClientPointAtScreenPoint (browserWindow, screenPoint) {
const contentPosition = browserWindow.getContentBounds()
return {
x: screenPoint.x - contentPosition.x,
y: screenPoint.y - contentPosition.y
}
}

function getWindowClientSize (browserWindow) {
const [width, height] = browserWindow.getContentSize()
return {
width,
height
}
}

/**
* Gets the window's client position of the mouse cursor
*/
function getWindowClientPointAtCursor (browserWindow) {
return getWindowClientPointAtScreenPoint(browserWindow, screen.getCursorScreenPoint())
}

/**
* Determines where a window should be positioned
* given the requirement that a client position is
* at a given screen position
*
* @param {*} screenPoint - the screen position
* @param {*} clientPoint - the client position
*/
async function getWindowPositionForClientPointAtScreenPoint (screenPoint, clientPoint) {
const frameSize = await getWindowFrameSize()
const x = Math.floor(screenPoint.x - clientPoint.x - frameSize.left)
const y = Math.floor(screenPoint.y - clientPoint.y - frameSize.top)
return { x, y }
}

/**
* Determines where a window should be positioned
* given the requirement that a client position is
* at the current cursor poisition
*
* @param {*} clientPoint - the client position
*/
function getWindowPositionForClientPointAtCursor (clientPoint) {
return getWindowPositionForClientPointAtScreenPoint(screen.getCursorScreenPoint(), clientPoint)
}

function moveClientPositionToMouseCursor (browserWindow, clientPoint, {
cursorScreenPoint = screen.getCursorScreenPoint(),
animate = false
} = {}) {
return moveClientPositionToScreenPoint(browserWindow, clientPoint, cursorScreenPoint, { animate })
}

async function moveClientPositionToScreenPoint (browserWindow, clientPoint, screenPoint, { animate = false } = {}) {
const windowScreenPoint = await getWindowPositionForClientPointAtScreenPoint(screenPoint, clientPoint)
browserWindow.setPosition(windowScreenPoint.x, windowScreenPoint.y, animate)
}

function animateWindowPosition (browserWindow, { fromPoint, getDestinationPoint }) {
// electron widow position animation is darwin-only
if (!isDarwin()) {
moveWindowToDestination(browserWindow, getDestinationPoint)
}
browserWindow.setPosition(fromPoint.x, fromPoint.y)
// just in case
let attempts = 190
const checkEveryMs = 16
const moveWindow = () => {
attempts--
if (attempts === 0) {
moveWindowToDestination(browserWindow, getDestinationPoint)
return
}
if (browserWindow.isVisible()) {
const [curX, curY] = browserWindow.getPosition()
if (curX === fromPoint.x && curY === fromPoint.y) {
moveWindowToDestination(browserWindow, getDestinationPoint, true)
} else {
setTimeout(moveWindow, checkEveryMs)
}
}
}
setTimeout(moveWindow, checkEveryMs)
}

function moveWindowToDestination (browserWindow, getDestinationPoint, animate = false) {
const toPoint = getDestinationPoint()
browserWindow.setPosition(toPoint.x, toPoint.y, animate)
}

function isClientPointWithinWindowBounds (browserWindow, windowClientPoint) {
const [width, height] = browserWindow.getSize()
return windowClientPoint.x >= 0 &&
windowClientPoint.x < width &&
windowClientPoint.y >= 0 &&
windowClientPoint.y < height
}

function isMouseCursorOverWindowContent (browserWindow, cursorScreenPoint = screen.getCursorScreenPoint()) {
const windowClientPoint = getWindowClientPointAtScreenPoint(browserWindow, cursorScreenPoint)
return isClientPointWithinWindowBounds(browserWindow, windowClientPoint)
}

function mirrorWindowSizeAndPosition (browserWindow, otherWindow) {
const [winX, winY] = otherWindow.getPosition()
const [width, height] = otherWindow.getSize()
browserWindow.setPosition(winX, winY)
browserWindow.setSize(width, height)
}

// BrowserWindow ctor options which can be manually
// set after window creation
Expand Down Expand Up @@ -108,6 +287,16 @@ function setPropertiesOnExistingWindow (browserWindow, properties, debug = false
}

module.exports = {
getWindowClientPointAtCursor,
getWindowClientPointAtScreenPoint,
moveClientPositionToMouseCursor,
animateWindowPosition,
getWindowPositionForClientPointAtCursor,
getScreenPointAtWindowClientPoint,
getWindowClientSize,
isMouseCursorOverWindowContent,
isClientPointWithinWindowBounds,
mirrorWindowSizeAndPosition,
canSetAllPropertiesOnExistingWindow,
setPropertiesOnExistingWindow
}
62 changes: 62 additions & 0 deletions app/common/lib/screenUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

const { EventEmitter } = require('events')
const electron = require('electron')

const INTERVAL_POLL_MOUSEPOSITION_MS = 10
let isObservingCursorPosition = false

const screenUtil = new EventEmitter()

screenUtil.on('newListener', (event) => {
if (event === 'mousemove') {
startObservingMousePosition()
}
})

screenUtil.on('removeListener', (event) => {
if (event === 'mousemove' && isObservingCursorPosition) {
const listenerCount = screenUtil.listenerCount('mousemove')
if (!listenerCount) {
stopObservingMousePosition()
}
}
})

function startObservingMousePosition () {
if (!isObservingCursorPosition) {
isObservingCursorPosition = true
continuouslyReportMousePosition()
}
}

let timeoutObserveMousePosition
function stopObservingMousePosition () {
isObservingCursorPosition = false
if (timeoutObserveMousePosition) {
clearTimeout(timeoutObserveMousePosition)
}
}

let cachePosX, cachePosY
function continuouslyReportMousePosition () {
timeoutObserveMousePosition = null
if (!isObservingCursorPosition) {
return
}
const mouseScreenPos = electron.screen.getCursorScreenPoint()
if (mouseScreenPos.x !== cachePosX || mouseScreenPos.y !== cachePosY) {
cachePosX = mouseScreenPos.x
cachePosY = mouseScreenPos.y
screenUtil.emit('mousemove', {
screenX: cachePosX,
screenY: cachePosY
})
}
// stop continuation of event if we've stopped listening
timeoutObserveMousePosition = setTimeout(continuouslyReportMousePosition, INTERVAL_POLL_MOUSEPOSITION_MS)
}

module.exports = screenUtil
36 changes: 36 additions & 0 deletions app/common/lib/webContentsUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/

// HACK mousemove will only trigger in the other window if the coords are inside the bounds but
// will trigger for this window even if the mouse is outside the window, since we started a dragEvent,
// *but* it will forward anything for globalX and globalY, so we'll send the client coords in those properties
// and send some fake coords in the clientX and clientY properties
// An alternative solution would be for the other window to just call electron API
// to get mouse cursor, and we could just send 0, 0 coords, but this reduces the spread of electron
// calls in components, and also puts the (tiny) computation in another process, freeing the other
// window to perform the animation,
// Or perhaps use a manual ipc channel
function createEventForSendMouseMoveInput (screenX, screenY, modifiers = [ ]) {
return {
type: 'mousemove',
x: 1, // identifier that we have created event manually, see HACK notes above
y: 99, // ^
globalX: screenX,
globalY: screenY,
modifiers
}
}

// HACK - see the related `createEventFromSendMouseMoveInput`
function translateEventFromSendMouseMoveInput (receivedEvent) {
return (receivedEvent.x === 1 && receivedEvent.y === 99)
? { clientX: receivedEvent.screenX || 0, clientY: receivedEvent.screenY || 0 }
: receivedEvent
}

module.exports = {
createEventForSendMouseMoveInput,
translateEventFromSendMouseMoveInput
}
Loading