diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts index 9863588e69..95f03b13fa 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock.store.ts @@ -27,12 +27,8 @@ export class DockStore { ]; protected storage = createStorage("dock", {}); // keep settings in localStorage - public defaultTabId = this.initialTabs[0].id; - private _minHeight = 100; - - set minHeight(val: number) { - this._minHeight = val - } + public readonly defaultTabId = this.initialTabs[0].id; + public readonly minHeight = 100; @observable isOpen = false; @observable fullSize = false; @@ -45,19 +41,24 @@ export class DockStore { } get defaultHeight() { - return Math.round(window.innerHeight / 2.5); + return Math.round(window.innerHeight * 0.4); } get maxHeight() { - const mainLayoutHeader = 40; - const mainLayoutTabs = 33; - const mainLayoutMargin = 16; - const dockTabs = 33; - return window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs; + const mainLayoutHeader = 40 + const mainLayoutTabs = 33 + const mainLayoutMargin = 16 + const dockTabs = 33 + const preferedMax = window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs + return Math.max(preferedMax, this.minHeight) // don't let max < min } constructor() { - Object.assign(this, this.storage.get()); + const stored = this.storage.get() + if (Object.entries(stored).length > 0) { + // don't override perfectly good defaults in the class decl + Object.assign(this, stored); + } reaction(() => ({ isOpen: this.isOpen, @@ -69,7 +70,6 @@ export class DockStore { }); // adjust terminal height if window size changes - this.checkMaxHeight(); window.addEventListener("resize", throttle(this.checkMaxHeight, 250)); } @@ -175,20 +175,19 @@ export class DockStore { @action selectTab(tabId: TabId) { - const tab = this.getTabById(tabId); - this.selectedTabId = tab ? tab.id : null; + this.selectedTabId = this.getTabById(tabId)?.id ?? null; } @action setHeight(height?: number) { - this.height = Math.max(0, Math.min(height || this._minHeight, this.maxHeight)); + this.height = Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)); } @action reset() { this.selectedTabId = this.defaultTabId; this.tabs.replace(this.initialTabs); - this.height = this.defaultHeight; + this.setHeight(this.defaultHeight); this.close(); } } diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index cf3336e9b4..c020d20672 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -4,7 +4,7 @@ import React, { Fragment } from "react"; import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { autobind, cssNames, prevDefault } from "../../utils"; -import { ResizingAnchor, ResizeDragEvent, ResizeDirection } from "../resizing-anchor"; +import { ResizingAnchor, ResizeDirection } from "../resizing-anchor"; import { Icon } from "../icon"; import { Tabs } from "../tabs/tabs"; import { MenuItem } from "../menu"; @@ -29,27 +29,6 @@ interface Props { @observer export class Dock extends React.Component { - onResizeStart = () => { - const { isOpen, open, setHeight } = dockStore; - if (!isOpen) { - open(); - setHeight(); - } - } - - onResize = ({ movementY }: ResizeDragEvent) => { - const { isOpen, close, height, setHeight, minHeight, defaultHeight } = dockStore; - console.log(height, movementY) - const newHeight = height + movementY; - if (height > newHeight && newHeight < minHeight) { - setHeight(defaultHeight); - close(); - } - else if (isOpen) { - setHeight(newHeight); - } - } - onKeydown = (evt: React.KeyboardEvent) => { const { close, closeTab, selectedTab } = dockStore; if (!selectedTab) return; @@ -107,9 +86,14 @@ export class Dock extends React.Component { > dockStore.height} + minExtent={dockStore.minHeight} + maxExtent={dockStore.maxHeight} direction={ResizeDirection.VERTICAL} - onStart={this.onResizeStart} - onDrag={this.onResize} + onStart={dockStore.open} + onMinExtentSubceed={dockStore.close} + onMinExtentExceed={dockStore.open} + onDrag={dockStore.setHeight} />
{ + // Since this function is debounced we need to read this value as late as possible + if (!this.isActive) return; this.fitAddon.fit(); const { cols, rows } = this.xterm; this.api.sendTerminalSize(cols, rows); @@ -150,7 +152,6 @@ export class Terminal { } onResize = () => { - if (!this.isActive) return; this.fitLazy(); this.focus(); } @@ -171,16 +172,16 @@ export class Terminal { // Handle custom hotkey bindings if (ctrlKey) { switch (code) { - // Ctrl+C: prevent terminal exit on windows / linux (?) - case "KeyC": - if (this.xterm.hasSelection()) return false; - break; + // Ctrl+C: prevent terminal exit on windows / linux (?) + case "KeyC": + if (this.xterm.hasSelection()) return false; + break; // Ctrl+W: prevent unexpected terminal tab closing, e.g. editing file in vim // https://github.com/kontena/lens-app/issues/156#issuecomment-534906480 - case "KeyW": - evt.preventDefault(); - break; + case "KeyW": + evt.preventDefault(); + break; } } diff --git a/src/renderer/components/resizing-anchor/resizing-anchor.tsx b/src/renderer/components/resizing-anchor/resizing-anchor.tsx index f337ecf949..9be6b5a9ec 100644 --- a/src/renderer/components/resizing-anchor/resizing-anchor.tsx +++ b/src/renderer/components/resizing-anchor/resizing-anchor.tsx @@ -1,8 +1,9 @@ import "./resizing-anchor.scss"; import React from "react"; -import { cssNames, noop } from "../../utils"; -import { action, computed, observable } from "mobx"; +import { action, observable } from "mobx"; import _ from "lodash" +import { findDOMNode } from "react-dom"; +import { cssNames, noop } from "../../utils"; export enum ResizeDirection { HORIZONTAL = "horizontal", @@ -26,58 +27,160 @@ export enum ResizeSide { TRAILING = "trailing", } +/** + * ResizeGrowthDirection determines how the anchor interprets the drag. + * + * Because the origin of the screen is top left a drag from bottom to top + * results in a negative directional delta. However, if the component being + * dragged grows in the opposite direction, this needs to be compensated for. + */ +export enum ResizeGrowthDirection { + TOP_TO_BOTTOM = 1, + BOTTOM_TO_TOP = -1, + LEFT_TO_RIGHT = 1, + RIGHT_TO_LEFT = -1, +} + interface Props { direction: ResizeDirection; + + /** + * getCurrentExtent should return the current prominent dimention in the + * given resizing direction. Width for HORIZONTAL and height for VERTICAL + */ + getCurrentExtent: () => number; + disabled?: boolean; placement?: ResizeSide; - onStart?: (data: ResizeStartEvent) => void; - onDrag?: (data: ResizeDragEvent) => void; - onEnd?: (data: ResizeEndEvent) => void; + growthDirection?: ResizeGrowthDirection; + + // Ability to restrict which mouse buttons are allowed to resize this component + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + onlyButtons?: number; + + // onStart is called when the ResizeAnchor is first clicked (mouse down) + onStart?: () => void; + + // onEnd is called when the ResizeAnchor is released (mouse up) + onEnd?: () => void; + + /** + * onDrag is called whenever there is a mousemove event. All calls will be + * bounded by matching `onStart` and `onEnd` calls. + */ + onDrag?: (newExtent: number) => void; + + // onDoubleClick is called when the the ResizeAnchor is double clicked + onDoubleClick?: () => void; + + /** + * The following two extents represent the max and min values set to `onDrag` + */ + maxExtent?: number; + minExtent?: number; + + /** + * The following events are triggerred with respect to the above values. + * - The "__Exceed" call will be made when the unbounded extent goes from + * < the above to >= the above + * - The "__Subceed" call is similar but is triggered when the unbounded + * extent goes from >= the above to < the above. + */ + onMaxExtentExceed?: () => void; + onMaxExtentSubceed?: () => void; + onMinExtentSubceed?: () => void; + onMinExtentExceed?: () => void; } -interface InitialPosition { - initX: number; - initY: number; +enum MovementTrigger { + EXCEED_MAX = "exceed", + SUBCEED_MIN = "subceed", } -interface CurrentPosition { - pageX: number; - pageY: number; +interface MovementCalc { + newExtent?: number; + otherTrigger?: MovementTrigger; + ignore?: boolean; } -interface MovementData { - movementX: number; - movementY: number; +interface Position { + readonly pageX: number; + readonly pageY: number; } -export type ResizeStartEvent = InitialPosition -export type ResizeDragEvent = InitialPosition & CurrentPosition & MovementData -export type ResizeEndEvent = InitialPosition & CurrentPosition +/** + * Return the direction delta, but ignore drags leading up to a moved item + * 1. `->|` => return `false` + * 2. `<-|` => return `directed length (P1, P2)` (negative) + * 3. `-|>` => return `directed length (M, P2)` (positive) + * 4. `<|-` => return `directed length (M, P2)` (negative) + * 5. `|->` => return `directed length (P1, P2)` (positive) + * 6. `|<-` => return `false` + * @param P the starting position on the number line + * @param Q the ending position on the number line + * @param M a third point that determines if the delta is meaningful + * @returns the directional difference between including appropriate sign. + */ +function directionDelta(P1: number, P2: number, B: number): number | false { + if (P1 < B) { + if (P2 >= B) { + // case 3 + return Math.abs(B - P2) + } -function calculateMovement(from: CurrentPosition, to: CurrentPosition): MovementData { - return { - movementX: from.pageX - to.pageX, - movementY: from.pageY - to.pageY, + if (P2 < P1) { + // case 2 + return -Math.abs(P1 - P2) + } + + // case 1 + return false } -} -const defaultProps: Partial = { - onStart: noop, - onDrag: noop, - onEnd: noop, - disabled: false, - placement: ResizeSide.LEADING, + if (P2 < B) { + // case 4 + return -Math.abs(B - P2) + } + + if (P1 < P2) { + // case 5 + return Math.abs(P1 - P2) + } + + // case 6 + return false } export class ResizingAnchor extends React.PureComponent { - @observable startingPosition?: ResizeStartEvent - @observable lastEvent?: MouseEvent + @observable lastMouseEvent?: MouseEvent + @observable elem: HTMLElement - static defaultProps = defaultProps + static defaultProps = { + onStart: noop, + onDrag: noop, + onEnd: noop, + onMaxExtentExceed: noop, + onMinExtentExceed: noop, + onMinExtentSubceed: noop, + onMaxExtentSubceed: noop, + onDoubleClick: noop, + disabled: false, + growthDirection: ResizeGrowthDirection.BOTTOM_TO_TOP, + maxExtent: Number.POSITIVE_INFINITY, + minExtent: 0, + placement: ResizeSide.LEADING, + } static IS_RESIZING = "resizing" constructor(props: Props) { super(props) + if (props.maxExtent < props.minExtent) { + throw new Error("maxExtent must be >= minExtent") + } + } + + componentDidMount() { + this.elem = findDOMNode(this) as HTMLElement } componentWillUnmount() { @@ -86,10 +189,10 @@ export class ResizingAnchor extends React.PureComponent { } @action - onDragInit = ({ pageX, pageY, buttons }: React.MouseEvent) => { - const { onStart } = this.props + onDragInit = (event: React.MouseEvent) => { + const { onStart, onlyButtons } = this.props - if (buttons !== 1) { + if (typeof onlyButtons === "number" && onlyButtons !== event.buttons) { return } @@ -97,32 +200,74 @@ export class ResizingAnchor extends React.PureComponent { document.addEventListener("mouseup", this.onDragEnd) document.body.classList.add(ResizingAnchor.IS_RESIZING) - this.startingPosition = { - initX: pageX, - initY: pageY - } - this.lastEvent = undefined + this.lastMouseEvent = undefined + onStart() + } - onStart(this.startingPosition) + calculateDelta(from: Position, to: Position): number | false { + const boundingBox = this.elem.getBoundingClientRect() + + if (this.props.direction === ResizeDirection.HORIZONTAL) { + const barX = Math.round(boundingBox.x + (boundingBox.width / 2)) + return directionDelta(from.pageX, to.pageX, barX) + } else { // direction === ResizeDirection.VERTICAL + const barY = Math.round(boundingBox.y + (boundingBox.height / 2)) + return directionDelta(from.pageY, to.pageY, barY) + } } onDrag = _.throttle((event: MouseEvent) => { - const { onDrag } = this.props - const { initX, initY } = this.startingPosition - const { pageX, pageY } = event - const { movementX, movementY } = calculateMovement(this.lastEvent ?? event, event) + /** + * Some notes to help understand the following: + * - A browser's origin point is in the top left of the screen + * - X increases going from left to right + * - Y increases going from top to bottom + * - Since the resize bar should always be a rectangle, use its centre + * line (in the resizing direction) as the line for determining if + * the bar has "jumped around" + * + * Desire: + * - Always ignore movement in the non-resizing direction + * - Figure out how much the user has "dragged" the resize bar + * - If the resize bar has jumped around, compensate by ignoring movement + * in the resizing direction if it is moving "towards" the resize bar's + * new location. + */ - onDrag({ movementX, movementY, pageX, pageY, initY, initX }) - this.lastEvent = event + if (!this.lastMouseEvent) { + this.lastMouseEvent = event + return + } + + const { maxExtent, minExtent, getCurrentExtent, growthDirection } = this.props + const { onDrag, onMaxExtentExceed, onMinExtentSubceed, onMaxExtentSubceed, onMinExtentExceed } = this.props + const delta = this.calculateDelta(this.lastMouseEvent, event) + if (delta === false) { + return + } + + const previousExtent = getCurrentExtent() + const unboundedExtent = previousExtent + (delta * growthDirection) + const boundedExtent = Math.round(Math.max(minExtent, Math.min(maxExtent, unboundedExtent))) + onDrag(boundedExtent) + + if (previousExtent <= minExtent && minExtent <= unboundedExtent) { + onMinExtentExceed() + } else if (previousExtent >= minExtent && minExtent >= unboundedExtent) { + onMinExtentSubceed() + } + if (previousExtent <= maxExtent && maxExtent <= unboundedExtent) { + onMaxExtentExceed() + } else if (previousExtent >= maxExtent && maxExtent >= unboundedExtent) { + onMaxExtentSubceed() + } + + this.lastMouseEvent = event }, 100) @action onDragEnd = (event: MouseEvent) => { - const { onEnd } = this.props - const { initX, initY } = this.startingPosition - const { pageX, pageY } = event - - onEnd(({ initX, initY, pageX, pageY })) + this.props.onEnd() document.removeEventListener("mousemove", this.onDrag) document.removeEventListener("mouseup", this.onDragEnd) document.body.classList.remove(ResizingAnchor.IS_RESIZING)