@witchcraft/layout 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -24
- package/dist/module.json +1 -1
- package/dist/runtime/components/FrameDragHandle.d.vue.ts +15 -0
- package/dist/runtime/components/FrameDragHandle.vue +28 -0
- package/dist/runtime/components/FrameDragHandle.vue.d.ts +15 -0
- package/dist/runtime/components/LayoutDecos.d.vue.ts +2 -4
- package/dist/runtime/components/LayoutDecos.vue +10 -29
- package/dist/runtime/components/LayoutDecos.vue.d.ts +2 -4
- package/dist/runtime/components/LayoutEdges.d.vue.ts +3 -3
- package/dist/runtime/components/LayoutEdges.vue +8 -8
- package/dist/runtime/components/LayoutEdges.vue.d.ts +3 -3
- package/dist/runtime/components/LayoutFrame.d.vue.ts +1 -1
- package/dist/runtime/components/LayoutFrame.vue +0 -1
- package/dist/runtime/components/LayoutFrame.vue.d.ts +1 -1
- package/dist/runtime/components/LayoutShapeSquare.d.vue.ts +3 -1
- package/dist/runtime/components/LayoutShapeSquare.vue.d.ts +3 -1
- package/dist/runtime/components/LayoutWindow.d.vue.ts +26 -12
- package/dist/runtime/components/LayoutWindow.vue +95 -84
- package/dist/runtime/components/LayoutWindow.vue.d.ts +26 -12
- package/dist/runtime/composables/useFrames.d.ts +15 -13
- package/dist/runtime/composables/useFrames.js +59 -39
- package/dist/runtime/demo/App.vue +116 -30
- package/dist/runtime/demo/DemoControls.d.vue.ts +4 -1
- package/dist/runtime/demo/DemoControls.vue +98 -4
- package/dist/runtime/demo/DemoControls.vue.d.ts +4 -1
- package/dist/runtime/drag/CloseAction.d.ts +26 -5
- package/dist/runtime/drag/CloseAction.js +87 -40
- package/dist/runtime/drag/DragActionHandler.d.ts +20 -8
- package/dist/runtime/drag/DragActionHandler.js +47 -12
- package/dist/runtime/drag/FrameDragAction.d.ts +45 -0
- package/dist/runtime/drag/FrameDragAction.js +143 -0
- package/dist/runtime/drag/SplitAction.d.ts +32 -11
- package/dist/runtime/drag/SplitAction.js +82 -24
- package/dist/runtime/drag/createDefaultHandlers.d.ts +9 -0
- package/dist/runtime/drag/createDefaultHandlers.js +10 -0
- package/dist/runtime/drag/defaultDragActions.d.ts +9 -0
- package/dist/runtime/drag/defaultDragActions.js +10 -0
- package/dist/runtime/drag/types.d.ts +82 -13
- package/dist/runtime/drag/types.js +1 -0
- package/dist/runtime/helpers/createZoneSideClipPath.d.ts +12 -0
- package/dist/runtime/helpers/createZoneSideClipPath.js +17 -0
- package/dist/runtime/helpers/doEdgesOverlap.d.ts +3 -1
- package/dist/runtime/helpers/doEdgesOverlap.js +5 -5
- package/dist/runtime/helpers/getDockBoundaries.d.ts +19 -0
- package/dist/runtime/helpers/getDockBoundaries.js +14 -0
- package/dist/runtime/helpers/getEdgeLength.d.ts +2 -0
- package/dist/runtime/helpers/getEdgeLength.js +5 -0
- package/dist/runtime/helpers/getIntersections.js +2 -2
- package/dist/runtime/helpers/getIntersectionsCss.js +2 -2
- package/dist/runtime/helpers/getMoveEdgeInfo.js +2 -2
- package/dist/runtime/helpers/getResizeLimit.js +2 -2
- package/dist/runtime/helpers/getShapeSquareCss.js +2 -2
- package/dist/runtime/helpers/getVisualEdgeCss.js +2 -2
- package/dist/runtime/helpers/getVisualEdges.d.ts +1 -1
- package/dist/runtime/helpers/getVisualEdges.js +4 -3
- package/dist/runtime/helpers/index.d.ts +4 -0
- package/dist/runtime/helpers/index.js +4 -0
- package/dist/runtime/helpers/isEdgeEqual.js +2 -4
- package/dist/runtime/helpers/isWindowEdge.js +2 -2
- package/dist/runtime/helpers/isWindowEdgePoint.js +2 -2
- package/dist/runtime/helpers/moveEdge.js +2 -2
- package/dist/runtime/helpers/numberToScaledPercent.d.ts +1 -1
- package/dist/runtime/helpers/numberToScaledPercent.js +2 -2
- package/dist/runtime/helpers/numberToScaledSize.js +2 -2
- package/dist/runtime/helpers/rotateFrames.d.ts +7 -0
- package/dist/runtime/helpers/rotateFrames.js +36 -0
- package/dist/runtime/helpers/scaledPointToPx.d.ts +13 -0
- package/dist/runtime/helpers/scaledPointToPx.js +7 -0
- package/dist/runtime/helpers/toWindowCoord.js +2 -2
- package/dist/runtime/layout/applyFrameChanges.d.ts +10 -0
- package/dist/runtime/layout/applyFrameChanges.js +29 -0
- package/dist/runtime/layout/createSplitDecoFromDrag.d.ts +6 -1
- package/dist/runtime/layout/createSplitDecoFromDrag.js +4 -4
- package/dist/runtime/layout/createSplitDecoShapes.d.ts +7 -0
- package/dist/runtime/layout/{createSplitDecoEdge.js → createSplitDecoShapes.js} +6 -3
- package/dist/runtime/layout/debugFrame.js +2 -1
- package/dist/runtime/layout/findSafeSplitEdge.js +2 -2
- package/dist/runtime/layout/frameCreate.js +2 -2
- package/dist/runtime/layout/getCloseFrameInfo.d.ts +7 -6
- package/dist/runtime/layout/getCloseFrameInfo.js +10 -3
- package/dist/runtime/layout/getDragZones.d.ts +8 -0
- package/dist/runtime/layout/getDragZones.js +32 -0
- package/dist/runtime/layout/getFillEmptySpaceInfo.d.ts +65 -0
- package/dist/runtime/layout/getFillEmptySpaceInfo.js +69 -0
- package/dist/runtime/layout/getFrameCollapseInfo.d.ts +13 -0
- package/dist/runtime/layout/getFrameCollapseInfo.js +93 -0
- package/dist/runtime/layout/getFrameDockInfo.d.ts +9 -0
- package/dist/runtime/layout/getFrameDockInfo.js +82 -0
- package/dist/runtime/layout/getFrameDragZones.d.ts +16 -0
- package/dist/runtime/layout/getFrameDragZones.js +74 -0
- package/dist/runtime/layout/getFrameRearrangeInfo.d.ts +139 -0
- package/dist/runtime/layout/getFrameRearrangeInfo.js +87 -0
- package/dist/runtime/layout/getFrameSplitInfo.d.ts +7 -5
- package/dist/runtime/layout/getFrameSplitInfo.js +10 -3
- package/dist/runtime/layout/getFrameSwapInfo.d.ts +9 -0
- package/dist/runtime/layout/getFrameSwapInfo.js +27 -0
- package/dist/runtime/layout/getFrameTo.js +2 -2
- package/dist/runtime/layout/getFrameUncollapseInfo.d.ts +12 -0
- package/dist/runtime/layout/getFrameUncollapseInfo.js +88 -0
- package/dist/runtime/layout/getFrameUndockInfo.d.ts +13 -0
- package/dist/runtime/layout/getFrameUndockInfo.js +51 -0
- package/dist/runtime/layout/getFramesRedistributeInfo.d.ts +29 -0
- package/dist/runtime/layout/getFramesRedistributeInfo.js +53 -0
- package/dist/runtime/layout/getWindowDragZones.d.ts +6 -0
- package/dist/runtime/layout/getWindowDragZones.js +49 -0
- package/dist/runtime/layout/index.d.ts +14 -5
- package/dist/runtime/layout/index.js +14 -5
- package/dist/runtime/layout/isPointInRect.d.ts +7 -0
- package/dist/runtime/layout/{isPointInFrame.js → isPointInRect.js} +1 -1
- package/dist/runtime/layout/resizeFrame.js +2 -2
- package/dist/runtime/settings.d.ts +41 -16
- package/dist/runtime/settings.js +95 -53
- package/dist/runtime/types/index.d.ts +324 -54
- package/dist/runtime/types/index.js +54 -20
- package/package.json +28 -29
- package/src/runtime/components/FrameDragHandle.vue +30 -0
- package/src/runtime/components/LayoutDecos.vue +12 -36
- package/src/runtime/components/LayoutEdges.vue +27 -23
- package/src/runtime/components/LayoutFrame.vue +6 -5
- package/src/runtime/components/LayoutShapeSquare.vue +9 -3
- package/src/runtime/components/LayoutWindow.vue +110 -101
- package/src/runtime/composables/useFrames.ts +80 -50
- package/src/runtime/demo/App.vue +126 -36
- package/src/runtime/demo/DemoControls.vue +115 -6
- package/src/runtime/drag/CloseAction.ts +106 -44
- package/src/runtime/drag/DragActionHandler.ts +71 -20
- package/src/runtime/drag/FrameDragAction.ts +202 -0
- package/src/runtime/drag/SplitAction.ts +106 -34
- package/src/runtime/drag/createDefaultHandlers.ts +19 -0
- package/src/runtime/drag/defaultDragActions.ts +19 -0
- package/src/runtime/drag/types.ts +90 -20
- package/src/runtime/helpers/createZoneSideClipPath.ts +41 -0
- package/src/runtime/helpers/doEdgesOverlap.ts +11 -5
- package/src/runtime/helpers/getDockBoundaries.ts +36 -0
- package/src/runtime/helpers/getEdgeLength.ts +10 -0
- package/src/runtime/helpers/getIntersections.ts +2 -2
- package/src/runtime/helpers/getIntersectionsCss.ts +2 -2
- package/src/runtime/helpers/getMoveEdgeInfo.ts +2 -2
- package/src/runtime/helpers/getResizeLimit.ts +2 -2
- package/src/runtime/helpers/getShapeSquareCss.ts +2 -2
- package/src/runtime/helpers/getVisualEdgeCss.ts +2 -2
- package/src/runtime/helpers/getVisualEdges.ts +5 -4
- package/src/runtime/helpers/index.ts +4 -0
- package/src/runtime/helpers/isEdgeEqual.ts +2 -4
- package/src/runtime/helpers/isWindowEdge.ts +2 -2
- package/src/runtime/helpers/isWindowEdgePoint.ts +2 -2
- package/src/runtime/helpers/moveEdge.ts +2 -2
- package/src/runtime/helpers/numberToScaledPercent.ts +3 -3
- package/src/runtime/helpers/numberToScaledSize.ts +2 -2
- package/src/runtime/helpers/rotateFrames.ts +45 -0
- package/src/runtime/helpers/scaledPointToPx.ts +13 -0
- package/src/runtime/helpers/toWindowCoord.ts +2 -2
- package/src/runtime/layout/applyFrameChanges.ts +39 -0
- package/src/runtime/layout/createSplitDecoFromDrag.ts +12 -6
- package/src/runtime/layout/{createSplitDecoEdge.ts → createSplitDecoShapes.ts} +17 -7
- package/src/runtime/layout/debugFrame.ts +1 -1
- package/src/runtime/layout/findSafeSplitEdge.ts +3 -3
- package/src/runtime/layout/frameCreate.ts +2 -2
- package/src/runtime/layout/getCloseFrameInfo.ts +21 -8
- package/src/runtime/layout/getDragZones.ts +48 -0
- package/src/runtime/layout/getFillEmptySpaceInfo.ts +177 -0
- package/src/runtime/layout/getFrameCollapseInfo.ts +164 -0
- package/src/runtime/layout/getFrameDockInfo.ts +126 -0
- package/src/runtime/layout/getFrameDragZones.ts +100 -0
- package/src/runtime/layout/getFrameRearrangeInfo.ts +261 -0
- package/src/runtime/layout/getFrameSplitInfo.ts +21 -8
- package/src/runtime/layout/getFrameSwapInfo.ts +45 -0
- package/src/runtime/layout/getFrameTo.ts +2 -2
- package/src/runtime/layout/getFrameUncollapseInfo.ts +160 -0
- package/src/runtime/layout/getFrameUndockInfo.ts +97 -0
- package/src/runtime/layout/getFramesRedistributeInfo.ts +98 -0
- package/src/runtime/layout/getWindowDragZones.ts +59 -0
- package/src/runtime/layout/index.ts +14 -5
- package/src/runtime/layout/isPointInRect.ts +7 -0
- package/src/runtime/layout/resizeFrame.ts +2 -2
- package/src/runtime/settings.ts +69 -49
- package/src/runtime/types/index.ts +143 -28
- package/dist/runtime/layout/closeFrame.d.ts +0 -5
- package/dist/runtime/layout/closeFrame.js +0 -13
- package/dist/runtime/layout/closeFrames.d.ts +0 -2
- package/dist/runtime/layout/closeFrames.js +0 -8
- package/dist/runtime/layout/createSplitDecoEdge.d.ts +0 -2
- package/dist/runtime/layout/frameSplit.d.ts +0 -16
- package/dist/runtime/layout/frameSplit.js +0 -9
- package/dist/runtime/layout/isPointInFrame.d.ts +0 -2
- package/src/runtime/layout/closeFrame.ts +0 -33
- package/src/runtime/layout/closeFrames.ts +0 -14
- package/src/runtime/layout/frameSplit.ts +0 -31
- package/src/runtime/layout/isPointInFrame.ts +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSplitDecoShapes } from "./createSplitDecoShapes.js"
|
|
2
2
|
|
|
3
3
|
import { dirToOrientation } from "../helpers/dirToOrientation.js"
|
|
4
|
-
import {
|
|
4
|
+
import { settings } from "../settings.js"
|
|
5
5
|
import type { Direction, LayoutFrame, Point, RawSplitDeco, Size, SplitDeco } from "../types/index.js"
|
|
6
6
|
|
|
7
7
|
export function createSplitDecoFromDrag(
|
|
@@ -9,16 +9,22 @@ export function createSplitDecoFromDrag(
|
|
|
9
9
|
frame: LayoutFrame,
|
|
10
10
|
dragDirection: Direction,
|
|
11
11
|
dragPoint: Point,
|
|
12
|
-
snapAmount: Point =
|
|
13
|
-
minSize: Size =
|
|
12
|
+
snapAmount: Point = settings.snapPointScaled,
|
|
13
|
+
minSize: Size = settings.minSizeScaled,
|
|
14
|
+
classes: {
|
|
15
|
+
/** @default "deco-split-edge bg-red-500" */
|
|
16
|
+
splitEdge?: string
|
|
17
|
+
/** @default "deco-split-new-frame bg-blue-500/50" */
|
|
18
|
+
splitNewFrame?: string
|
|
19
|
+
} = {}
|
|
14
20
|
): SplitDeco {
|
|
15
21
|
const orientation = dirToOrientation(dragDirection)
|
|
16
|
-
const deco: RawSplitDeco = {
|
|
22
|
+
const deco: RawSplitDeco & Partial<SplitDeco> = {
|
|
17
23
|
type: "split",
|
|
18
24
|
id: frame.id,
|
|
19
25
|
position: dragPoint[orientation === "horizontal" ? "x" : "y"],
|
|
20
26
|
direction: dragDirection
|
|
21
27
|
}
|
|
22
|
-
|
|
28
|
+
deco.shapes = createSplitDecoShapes(frames, deco, snapAmount, minSize, classes)
|
|
23
29
|
return deco as SplitDeco
|
|
24
30
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { findSafeSplitEdgeAndPosition } from "./findSafeSplitEdge.js"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import type { LayoutFrame, Point, RawSplitDeco, Size
|
|
3
|
+
import { settings } from "../settings.js"
|
|
4
|
+
import type { LayoutFrame, LayoutShape, Point, RawSplitDeco, Size } from "../types/index.js"
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function createSplitDecoShapes(
|
|
7
7
|
frames: Record<string, LayoutFrame>,
|
|
8
8
|
deco: RawSplitDeco,
|
|
9
|
-
snapAmount: Point =
|
|
10
|
-
minSize: Size =
|
|
11
|
-
|
|
9
|
+
snapAmount: Point = settings.snapPointScaled,
|
|
10
|
+
minSize: Size = settings.minSizeScaled,
|
|
11
|
+
classes: {
|
|
12
|
+
/** @default "deco-split-edge bg-red-500" */
|
|
13
|
+
splitEdge?: string
|
|
14
|
+
/** @default "deco-split-new-frame bg-blue-500/50" */
|
|
15
|
+
splitNewFrame?: string
|
|
16
|
+
} = {}
|
|
17
|
+
|
|
18
|
+
): LayoutShape[] {
|
|
12
19
|
const frame = frames[deco.id]
|
|
13
20
|
const { edge, position } = findSafeSplitEdgeAndPosition(frame, deco.direction, deco.position, snapAmount, minSize)
|
|
14
21
|
const newFrame = { x: frame.x, y: frame.y, width: frame.width, height: frame.height }
|
|
@@ -30,5 +37,8 @@ export function createSplitDecoEdge(
|
|
|
30
37
|
break
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
return
|
|
40
|
+
return [
|
|
41
|
+
{ type: "edge", data: edge, attrs: { class: classes.splitEdge ?? "deco-split-edge bg-red-500" } },
|
|
42
|
+
{ type: "square", data: newFrame, attrs: { class: classes.splitNewFrame ?? "deco-split-new-frame bg-blue-500/50" } }
|
|
43
|
+
]
|
|
34
44
|
}
|
|
@@ -2,5 +2,5 @@ import type { LayoutFrame } from "../types/index.js"
|
|
|
2
2
|
|
|
3
3
|
export function debugFrame(frame: LayoutFrame): string {
|
|
4
4
|
const f = frame
|
|
5
|
-
return `id: ${f.id.slice(0, 4)}, x: ${f.x}, y: ${f.y}, w: ${f.width}, h: ${f.height}`
|
|
5
|
+
return `id: ${f.id.slice(0, 4)}, x: ${f.x}, y: ${f.y}, w: ${f.width}, h: ${f.height}\ndocked: ${f.docked}, collapsed: ${f.collapsed}`
|
|
6
6
|
}
|
|
@@ -2,15 +2,15 @@ import { clampNumber, snapNumber } from "@alanscodelog/utils"
|
|
|
2
2
|
|
|
3
3
|
import { dirToOrientation } from "../helpers/dirToOrientation.js"
|
|
4
4
|
import { oppositeSide } from "../helpers/oppositeSide.js"
|
|
5
|
-
import {
|
|
5
|
+
import { settings } from "../settings.js"
|
|
6
6
|
import type { Direction, Edge, LayoutFrame, Point, Size } from "../types/index.js"
|
|
7
7
|
|
|
8
8
|
export function findSafeSplitEdgeAndPosition(
|
|
9
9
|
frame: LayoutFrame,
|
|
10
10
|
dragDirection: Direction,
|
|
11
11
|
dragPointOrPosition: Point | number,
|
|
12
|
-
snapAmount: Point =
|
|
13
|
-
minSize: Size =
|
|
12
|
+
snapAmount: Point = settings.snapPointScaled,
|
|
13
|
+
minSize: Size = settings.minSizeScaled
|
|
14
14
|
): { edge: Edge, position: number } {
|
|
15
15
|
const orientation = dirToOrientation(dragDirection)
|
|
16
16
|
const position
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { settings } from "../settings.js"
|
|
4
4
|
import type {
|
|
5
5
|
BaseLayoutFrame,
|
|
6
6
|
ExtendedLayoutFrame,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
export function frameCreate(
|
|
12
12
|
opts: Partial<BaseLayoutFrame> & Omit<ExtendedLayoutFrame, keyof BaseLayoutFrame> = {}
|
|
13
13
|
): LayoutFrame {
|
|
14
|
-
const maxInt =
|
|
14
|
+
const maxInt = settings.maxInt
|
|
15
15
|
return {
|
|
16
16
|
width: maxInt,
|
|
17
17
|
height: maxInt,
|
|
@@ -10,12 +10,14 @@ import { dirToSide } from "../helpers/dirToSide.js"
|
|
|
10
10
|
import { findFrameDraggableEdges } from "../helpers/findFrameDraggableEdges.js"
|
|
11
11
|
import { getEdgeOrientation } from "../helpers/getEdgeOrientation.js"
|
|
12
12
|
import { oppositeSide } from "../helpers/oppositeSide.js"
|
|
13
|
-
import {
|
|
14
|
-
import { type Direction, type Edge, type EdgeSide, LAYOUT_ERROR, type LayoutFrame, type Size } from "../types/index.js"
|
|
13
|
+
import { settings } from "../settings.js"
|
|
14
|
+
import { type Direction, type Edge, type EdgeSide, LAYOUT_ERROR, type LayoutChange, type LayoutFrame, type Size } from "../types/index.js"
|
|
15
15
|
import { KnownError } from "../utils/KnownError.js"
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Returns the information necessary to close a frame or frames (if force: true).
|
|
18
|
+
* Returns a {@link LayoutChange} with the information necessary to close a frame or frames (if force: true).
|
|
19
|
+
*
|
|
20
|
+
* Changes can be applied to a window with {@link applyFrameChanges}.
|
|
19
21
|
*
|
|
20
22
|
* Can close by direction or by frame edge.
|
|
21
23
|
*
|
|
@@ -74,6 +76,8 @@ import { KnownError } from "../utils/KnownError.js"
|
|
|
74
76
|
*│C │ │
|
|
75
77
|
*└─────────┴──┘
|
|
76
78
|
* ```
|
|
79
|
+
*
|
|
80
|
+
* Changes can be applied with {@link applyFrameChanges}.
|
|
77
81
|
*/
|
|
78
82
|
|
|
79
83
|
export function getCloseFrameInfo<T extends "edge" | "dir">(
|
|
@@ -88,19 +92,28 @@ export function getCloseFrameInfo<T extends "edge" | "dir">(
|
|
|
88
92
|
closeDirOrSide: (T extends "dir" ? Direction : EdgeSide),
|
|
89
93
|
closeBy: T = "dir" as any as T,
|
|
90
94
|
force: boolean = false,
|
|
91
|
-
minSize: Size =
|
|
92
|
-
):
|
|
95
|
+
minSize: Size = settings.minSizeScaled
|
|
96
|
+
): LayoutChange
|
|
93
97
|
| KnownError<
|
|
94
98
|
| typeof LAYOUT_ERROR.CANT_CLOSE_NEARBY_FRAMES_TOO_SMALL
|
|
95
99
|
| typeof LAYOUT_ERROR.CANT_CLOSE_NO_DRAG_EDGE
|
|
96
100
|
| typeof LAYOUT_ERROR.CANT_CLOSE_SINGLE_FRAME
|
|
97
101
|
| typeof LAYOUT_ERROR.CANT_CLOSE_WITHOUT_FORCE
|
|
102
|
+
| typeof LAYOUT_ERROR.CANT_LEAVE_NO_UNDOCKED_FRAMES
|
|
98
103
|
> {
|
|
99
104
|
if (frames.length === 1) {
|
|
100
105
|
return new KnownError(LAYOUT_ERROR.CANT_CLOSE_SINGLE_FRAME,
|
|
101
106
|
`Can't close frame ${frame.id}, it is the last frame in the window.`,
|
|
102
107
|
{ frame })
|
|
103
108
|
}
|
|
109
|
+
if (!frame.docked && frames.filter(_ => !_.docked).length === 0) {
|
|
110
|
+
return new KnownError(
|
|
111
|
+
LAYOUT_ERROR.CANT_LEAVE_NO_UNDOCKED_FRAMES,
|
|
112
|
+
`Can't close last undocked frame. One undocked frame must remain`,
|
|
113
|
+
{ frame: frame.id }
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
const side = closeBy === "dir"
|
|
105
118
|
? oppositeSide(dirToSide(closeDirOrSide as Direction))
|
|
106
119
|
: closeDirOrSide as EdgeSide
|
|
@@ -159,8 +172,8 @@ export function getCloseFrameInfo<T extends "edge" | "dir">(
|
|
|
159
172
|
)
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
|
-
const modifiedFrames = []
|
|
163
|
-
const deletedFrames = []
|
|
175
|
+
const modifiedFrames: LayoutFrame[] = []
|
|
176
|
+
const deletedFrames: LayoutFrame[] = []
|
|
164
177
|
const moveAmount = thisFrameSize
|
|
165
178
|
|
|
166
179
|
for (const entry of sameSideEntries) {
|
|
@@ -189,5 +202,5 @@ export function getCloseFrameInfo<T extends "edge" | "dir">(
|
|
|
189
202
|
clone[sizeKey] += moveAmount
|
|
190
203
|
modifiedFrames.push(clone)
|
|
191
204
|
}
|
|
192
|
-
return { modifiedFrames, deletedFrames }
|
|
205
|
+
return { modified: modifiedFrames, created: [], deleted: deletedFrames }
|
|
193
206
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getFrameDragZones } from "./getFrameDragZones.js"
|
|
2
|
+
import { getWindowDragZones } from "./getWindowDragZones.js"
|
|
3
|
+
import { isPointInRect } from "./isPointInRect.js"
|
|
4
|
+
|
|
5
|
+
import type { DragState, DragZone, WindowEdgeZone } from "../types/index.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find the drag zone under the drag point.
|
|
9
|
+
*/
|
|
10
|
+
export function getDragZones(
|
|
11
|
+
state: Pick<DragState, "dragPoint" | "dragHoveredFrame" | "frames" | "win">,
|
|
12
|
+
{ frameEdgePx, windowEdgePx }: { frameEdgePx: number, windowEdgePx: number }
|
|
13
|
+
): DragZone | undefined {
|
|
14
|
+
const { dragPoint, win } = state
|
|
15
|
+
const frameId = state.dragHoveredFrame?.id
|
|
16
|
+
if (!frameId) return
|
|
17
|
+
|
|
18
|
+
const frame = state.frames[frameId]
|
|
19
|
+
if (!frame || !dragPoint) return
|
|
20
|
+
|
|
21
|
+
const frameZones = getFrameDragZones(frame, frameEdgePx, win.pxWidth, win.pxHeight)
|
|
22
|
+
|
|
23
|
+
const windowZones = getWindowDragZones(win, windowEdgePx)
|
|
24
|
+
|
|
25
|
+
let matchedZone: DragZone | undefined
|
|
26
|
+
for (const zone of windowZones) {
|
|
27
|
+
if (isPointInRect(zone, dragPoint)) {
|
|
28
|
+
matchedZone = {
|
|
29
|
+
...zone,
|
|
30
|
+
type: "window"
|
|
31
|
+
} as WindowEdgeZone
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!matchedZone) {
|
|
37
|
+
for (const zone of frameZones) {
|
|
38
|
+
if (isPointInRect(zone, dragPoint)) {
|
|
39
|
+
matchedZone = zone
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!matchedZone) return undefined
|
|
46
|
+
|
|
47
|
+
return matchedZone
|
|
48
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { doEdgesOverlap } from "../helpers/doEdgesOverlap.js"
|
|
2
|
+
import { frameToEdges } from "../helpers/frameToEdges.js"
|
|
3
|
+
import { getEdgeLength } from "../helpers/getEdgeLength.js"
|
|
4
|
+
import { oppositeSide } from "../helpers/oppositeSide.js"
|
|
5
|
+
import type { EdgeSide, LayoutChange, LayoutFrame, LayoutWindow } from "../types/index.js"
|
|
6
|
+
import { LAYOUT_ERROR } from "../types/index.js"
|
|
7
|
+
import { KnownError } from "../utils/KnownError.js"
|
|
8
|
+
/**
|
|
9
|
+
* Finds a frame to fill the empty space left behind by a moved/empty frame (e.g. a dragged frame's original position).
|
|
10
|
+
*
|
|
11
|
+
* Returns a {@link LayoutChange} with the information necessary information..
|
|
12
|
+
*
|
|
13
|
+
* Changes can be applied to a window with {@link applyFrameChanges}.
|
|
14
|
+
*
|
|
15
|
+
* Examples where * would be the empty space:
|
|
16
|
+
*
|
|
17
|
+
* Selection priority:
|
|
18
|
+
* 1. Prefer frames listed in preferredFrames (for dragging these are the dragged and target frames).
|
|
19
|
+
* ┌─────┬─────┐
|
|
20
|
+
* │A │* │
|
|
21
|
+
* ├─────┼─────┤
|
|
22
|
+
* │C │B │
|
|
23
|
+
* └─────┴─────┘
|
|
24
|
+
*
|
|
25
|
+
* Without preferredFrames set to B, A would be preferred as it shares the shortest edge with the empty space.
|
|
26
|
+
*
|
|
27
|
+
* ┌─────┬─────┐
|
|
28
|
+
* │A │B │
|
|
29
|
+
* ├─────┤ │
|
|
30
|
+
* │C │ │
|
|
31
|
+
* └─────┴─────┘
|
|
32
|
+
*
|
|
33
|
+
* In a more complex example:
|
|
34
|
+
*
|
|
35
|
+
* ┌─────┬─────┬─────┐
|
|
36
|
+
* │A │* │C │
|
|
37
|
+
* │ │ ├─────┤
|
|
38
|
+
* │ │ │D │
|
|
39
|
+
* └─────┴─────┴─────┘
|
|
40
|
+
*
|
|
41
|
+
* If preferredFrames is C OR D (usually it's both but it might only be one, just take all other frames along that edge if they end at the empty frames end).
|
|
42
|
+
*
|
|
43
|
+
* ┌─────┬──────────┐
|
|
44
|
+
* │A │C │
|
|
45
|
+
* │ ├──────────┤
|
|
46
|
+
* │ │D │
|
|
47
|
+
* └─────┴──────────┘
|
|
48
|
+
*
|
|
49
|
+
* And then in the following we would not be able to even expand C or D so we would not be able to satisfy the preference:
|
|
50
|
+
*
|
|
51
|
+
* ┌─────┬─────┬─────┐
|
|
52
|
+
* │A │* │C │
|
|
53
|
+
* │ │ ├─────┤
|
|
54
|
+
* │ │ │ │
|
|
55
|
+
* │ ├─────┤D │
|
|
56
|
+
* │ │B │ │
|
|
57
|
+
* └─────┴─────┴─────┘
|
|
58
|
+
*
|
|
59
|
+
* 2. Prefer the shortest frame that shares an exact edge. Taking the previous example it would be B:
|
|
60
|
+
*
|
|
61
|
+
* ┌─────┬─────┬─────┐
|
|
62
|
+
* │A │B │C │
|
|
63
|
+
* │ │ ├─────┤
|
|
64
|
+
* │ │ │ │
|
|
65
|
+
* │ │ │D │
|
|
66
|
+
* │ │ │ │
|
|
67
|
+
* └─────┴─────┴─────┘
|
|
68
|
+
*/
|
|
69
|
+
export function getFillEmptySpaceInfo(
|
|
70
|
+
win: LayoutWindow,
|
|
71
|
+
emptyFrame: Omit<LayoutFrame, "id">,
|
|
72
|
+
preferredFrames: string[] = [],
|
|
73
|
+
skipFrames: string[] = []
|
|
74
|
+
): LayoutChange | KnownError<typeof LAYOUT_ERROR.NO_FILL_CANDIDATES> {
|
|
75
|
+
const emptyEdges = frameToEdges(emptyFrame as LayoutFrame)
|
|
76
|
+
const vacHeight = emptyFrame.height
|
|
77
|
+
const vacWidth = emptyFrame.width
|
|
78
|
+
|
|
79
|
+
type Candidate = {
|
|
80
|
+
frame: LayoutFrame
|
|
81
|
+
frameSide: EdgeSide
|
|
82
|
+
emptySide: EdgeSide
|
|
83
|
+
edgeLength: number
|
|
84
|
+
exact: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type SideGroup = {
|
|
88
|
+
candidates: Candidate[]
|
|
89
|
+
skip: boolean
|
|
90
|
+
totalLength: number
|
|
91
|
+
hasExact: boolean
|
|
92
|
+
hasPreferred: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const candidatesBySides = new Map<EdgeSide, SideGroup>()
|
|
96
|
+
|
|
97
|
+
for (const frame of Object.values(win.frames)) {
|
|
98
|
+
if (skipFrames.includes(frame.id)) continue
|
|
99
|
+
const frameEdges = frameToEdges(frame)
|
|
100
|
+
|
|
101
|
+
for (const frameSide of ["top", "bottom", "left", "right"] as EdgeSide[]) {
|
|
102
|
+
const frameEdge = frameEdges[frameSide]
|
|
103
|
+
const emptyOppositeSide = oppositeSide(frameSide)
|
|
104
|
+
const emptyEdge = emptyEdges[emptyOppositeSide]
|
|
105
|
+
|
|
106
|
+
if (candidatesBySides.get(emptyOppositeSide)?.skip) continue
|
|
107
|
+
|
|
108
|
+
const exact = (
|
|
109
|
+
frameEdge.startX === emptyEdge.startX
|
|
110
|
+
&& frameEdge.startY === emptyEdge.startY
|
|
111
|
+
&& frameEdge.endX === emptyEdge.endX
|
|
112
|
+
&& frameEdge.endY === emptyEdge.endY
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (exact || doEdgesOverlap(frameEdge, emptyEdge, undefined, false)) {
|
|
116
|
+
if (!candidatesBySides.has(emptyOppositeSide)) {
|
|
117
|
+
candidatesBySides.set(emptyOppositeSide, { candidates: [], skip: false, totalLength: 0, hasExact: false, hasPreferred: false })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const group = candidatesBySides.get(emptyOppositeSide)!
|
|
121
|
+
|
|
122
|
+
// for shared edges if any of the candidates exceed the empty edge we can't use that side
|
|
123
|
+
// so we mark it as skip so we can skip all other candidates on that side
|
|
124
|
+
if (!exact) {
|
|
125
|
+
const isVertical = frameEdge.startX === emptyEdge.startX
|
|
126
|
+
const extendsBeyond = isVertical
|
|
127
|
+
? (frameEdge.startY < emptyEdge.startY || frameEdge.endY > emptyEdge.endY)
|
|
128
|
+
: (frameEdge.startX < emptyEdge.startX || frameEdge.endX > emptyEdge.endX)
|
|
129
|
+
|
|
130
|
+
if (extendsBeyond) {
|
|
131
|
+
group.skip = true
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const len = getEdgeLength(frameEdge)
|
|
135
|
+
group.totalLength += len
|
|
136
|
+
if (exact) group.hasExact = true
|
|
137
|
+
if (preferredFrames.includes(frame.id)) group.hasPreferred = true
|
|
138
|
+
group.candidates.push({ frame, frameSide, emptySide: emptyOppositeSide, edgeLength: len, exact })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const entries = [...candidatesBySides.values()]
|
|
144
|
+
const preferredSides = entries.filter(c => c.hasPreferred && !c.skip).sort((a, b) => a.totalLength - b.totalLength)
|
|
145
|
+
const fallbackSides = entries.filter(c => !c.hasPreferred && !c.skip).sort((a, b) => a.totalLength - b.totalLength)
|
|
146
|
+
|
|
147
|
+
if (preferredSides.length === 0 && fallbackSides.length === 0) return new KnownError(LAYOUT_ERROR.NO_FILL_CANDIDATES, `No fill candidates found.`, {})
|
|
148
|
+
|
|
149
|
+
const chosenCandidates = preferredSides.length > 0 ? preferredSides[0].candidates : fallbackSides[0].candidates
|
|
150
|
+
|
|
151
|
+
const result: LayoutFrame[] = []
|
|
152
|
+
for (const chosen of chosenCandidates) {
|
|
153
|
+
const { frame, frameSide } = chosen
|
|
154
|
+
const updated = { ...frame }
|
|
155
|
+
|
|
156
|
+
switch (frameSide) {
|
|
157
|
+
case "top":
|
|
158
|
+
updated.y -= vacHeight
|
|
159
|
+
updated.height += vacHeight
|
|
160
|
+
break
|
|
161
|
+
case "bottom":
|
|
162
|
+
updated.height += vacHeight
|
|
163
|
+
break
|
|
164
|
+
case "left":
|
|
165
|
+
updated.x -= vacWidth
|
|
166
|
+
updated.width += vacWidth
|
|
167
|
+
break
|
|
168
|
+
case "right":
|
|
169
|
+
updated.width += vacWidth
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result.push(updated)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { modified: result, created: [], deleted: [] }
|
|
177
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
|
|
2
|
+
import { walk } from "@alanscodelog/utils/walk"
|
|
3
|
+
|
|
4
|
+
import { applyFrameChanges } from "./applyFrameChanges.js"
|
|
5
|
+
import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
|
|
6
|
+
|
|
7
|
+
import { oppositeSide } from "../helpers/oppositeSide.js"
|
|
8
|
+
import { settings } from "../settings.js"
|
|
9
|
+
import type { EdgeSide, LayoutChange, LayoutWindow, Size } from "../types/index.js"
|
|
10
|
+
import { LAYOUT_ERROR } from "../types/index.js"
|
|
11
|
+
import { KnownError } from "../utils/KnownError.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns a {@link LayoutChange} with the information necessary to collapse a docked frame.
|
|
15
|
+
*
|
|
16
|
+
* Changes can be applied to a window with {@link applyFrameChanges}.
|
|
17
|
+
*
|
|
18
|
+
* Collapsing shrinks the frame to the configured collapse size and redistributes
|
|
19
|
+
* the freed space to neighboring frames. The pre-collapse size is stored in
|
|
20
|
+
* `collapsed` so it can be restored later.
|
|
21
|
+
*/
|
|
22
|
+
export function getFrameCollapseInfo(
|
|
23
|
+
win: LayoutWindow,
|
|
24
|
+
frameId: string
|
|
25
|
+
): LayoutChange
|
|
26
|
+
| KnownError<typeof LAYOUT_ERROR.CANT_COLLAPSE_NOT_DOCKED> {
|
|
27
|
+
win = walk(win, undefined, { save: true }) as typeof win
|
|
28
|
+
const frame = win.frames[frameId]
|
|
29
|
+
if (!frame) throw new Error(`Unknown frame ${frameId}`)
|
|
30
|
+
if (!frame.docked) throw new Error(`Frame ${frameId} is not docked.`)
|
|
31
|
+
if (typeof frame.collapsed === "number") throw new Error(`Frame ${frameId} is already collapsed.`)
|
|
32
|
+
|
|
33
|
+
const toExtract = [frame.id]
|
|
34
|
+
if (frame.docked === undefined) {
|
|
35
|
+
return new KnownError(
|
|
36
|
+
LAYOUT_ERROR.CANT_COLLAPSE_NOT_DOCKED,
|
|
37
|
+
`Frame ${frameId} is not docked and cannot be collapsed.`,
|
|
38
|
+
{ frame }
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isVertical = frame.docked === "left" || frame.docked === "right"
|
|
43
|
+
const sizeKey = isVertical ? "width" : "height" as const
|
|
44
|
+
const posKey = isVertical ? "x" : "y"
|
|
45
|
+
const oppositePosKey = isVertical ? "y" : "x"
|
|
46
|
+
const oppositeSizeKey = isVertical ? "height" : "width"
|
|
47
|
+
|
|
48
|
+
const collapseSize: Size = settings.collapseSizeScaled
|
|
49
|
+
|
|
50
|
+
const currentSize = frame[sizeKey]
|
|
51
|
+
const collapseAmount = collapseSize[sizeKey]
|
|
52
|
+
const shrinkAmount = currentSize - collapseAmount
|
|
53
|
+
|
|
54
|
+
const dockedSide = frame.docked as EdgeSide
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* When multiple frames are docked* they may share an edge like this:
|
|
59
|
+
* For example, if we collapse A, we need to allow everything else to expand,
|
|
60
|
+
* but if we expand D it will overflow the bounds of the window and we'd get an error.
|
|
61
|
+
* To avoid this, we shrink these frames towards the opposite side first.
|
|
62
|
+
*
|
|
63
|
+
* collapsed size/amount
|
|
64
|
+
* ├──┘
|
|
65
|
+
* │ shrink amount
|
|
66
|
+
* │ ├───┘
|
|
67
|
+
* ├──────┬───────────┐
|
|
68
|
+
* │A*│ │B* │
|
|
69
|
+
* │ ├──────┬────┤
|
|
70
|
+
* │ │ │E │C* │
|
|
71
|
+
* │ │ │ │
|
|
72
|
+
* ├──┴───┴──────┤ │
|
|
73
|
+
* │D* │ │
|
|
74
|
+
* └──────┬──────┴────┘
|
|
75
|
+
* │we would shrink D to here
|
|
76
|
+
*
|
|
77
|
+
* D.x = A.x + A.width, and then subtract the difference from it's width
|
|
78
|
+
*
|
|
79
|
+
* ***IMPROTANT: It needs to stay aligned with the right side of A and the rest of the frames, or redistribute doesn't know how to redistribute it properly.
|
|
80
|
+
*
|
|
81
|
+
* When redistribute expands it again by shrinkAmount it will end up at A.x + collapseSize.
|
|
82
|
+
*
|
|
83
|
+
* This works fine when collapseSize is 0, but otherwise breaks so we keep a list of
|
|
84
|
+
* fixes to make after redistributing to place it's left edge back at the right place (it's right edge will have been moved by redistribute).
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
const framesToFix: ({
|
|
88
|
+
id: string
|
|
89
|
+
posKey: "x" | "y"
|
|
90
|
+
sizeKey: "width" | "height"
|
|
91
|
+
type: "start" | "end"
|
|
92
|
+
})[] = []
|
|
93
|
+
const opposite = oppositeSide(dockedSide)
|
|
94
|
+
for (const other of Object.values(win.frames)) {
|
|
95
|
+
if (frame.id === other.id || !other.docked) continue
|
|
96
|
+
|
|
97
|
+
if (other.docked === opposite) continue
|
|
98
|
+
if (dockedSide === "left" || dockedSide === "top") {
|
|
99
|
+
if (other[posKey] !== 0) continue
|
|
100
|
+
other[posKey] = frame[posKey] + frame[sizeKey]
|
|
101
|
+
other[sizeKey] -= frame[sizeKey]
|
|
102
|
+
framesToFix.push({
|
|
103
|
+
id: other.id,
|
|
104
|
+
posKey,
|
|
105
|
+
sizeKey,
|
|
106
|
+
type: "start"
|
|
107
|
+
})
|
|
108
|
+
} else {
|
|
109
|
+
if (other[posKey] + other[sizeKey] !== settings.maxInt) continue
|
|
110
|
+
other[sizeKey] -= frame[sizeKey]
|
|
111
|
+
framesToFix.push({
|
|
112
|
+
id: other.id,
|
|
113
|
+
posKey,
|
|
114
|
+
sizeKey,
|
|
115
|
+
type: "end"
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
|
|
121
|
+
const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
|
|
122
|
+
|
|
123
|
+
const redistributeSide = oppositeSide(dockedSide)
|
|
124
|
+
|
|
125
|
+
const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, -shrinkAmount, true)
|
|
126
|
+
|
|
127
|
+
if (changes instanceof KnownError) {
|
|
128
|
+
// we should never get out of bounds and because this is a collapse there should always be space
|
|
129
|
+
if (changes.code === LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS || LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE) {
|
|
130
|
+
changes.message = `This error should never happen, please file a bug report: ${changes.message}`
|
|
131
|
+
throw changes
|
|
132
|
+
}
|
|
133
|
+
return changes as any // in case of other errors
|
|
134
|
+
}
|
|
135
|
+
applyFrameChanges(win, changes)
|
|
136
|
+
pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
|
|
137
|
+
|
|
138
|
+
for (const fix of framesToFix) {
|
|
139
|
+
const other = win.frames[fix.id]
|
|
140
|
+
if (fix.type === "start") {
|
|
141
|
+
const sizeDiff = other[fix.posKey]
|
|
142
|
+
other[fix.posKey] = 0
|
|
143
|
+
other[fix.sizeKey] += sizeDiff
|
|
144
|
+
} else {
|
|
145
|
+
const sizeDiff = settings.maxInt - (other[fix.posKey] + other[fix.sizeKey])
|
|
146
|
+
other[fix.sizeKey] += sizeDiff
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
pushIfNotIn(toExtract, framesToFix.map(_ => _.id))
|
|
150
|
+
|
|
151
|
+
if (frame.docked === "right" || frame.docked === "bottom") {
|
|
152
|
+
frame[posKey] = frame[posKey] + (frame[sizeKey] - collapseSize[sizeKey])
|
|
153
|
+
}
|
|
154
|
+
frame[sizeKey] = collapseSize[sizeKey]
|
|
155
|
+
frame.collapsed = currentSize
|
|
156
|
+
// when we collapse to 0 it's a special case where the frame will always fit the entire window edge
|
|
157
|
+
// for proper uncollapsing later
|
|
158
|
+
if (collapseSize[sizeKey] === 0) {
|
|
159
|
+
frame[oppositePosKey] = 0
|
|
160
|
+
frame[oppositeSizeKey] = settings.maxInt
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
|
|
164
|
+
}
|