1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Make the ResizeAnchor much more explicit, powerful, and customizable

- Add min/max exceed/subceed events
- Add customizing desired growth direction
- Simplfy onDrag signature

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-09-30 16:55:07 -04:00
parent a25540b9ad
commit 4bb46a797f
4 changed files with 230 additions and 101 deletions

View File

@ -27,12 +27,8 @@ export class DockStore {
]; ];
protected storage = createStorage("dock", {}); // keep settings in localStorage protected storage = createStorage("dock", {}); // keep settings in localStorage
public defaultTabId = this.initialTabs[0].id; public readonly defaultTabId = this.initialTabs[0].id;
private _minHeight = 100; public readonly minHeight = 100;
set minHeight(val: number) {
this._minHeight = val
}
@observable isOpen = false; @observable isOpen = false;
@observable fullSize = false; @observable fullSize = false;
@ -45,19 +41,24 @@ export class DockStore {
} }
get defaultHeight() { get defaultHeight() {
return Math.round(window.innerHeight / 2.5); return Math.round(window.innerHeight * 0.4);
} }
get maxHeight() { get maxHeight() {
const mainLayoutHeader = 40; const mainLayoutHeader = 40
const mainLayoutTabs = 33; const mainLayoutTabs = 33
const mainLayoutMargin = 16; const mainLayoutMargin = 16
const dockTabs = 33; const dockTabs = 33
return window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs; const preferedMax = window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs
return Math.max(preferedMax, this.minHeight) // don't let max < min
} }
constructor() { 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(() => ({ reaction(() => ({
isOpen: this.isOpen, isOpen: this.isOpen,
@ -69,7 +70,6 @@ export class DockStore {
}); });
// adjust terminal height if window size changes // adjust terminal height if window size changes
this.checkMaxHeight();
window.addEventListener("resize", throttle(this.checkMaxHeight, 250)); window.addEventListener("resize", throttle(this.checkMaxHeight, 250));
} }
@ -175,20 +175,19 @@ export class DockStore {
@action @action
selectTab(tabId: TabId) { selectTab(tabId: TabId) {
const tab = this.getTabById(tabId); this.selectedTabId = this.getTabById(tabId)?.id ?? null;
this.selectedTabId = tab ? tab.id : null;
} }
@action @action
setHeight(height?: number) { 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 @action
reset() { reset() {
this.selectedTabId = this.defaultTabId; this.selectedTabId = this.defaultTabId;
this.tabs.replace(this.initialTabs); this.tabs.replace(this.initialTabs);
this.height = this.defaultHeight; this.setHeight(this.defaultHeight);
this.close(); this.close();
} }
} }

View File

@ -4,7 +4,7 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { autobind, cssNames, prevDefault } from "../../utils"; import { autobind, cssNames, prevDefault } from "../../utils";
import { ResizingAnchor, ResizeDragEvent, ResizeDirection } from "../resizing-anchor"; import { ResizingAnchor, ResizeDirection } from "../resizing-anchor";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Tabs } from "../tabs/tabs"; import { Tabs } from "../tabs/tabs";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
@ -29,27 +29,6 @@ interface Props {
@observer @observer
export class Dock extends React.Component<Props> { export class Dock extends React.Component<Props> {
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<HTMLElement>) => { onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => {
const { close, closeTab, selectedTab } = dockStore; const { close, closeTab, selectedTab } = dockStore;
if (!selectedTab) return; if (!selectedTab) return;
@ -107,9 +86,14 @@ export class Dock extends React.Component<Props> {
> >
<ResizingAnchor <ResizingAnchor
disabled={!hasTabs()} disabled={!hasTabs()}
getCurrentExtent={() => dockStore.height}
minExtent={dockStore.minHeight}
maxExtent={dockStore.maxHeight}
direction={ResizeDirection.VERTICAL} direction={ResizeDirection.VERTICAL}
onStart={this.onResizeStart} onStart={dockStore.open}
onDrag={this.onResize} onMinExtentSubceed={dockStore.close}
onMinExtentExceed={dockStore.open}
onDrag={dockStore.setHeight}
/> />
<div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}> <div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}>
<Tabs <Tabs

View File

@ -121,6 +121,8 @@ export class Terminal {
} }
fit = () => { fit = () => {
// Since this function is debounced we need to read this value as late as possible
if (!this.isActive) return;
this.fitAddon.fit(); this.fitAddon.fit();
const { cols, rows } = this.xterm; const { cols, rows } = this.xterm;
this.api.sendTerminalSize(cols, rows); this.api.sendTerminalSize(cols, rows);
@ -150,7 +152,6 @@ export class Terminal {
} }
onResize = () => { onResize = () => {
if (!this.isActive) return;
this.fitLazy(); this.fitLazy();
this.focus(); this.focus();
} }

View File

@ -1,8 +1,9 @@
import "./resizing-anchor.scss"; import "./resizing-anchor.scss";
import React from "react"; import React from "react";
import { cssNames, noop } from "../../utils"; import { action, observable } from "mobx";
import { action, computed, observable } from "mobx";
import _ from "lodash" import _ from "lodash"
import { findDOMNode } from "react-dom";
import { cssNames, noop } from "../../utils";
export enum ResizeDirection { export enum ResizeDirection {
HORIZONTAL = "horizontal", HORIZONTAL = "horizontal",
@ -26,58 +27,160 @@ export enum ResizeSide {
TRAILING = "trailing", 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 { interface Props {
direction: ResizeDirection; 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; disabled?: boolean;
placement?: ResizeSide; placement?: ResizeSide;
onStart?: (data: ResizeStartEvent) => void; growthDirection?: ResizeGrowthDirection;
onDrag?: (data: ResizeDragEvent) => void;
onEnd?: (data: ResizeEndEvent) => void; // 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 { enum MovementTrigger {
initX: number; EXCEED_MAX = "exceed",
initY: number; SUBCEED_MIN = "subceed",
} }
interface CurrentPosition { interface MovementCalc {
pageX: number; newExtent?: number;
pageY: number; otherTrigger?: MovementTrigger;
ignore?: boolean;
} }
interface MovementData { interface Position {
movementX: number; readonly pageX: number;
movementY: number; readonly pageY: number;
} }
export type ResizeStartEvent = InitialPosition /**
export type ResizeDragEvent = InitialPosition & CurrentPosition & MovementData * Return the direction delta, but ignore drags leading up to a moved item
export type ResizeEndEvent = InitialPosition & CurrentPosition * 1. `->|` => return `false`
* 2. `<-|` => return `directed length (P1, P2)` (negative)
function calculateMovement(from: CurrentPosition, to: CurrentPosition): MovementData { * 3. `-|>` => return `directed length (M, P2)` (positive)
return { * 4. `<|-` => return `directed length (M, P2)` (negative)
movementX: from.pageX - to.pageX, * 5. `|->` => return `directed length (P1, P2)` (positive)
movementY: from.pageY - to.pageY, * 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)
} }
const defaultProps: Partial<Props> = { if (P2 < P1) {
onStart: noop, // case 2
onDrag: noop, return -Math.abs(P1 - P2)
onEnd: noop, }
disabled: false,
placement: ResizeSide.LEADING, // case 1
return false
}
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<Props> { export class ResizingAnchor extends React.PureComponent<Props> {
@observable startingPosition?: ResizeStartEvent @observable lastMouseEvent?: MouseEvent
@observable lastEvent?: 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" static IS_RESIZING = "resizing"
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
if (props.maxExtent < props.minExtent) {
throw new Error("maxExtent must be >= minExtent")
}
}
componentDidMount() {
this.elem = findDOMNode(this) as HTMLElement
} }
componentWillUnmount() { componentWillUnmount() {
@ -86,10 +189,10 @@ export class ResizingAnchor extends React.PureComponent<Props> {
} }
@action @action
onDragInit = ({ pageX, pageY, buttons }: React.MouseEvent<any>) => { onDragInit = (event: React.MouseEvent) => {
const { onStart } = this.props const { onStart, onlyButtons } = this.props
if (buttons !== 1) { if (typeof onlyButtons === "number" && onlyButtons !== event.buttons) {
return return
} }
@ -97,32 +200,74 @@ export class ResizingAnchor extends React.PureComponent<Props> {
document.addEventListener("mouseup", this.onDragEnd) document.addEventListener("mouseup", this.onDragEnd)
document.body.classList.add(ResizingAnchor.IS_RESIZING) document.body.classList.add(ResizingAnchor.IS_RESIZING)
this.startingPosition = { this.lastMouseEvent = undefined
initX: pageX, onStart()
initY: pageY
} }
this.lastEvent = undefined
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) => { onDrag = _.throttle((event: MouseEvent) => {
const { onDrag } = this.props /**
const { initX, initY } = this.startingPosition * Some notes to help understand the following:
const { pageX, pageY } = event * - A browser's origin point is in the top left of the screen
const { movementX, movementY } = calculateMovement(this.lastEvent ?? event, event) * - 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 }) if (!this.lastMouseEvent) {
this.lastEvent = event 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) }, 100)
@action @action
onDragEnd = (event: MouseEvent) => { onDragEnd = (event: MouseEvent) => {
const { onEnd } = this.props this.props.onEnd()
const { initX, initY } = this.startingPosition
const { pageX, pageY } = event
onEnd(({ initX, initY, pageX, pageY }))
document.removeEventListener("mousemove", this.onDrag) document.removeEventListener("mousemove", this.onDrag)
document.removeEventListener("mouseup", this.onDragEnd) document.removeEventListener("mouseup", this.onDragEnd)
document.body.classList.remove(ResizingAnchor.IS_RESIZING) document.body.classList.remove(ResizingAnchor.IS_RESIZING)