chore: update demo

This commit is contained in:
mathuo 2021-06-03 21:27:28 +01:00
parent f40f96f840
commit c01290811e
5 changed files with 759 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -0,0 +1,74 @@
.markdown-line {
padding-left: 20px;
}
.indent-1 {
padding-left: 40px;
}
.markdown-highlight {
background-color: rgba(30, 144, 255, 0.1);
}
.sash {
background-color: orange;
position: absolute;
top: 0px;
width: 4px;
height: 100%;
z-index: 2;
cursor: ew-resize;
user-select: none;
}
.debug-sash-max {
height: 10px;
width: 1px;
position: absolute;
z-index: 999;
top: -10px;
}
.debug-sash-min {
height: 10px;
width: 1px;
position: absolute;
z-index: 999;
top: 100%;
}
.debug-sash-text {
height: 20px;
line-height: 20px;
width: 80px;
display: flex;
justify-content: center;
position: absolute;
z-index: 999;
font-size: 14px;
}
.sash-container {
position: absolute;
height: 100%;
}
.view-container {
position: relative;
height: 100%;
}
.view {
position: absolute;
height: 100%;
background-color: dodgerblue;
z-index: 1;
top: 0px;
padding: 10px;
color: white;
box-sizing: border-box;
}
.sash.drag-sash {
background-color: red;
}

View File

@ -2,6 +2,348 @@ import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Library/Splitview/Documentation" />
# Splitview documentation
import { Splitview } from './splitview';
import VisualOne from './visual_1.jpg';
import Contraints from './constraints.jpg';
<div className="subheading">Components</div>
# Splitview
The splitview component is one of the libraries key components.
The paneview is an extension of thesplitview, the gridview is a collection of nested splitviews and the dockview adds tabular panels to the gridview.
You can view several interactive examples of the Splitview component in action here, as well as read a description as to how this key component actually works.
# The math behind the split view layouting
The below is a language independant walkthrough of the math used within to layout these panels. This is the end result and below will explain the steps taken to reach this point.
The origins of this logic stem from the VSCode source code found [here](https://github.com/microsoft/vscode/tree/main/src/vs/base/browser/ui/splitview).
<Splitview mode={3} debug={false} />
## What is a Splitview?
> A Splitview control is a collection of _views_ stacked either _horizontally_ or _vertically_ where each view can be indepedantly resized by dragging on the edge of a particular view.
To explain how this works will require some better definitions so lets start with the two fundamental components of this control, the <b>View</b> and the <b>Sash</b>.
Assume the Splitview control has <b>n</b> views where n is positive number (i.e if n=4 then our split view controls has 4 views). A single view can then be defined as
<div>
<div>View</div>
<ul style={{ marginLeft: '40px' }}>
<li>
The size of the n<sup>th</sup> view will be known as V<sub>n</sub>
</li>
<li>
The minimum size of the n<sup>th</sup> view will be known as V
<sup>min</sup>
<sub>n</sub>
</li>
<li>
The maximum size of the n<sup>th</sup> view will be known as V
<sup>max</sup>
<sub>n</sub>
</li>
</ul>
</div>
Additionally by definition we can known V<sup>min</sup><sub>n</sub> <= V<sub>n</sub> <= V<sup>max</sup><sub>n</sub>
To be able to resize a view you need to be able to drag on the edge of a view to increase or decrease it's size.
This can be achieved by introducing a narrow component that sits between each view acting as a _drag handle_.
Lets call this component a <b>Sash</b> (see [link](https://en.wikipedia.org/wiki/Sash_window)) and we can define this as
<div>
<div>Sash</div>
<ul style={{marginLeft: "40px"}}>
<li>If we have n views then we will have n-1 sashes. There is no sash before V<sub>0</sub> nor after V<sub>n</sub></li>
<li>The sash between V<sub>n</sub> and V<sub>n+1</sub> is known as S<sub>n</sub></li>
<li>The sash is of fixed width, and it's sole purpose is to act a drag-handle for resizing views</li>
</ul>
</div>
To calculate the new view sizes after a sash is dragged we need to know which sash is being dragged.
Lets denote the sash S<sub>i</sub> as the sash to drag, which will give us a set of definitions to work with inline with the below diagram.
<img src={VisualOne} />
If we are to drag the sash S<sub>i</sub> then it's also needed to know how far along the x-axis, or the y-axis (in the case of vertically stacked views) you have travelled.
Lets denote this as the delta, using the symbol Δ. Delta is only limited by the width (or height) of the control so in it's most general form we say it ranges from negative to positive infinity, that is -∞ < Δ < ∞ .
In reality as you will see we will apply a set of constraints on the value of Δ reducing it's overall set of valid values.
This defines everything we need to describe the definition of a Splitview. The first approach to show will be the most native building the complexity after each iteration.
## Iteration #1 - The naive approach (aka. the accordian)
The most basic form of resizing may be to say as I add delta increase view sizes and as I remove delta I decrease view sizes. This could be further described with the following statements:
- As the sash moves left shrink each view to the left and as the sash moves right expand each view to left, from right-most to left-most in both cases.
- If there is enough delta to shrink a view to it's mimimum size then progress onto the next view, and if we have enough delta to expand a view to it's maximum size then again progress onto the next view.
- Shrink no more once everything to the left is at minimums and expand no more once everything to the left is at maximums
- We don't manipulate any views to the right of the active sash S<sub>i</sub>
You should be able to show each of the four points above hold true for the below interactive example.
You'll see that changes to the right will always remain at zero because we are not manipulating views to the right of the active sash.
<Splitview mode={1} debug={true} />
Putting this implemenation in psuedocode using the definitions from above where we drag sash S<sub>i</sub> by an amount Δ
<div
style={{
display: 'inline-block',
marginLeft: '20px',
marginBottom: '20px',
borderLeft: '2px solid black',
}}
>
<div className="markdown-line">
Δ<sub>remaining</sub> = Δ
</div>
<div className="markdown-line">
<span style={{ fontWeight: 'bold' }}>for</span>
<span>
(<span style={{ fontStyle: 'italic' }}>j = i; j >= 0; i--</span>)
</span>
<span style={{ fontWeight: 'bold' }}> do</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>next</sup>
<sub>j</sub> = <span style={{ fontWeight: 'bold' }}>Min</span>(V<sup>
max
</sup>
<sub>j</sub>, <span style={{ fontWeight: 'bold' }}>Max</span>(V
<sup>min</sup>
<sub>j</sub>, V<sub>j</sub> + Δ<sub>remaining</sub>))
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>Δ</sup>
<sub>j</sub> = V<sup>next</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
Δ<sub>remaining</sub> = Δ<sub>remaining</sub> - V<sup>Δ</sup>
<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup>
<sub>j</sub>
</span>
</div>
</div>
and as instructions
<div
style={{
borderLeft: '2px solid black',
margin: '0px 20px 20px 20px',
display: 'flex',
flexDirection: 'row',
}}
>
<div
style={{ borderRight: '2px solid black', flexShrink: 0, width: '20px' }}
>
<div style={{ display: 'flex', justifyContent: 'center' }}>1</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>2</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>3</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>4</div>
</div>
<div style={{ flexGrow: 1, marginLeft: '20px' }}>
<div>
For each view j to the left of the sash we have dragged, from
right-most to left-most
</div>
<div>
Add the delta to the view j (clamped at either the maximum or
minimum value)
</div>
<div>
Subtract the different between the new and old size (the used delta)
from the remaining delta
</div>
<div>repeat</div>
</div>
</div>
There are some obvious flaws with this approach. Nothing to the right of the active sash is resizes which also related to the fact that the width of the control does not remain constant.
## Iteration #2 - When Δ is added an equal Δ must be removed
For the width of the control to remain constant it would make sense that if I add Δ to the left then I should add -Δ (or remove Δ) on the right, and vice-versa, which is the approach of the below interactive example.
As you may see it is right, but there are still some edge cases that fail.
<Splitview mode={2} debug={true} />
To write this approach in pseudocode lets define another variable to track the delta we've added on the left, Δ<sub>used</sub>.
After we've applied changes to the left side we'll substract this Δ<sub>used</sub> from the right side with the aim to keep the width of the control constant.
<div style={{display: "inline-block", marginLeft: "20px", marginBottom: "20px", borderLeft: "2px solid black"}}>
<div className="markdown-line">
Δ<sub>remaining</sub> = Δ
</div>
<div className="markdown-line markdown-highlight">
Δ<sub>used</sub> = 0
</div>
<div className="markdown-line">
<span style={{fontWeight: "bold"}}>for</span><span>(<span style={{fontStyle: "italic"}}>j = i; j >= 0; i--</span>)</span>
<span style={{fontWeight: "bold"}}> do</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>next</sup><sub>j</sub> = <span style={{fontWeight: "bold"}}>Min</span>(V<sup>max</sup><sub>j</sub>, <span style={{fontWeight: "bold"}}>Max</span>(V<sup>min</sup><sub>j</sub>, V<sub>j</sub> + Δ<sub>remaining</sub>))
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sup>Δ</sup><sub>j</sub> = V<sup>next</sup><sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
Δ<sub>remaining</sub> = Δ<sub>remaining</sub> - V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1 markdown-highlight">
<span>
Δ<sub>used</sub> = Δ<sub>used</sub> + V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div className="markdown-line indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup><sub>j</sub>
</span>
</div>
<div style={{height: "0px", width: "100%", marginBottom: "20px"}}/>
<div className="markdown-line markdown-highlight">
<span style={{fontWeight: "bold"}}>for</span><span>(<span style={{fontStyle: "italic"}}>{"j = i+1; j < n; i++"}</span>)</span>
<span style={{fontWeight: "bold"}}> do</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
V<sup>next</sup><sub>j</sub> = <span style={{fontWeight: "bold"}}>Min</span>(V<sup>max</sup><sub>j</sub>, <span style={{fontWeight: "bold"}}>Max</span>(V<sup>min</sup><sub>j</sub>,V<sub>j</sub> - Δ<sub>used</sub>))
</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
V<sup>Δ</sup><sub>j</sub> = V<sup>next</sup><sub>j</sub> - V<sub>j</sub>
</span>
</div>
<div className="markdown-line markdown-highlight indent-1">
<span>
Δ<sub>used</sub> = Δ<sub>used</sub> + V<sup>Δ</sup><sub>j</sub>
</span>
</div>
<div style={{marginBottom: "20px"}} className="markdown-line markdown-highlight indent-1">
<span>
V<sub>j</sub> = V<sup>next</sup><sub>j</sub>
</span>
</div>
</div>
Go back and try to minimise or maximise every view in the container. The width is no longer preserved, you can see at some point the change to the left
is not longer eqaul to the change on the right, which causes the container to once again flex.
## Iteration #3 - Constraining the values of Δ
The failure of iteration #2 can be explained as the addition or removal of too much delta which means we need to look at what constraints we can apply to the problem.
For a sash S<sub>i</sub> think about the minimum and maximum amount of delta that can be both added and removed.
Minimized view constraints
- S<sub>i</sub> can go no further left that the sum of the minimum sizes of the views to the left because you would then have a view smaller than it's minimum size
- S<sub>i</sub> can go no further further right than the sum of the minimum sizes of the views to the right because you would then have a view smaller than it's minimum size
Maximised view constraints
- S<sub>i</sub> can go no further left that the sum of the maximum sizes of the views to the right because otherwise you would have a viewer larger than it's maximum size
- S<sub>i</sub> can go no further right that the sum of the maximum sizes of the views to the left because otherwise you would have a viewer larger than it's maximum size
Since Δ is relative to S<sub>i</sub> we need these to define these constraints relative to S<sub>i</sub>.
When the views to the left of S<sub>i</sub> are all at minimum size define the distance between here and Δ to be Δ<sup>min</sup><sub>left</sub>.
This distance would be the sum of the differences between V<sup>min</sup><sub>j</sub> and V<sub>j</sub> for each view:
<div style={{ margin: '20px' }}>
<span>
Δ<sup>min</sup>
<sub>left</sub> = Σ V<sup>min</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i,...0</span>
</div>
Similarly we can work out the distance between S<sub>i</sub> and the point at each every view to the left is at its
maximum size as the sum of differences between V<sup>max</sup><sub>j</sub> an V<sub>j</sub>
<div style={{ margin: '20px' }}>
<span>
Δ<sup>max</sup>
<sub>left</sub> = Σ V<sup>max</sup>
<sub>j</sub> - V<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i,...0</span>
</div>
The same logic can be applied to work out those values for Δ<sup>min</sup><sub>right</sub> and Δ<sup>max</sup><sub>right</sub>
<div style={{ margin: '20px' }}>
<div style={{ marginBottom: '10px' }}>
<span>
Δ<sup>min</sup>
<sub>right</sub> = Σ V<sub>j</sub> - V<sup>min</sup>
<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i+1...n</span>
</div>
<div>
<span>
Δ<sup>max</sup>
<sub>right</sub> = Σ V<sub>j</sub> - V<sup>min</sup>
<sub>j</sub>
</span>
<span style={{ marginLeft: '20px' }}>j = i+1...n</span>
</div>
</div>
This leaves us with two minimum constraints which are V<sup>min</sup><sub>left</sub> and V<sup>max</sup><sub>right</sub> and two maximum
constraints V<sup>max</sup><sub>left</sub> and V<sup>min</sup><sub>right</sub>.
We can reduce these down to a single minimum and maximum contraint by taking the maximum of the two minimums and the minimum of the two maximums leaving us with the following constraints:
<div style={{ margin: '20px' }}>
<div style={{ marginBottom: '10px' }}>
Δ<sub>min</sub> = Max ( V<sup>min</sup>
<sub>left</sub> , V<sup>max</sup>
<sub>right</sub> )
</div>
<div>
Δ<sub>max</sub> = Min ( V<sup>max</sup>
<sub>left</sub> , V<sup>min</sup>
<sub>right</sub> )
</div>
</div>
Given these constraints we can clamp the value of Δ to be within this minimum and maxium boundary.
This clamped delta can be used in place of delta in the pseudocode from Iteration #2.
<div style={{ margin: '20px' }}>
Δ<sub>clamped</sub> = MIN ( V<sub>max</sub> , MAX ( V<sub>min</sub> , Δ ) )
</div>
You can see how this works in this interactive example which also visually indicates this boundary conditions.
<Splitview mode={3} debug={true} />
For a more visual explaination it's worth studying the below diagram:
<img src={Contraints} />

View File

@ -0,0 +1,341 @@
import * as React from 'react';
import './splitview.css';
const min = 100;
const max = 300;
interface IDebugResize {
leftmin: number;
leftmax: number;
rightmin: number;
rightmax: number;
min: number;
max: number;
}
const resize = (
index: number,
delta: number,
sizes: number[],
mode: number
) => {
const nextSizes = [...sizes];
const left = nextSizes.filter((_, i) => i <= index);
const right = nextSizes.filter((_, i) => i > index);
let result: IDebugResize = {
leftmin: undefined,
leftmax: undefined,
rightmin: undefined,
rightmax: undefined,
max: undefined,
min: undefined,
};
// step 3
if (mode > 2) {
const leftMinimumsDelta = left
.map((x) => min - x)
.reduce((x, y) => x + y, 0);
const leftMaximumsDelta = left
.map((x) => max - x)
.reduce((x, y) => x + y, 0);
const rightMinimumsDelta = right
.map((x) => x - min)
.reduce((x, y) => x + y, 0);
const rightMaximumsDelta = right
.map((x) => x - max)
.reduce((x, y) => x + y, 0);
const _min = Math.max(leftMinimumsDelta, rightMaximumsDelta);
const _max = Math.min(leftMaximumsDelta, rightMinimumsDelta);
const clamp = Math.max(_min, Math.min(_max, delta));
result = {
leftmin: leftMinimumsDelta,
leftmax: leftMaximumsDelta,
rightmin: rightMinimumsDelta,
rightmax: rightMaximumsDelta,
max: _max,
min: _min,
};
delta = clamp;
}
let usedDelta = 0;
let remainingDelta = delta;
// Step 1
for (let i = left.length - 1; i > -1; i--) {
const x = Math.max(min, Math.min(max, left[i] + remainingDelta));
const viewDelta = x - left[i];
usedDelta += viewDelta;
remainingDelta -= viewDelta;
left[i] = x;
}
// Step 2
if (mode > 1) {
for (let i = 0; i < right.length; i++) {
const x = Math.max(min, Math.min(max, right[i] - usedDelta));
const viewDelta = x - right[i];
usedDelta += viewDelta;
right[i] = x;
}
}
return { ...result, sizes: [...left, ...right] };
};
interface ILayoutState {
sashes: number[];
views: number[];
deltas: number[];
left: number;
right: number;
debug: IDebugResize;
drag: number;
}
export const Splitview = (props: { mode: number; debug: boolean }) => {
// keep the sashes and views in one state to prevent weird out-of-sync-ness
const [layout, setLayout] = React.useState<ILayoutState>({
sashes: [200, 400, 600],
views: [200, 200, 200, 200],
deltas: [0, 0, 0, 0],
left: 0,
right: 0,
debug: undefined,
drag: -1,
});
const ref = React.useRef<HTMLDivElement>();
const onMouseDown = (index: number) => (ev: React.MouseEvent) => {
const start = ev.clientX;
const sizes = [...layout.views];
const mousemove = (ev: MouseEvent) => {
const current = ev.clientX;
const delta = current - start;
const {
sizes: nextLayout,
rightmin,
rightmax,
leftmin,
leftmax,
max,
min,
} = resize(index, delta, sizes, props.mode);
const sashes = nextLayout.reduce(
(x, y) => [...x, y + (x.length === 0 ? 0 : x[x.length - 1])],
[]
);
sashes.splice(sashes.length - 1, 1);
const deltas = sizes.map((x, i) => nextLayout[i] - x);
const offset = start - ref.current?.getBoundingClientRect().left;
setLayout({
views: nextLayout,
sashes,
deltas,
left: deltas
.filter((_, i) => i <= index)
.reduce((x, y) => x + y, 0),
right: deltas
.filter((_, i) => i > index)
.reduce((x, y) => x + y, 0),
debug: {
leftmax: leftmax + offset,
leftmin: leftmin + offset,
rightmax: rightmax + offset,
rightmin: rightmin + offset,
min: min + offset,
max: max + offset,
},
drag: index,
});
};
const end = (ev: MouseEvent) => {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', end);
setLayout((_) => ({
..._,
deltas: _.deltas.map((_) => 0),
left: 0,
right: 0,
drag: -1,
}));
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', end);
};
const extras = React.useMemo(() => {
if (!props.debug || !layout.debug || props.mode < 3) {
return null;
}
return (
<>
<div
style={{
left: `${layout.debug.leftmax - 40}px`,
top: '-30px',
}}
className="debug-sash-text"
>
left-max
</div>
<div
style={{
left: `${layout.debug.leftmin - 40}px`,
top: '-30px',
}}
className="debug-sash-text"
>
left-min
</div>
<div
style={{
left: `${layout.debug.rightmax - 40}px`,
bottom: '-30px',
}}
className="debug-sash-text"
>
right-max
</div>
<div
style={{
left: `${layout.debug.rightmin - 40}px`,
bottom: '-30px',
}}
className="debug-sash-text"
>
right-min
</div>
<div
className="debug-sash-max"
style={{
left: `${layout.debug.leftmax - 1}px`,
border: '2px solid purple',
}}
/>
<div
className="debug-sash-max"
style={{
left: `${layout.debug.leftmin - 1}px`,
border: '2px solid green',
}}
/>
<div
className="debug-sash-min"
style={{
left: `${layout.debug.rightmax - 1}px`,
border: '2px solid cyan',
}}
/>
<div
className="debug-sash-min"
style={{
left: `${layout.debug.rightmin - 1}px`,
border: '2px solid pink',
}}
/>
</>
);
}, [layout.debug]);
return (
<div
style={{
marginBottom: '40px',
marginTop: '30px',
backgroundColor: 'gray',
}}
>
{props.debug && (
<div style={{ marginBottom: extras ? '25px' : '0px' }}>
<span>{`Change to left ${layout?.left}`}</span>
<span
style={{
marginLeft: '10px',
backgroundColor:
-layout?.right !== layout?.left ? 'red' : '',
}}
>{`Change to right ${layout?.right}`}</span>
<span
style={{ marginLeft: '10px' }}
>{`Total size ${layout?.views.reduce(
(x, y) => x + y,
0
)}`}</span>
</div>
)}
<div
ref={ref}
style={{
height: '100px',
width: '100%',
position: 'relative',
backgroundColor: 'dimgray',
}}
>
<div className="sash-container">
{layout.sashes.map((x, i) => {
const className =
layout.drag === i ? 'sash drag-sash' : 'sash';
return (
<div
key={i}
onMouseDown={onMouseDown(i)}
style={{
left: `${x - 2}px`,
}}
className={className}
></div>
);
})}
{extras}
</div>
<div className="view-container">
{layout.views.map((x, i) => {
const isMax = x >= max;
const isMin = x <= min;
return (
<div
key={i}
style={{
left: `${
i === 0 ? 0 : layout.sashes[i - 1]
}px`,
width: `${x}px`,
}}
className="view"
>
{props.debug && (
<>
<div>
{`${layout.views[i]} (${
layout.deltas[i] > -1 ? '+' : ''
}${layout.deltas[i]})`}
</div>
<div
style={{ fontSize: '12px' }}
>{`isMin = ${isMin}`}</div>
<div
style={{ fontSize: '12px' }}
>{`isMax = ${isMax}`}</div>
</>
)}
</div>
);
})}
</div>
</div>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB