@witchcraft/layout 0.1.3 → 0.2.0

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.
Files changed (189) hide show
  1. package/README.md +27 -24
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/FrameDragHandle.d.vue.ts +15 -0
  4. package/dist/runtime/components/FrameDragHandle.vue +28 -0
  5. package/dist/runtime/components/FrameDragHandle.vue.d.ts +15 -0
  6. package/dist/runtime/components/LayoutDecos.d.vue.ts +2 -4
  7. package/dist/runtime/components/LayoutDecos.vue +10 -29
  8. package/dist/runtime/components/LayoutDecos.vue.d.ts +2 -4
  9. package/dist/runtime/components/LayoutEdges.d.vue.ts +3 -3
  10. package/dist/runtime/components/LayoutEdges.vue +8 -8
  11. package/dist/runtime/components/LayoutEdges.vue.d.ts +3 -3
  12. package/dist/runtime/components/LayoutFrame.d.vue.ts +1 -1
  13. package/dist/runtime/components/LayoutFrame.vue +0 -1
  14. package/dist/runtime/components/LayoutFrame.vue.d.ts +1 -1
  15. package/dist/runtime/components/LayoutShapeSquare.d.vue.ts +3 -1
  16. package/dist/runtime/components/LayoutShapeSquare.vue.d.ts +3 -1
  17. package/dist/runtime/components/LayoutWindow.d.vue.ts +26 -12
  18. package/dist/runtime/components/LayoutWindow.vue +95 -84
  19. package/dist/runtime/components/LayoutWindow.vue.d.ts +26 -12
  20. package/dist/runtime/composables/useFrames.d.ts +15 -13
  21. package/dist/runtime/composables/useFrames.js +59 -39
  22. package/dist/runtime/demo/App.vue +116 -30
  23. package/dist/runtime/demo/DemoControls.d.vue.ts +4 -1
  24. package/dist/runtime/demo/DemoControls.vue +98 -4
  25. package/dist/runtime/demo/DemoControls.vue.d.ts +4 -1
  26. package/dist/runtime/drag/CloseAction.d.ts +26 -5
  27. package/dist/runtime/drag/CloseAction.js +87 -40
  28. package/dist/runtime/drag/DragActionHandler.d.ts +20 -8
  29. package/dist/runtime/drag/DragActionHandler.js +47 -12
  30. package/dist/runtime/drag/FrameDragAction.d.ts +45 -0
  31. package/dist/runtime/drag/FrameDragAction.js +143 -0
  32. package/dist/runtime/drag/SplitAction.d.ts +32 -11
  33. package/dist/runtime/drag/SplitAction.js +82 -24
  34. package/dist/runtime/drag/createDefaultHandlers.d.ts +9 -0
  35. package/dist/runtime/drag/createDefaultHandlers.js +10 -0
  36. package/dist/runtime/drag/defaultDragActions.d.ts +9 -0
  37. package/dist/runtime/drag/defaultDragActions.js +10 -0
  38. package/dist/runtime/drag/types.d.ts +82 -13
  39. package/dist/runtime/drag/types.js +1 -0
  40. package/dist/runtime/helpers/createZoneSideClipPath.d.ts +12 -0
  41. package/dist/runtime/helpers/createZoneSideClipPath.js +17 -0
  42. package/dist/runtime/helpers/doEdgesOverlap.d.ts +3 -1
  43. package/dist/runtime/helpers/doEdgesOverlap.js +5 -5
  44. package/dist/runtime/helpers/getDockBoundaries.d.ts +19 -0
  45. package/dist/runtime/helpers/getDockBoundaries.js +14 -0
  46. package/dist/runtime/helpers/getEdgeLength.d.ts +2 -0
  47. package/dist/runtime/helpers/getEdgeLength.js +5 -0
  48. package/dist/runtime/helpers/getIntersections.js +2 -2
  49. package/dist/runtime/helpers/getIntersectionsCss.js +2 -2
  50. package/dist/runtime/helpers/getMoveEdgeInfo.js +2 -2
  51. package/dist/runtime/helpers/getResizeLimit.js +2 -2
  52. package/dist/runtime/helpers/getShapeSquareCss.js +2 -2
  53. package/dist/runtime/helpers/getVisualEdgeCss.js +2 -2
  54. package/dist/runtime/helpers/getVisualEdges.d.ts +1 -1
  55. package/dist/runtime/helpers/getVisualEdges.js +4 -3
  56. package/dist/runtime/helpers/index.d.ts +4 -0
  57. package/dist/runtime/helpers/index.js +4 -0
  58. package/dist/runtime/helpers/isEdgeEqual.js +2 -4
  59. package/dist/runtime/helpers/isWindowEdge.js +2 -2
  60. package/dist/runtime/helpers/isWindowEdgePoint.js +2 -2
  61. package/dist/runtime/helpers/moveEdge.js +2 -2
  62. package/dist/runtime/helpers/numberToScaledPercent.d.ts +1 -1
  63. package/dist/runtime/helpers/numberToScaledPercent.js +2 -2
  64. package/dist/runtime/helpers/numberToScaledSize.js +2 -2
  65. package/dist/runtime/helpers/rotateFrames.d.ts +7 -0
  66. package/dist/runtime/helpers/rotateFrames.js +36 -0
  67. package/dist/runtime/helpers/scaledPointToPx.d.ts +13 -0
  68. package/dist/runtime/helpers/scaledPointToPx.js +7 -0
  69. package/dist/runtime/helpers/toWindowCoord.js +2 -2
  70. package/dist/runtime/layout/applyFrameChanges.d.ts +10 -0
  71. package/dist/runtime/layout/applyFrameChanges.js +29 -0
  72. package/dist/runtime/layout/createSplitDecoFromDrag.d.ts +6 -1
  73. package/dist/runtime/layout/createSplitDecoFromDrag.js +4 -4
  74. package/dist/runtime/layout/createSplitDecoShapes.d.ts +7 -0
  75. package/dist/runtime/layout/{createSplitDecoEdge.js → createSplitDecoShapes.js} +6 -3
  76. package/dist/runtime/layout/debugFrame.js +2 -1
  77. package/dist/runtime/layout/findSafeSplitEdge.js +2 -2
  78. package/dist/runtime/layout/frameCreate.js +2 -2
  79. package/dist/runtime/layout/getCloseFrameInfo.d.ts +7 -6
  80. package/dist/runtime/layout/getCloseFrameInfo.js +10 -3
  81. package/dist/runtime/layout/getDragZones.d.ts +8 -0
  82. package/dist/runtime/layout/getDragZones.js +32 -0
  83. package/dist/runtime/layout/getFillEmptySpaceInfo.d.ts +65 -0
  84. package/dist/runtime/layout/getFillEmptySpaceInfo.js +69 -0
  85. package/dist/runtime/layout/getFrameCollapseInfo.d.ts +13 -0
  86. package/dist/runtime/layout/getFrameCollapseInfo.js +93 -0
  87. package/dist/runtime/layout/getFrameDockInfo.d.ts +9 -0
  88. package/dist/runtime/layout/getFrameDockInfo.js +82 -0
  89. package/dist/runtime/layout/getFrameDragZones.d.ts +16 -0
  90. package/dist/runtime/layout/getFrameDragZones.js +74 -0
  91. package/dist/runtime/layout/getFrameRearrangeInfo.d.ts +139 -0
  92. package/dist/runtime/layout/getFrameRearrangeInfo.js +87 -0
  93. package/dist/runtime/layout/getFrameSplitInfo.d.ts +7 -5
  94. package/dist/runtime/layout/getFrameSplitInfo.js +10 -3
  95. package/dist/runtime/layout/getFrameSwapInfo.d.ts +9 -0
  96. package/dist/runtime/layout/getFrameSwapInfo.js +27 -0
  97. package/dist/runtime/layout/getFrameTo.js +2 -2
  98. package/dist/runtime/layout/getFrameUncollapseInfo.d.ts +12 -0
  99. package/dist/runtime/layout/getFrameUncollapseInfo.js +88 -0
  100. package/dist/runtime/layout/getFrameUndockInfo.d.ts +13 -0
  101. package/dist/runtime/layout/getFrameUndockInfo.js +51 -0
  102. package/dist/runtime/layout/getFramesRedistributeInfo.d.ts +29 -0
  103. package/dist/runtime/layout/getFramesRedistributeInfo.js +53 -0
  104. package/dist/runtime/layout/getWindowDragZones.d.ts +6 -0
  105. package/dist/runtime/layout/getWindowDragZones.js +49 -0
  106. package/dist/runtime/layout/index.d.ts +14 -5
  107. package/dist/runtime/layout/index.js +14 -5
  108. package/dist/runtime/layout/isPointInRect.d.ts +7 -0
  109. package/dist/runtime/layout/{isPointInFrame.js → isPointInRect.js} +1 -1
  110. package/dist/runtime/layout/resizeFrame.js +2 -2
  111. package/dist/runtime/settings.d.ts +41 -16
  112. package/dist/runtime/settings.js +95 -53
  113. package/dist/runtime/types/index.d.ts +324 -54
  114. package/dist/runtime/types/index.js +54 -20
  115. package/package.json +28 -29
  116. package/src/runtime/components/FrameDragHandle.vue +30 -0
  117. package/src/runtime/components/LayoutDecos.vue +12 -36
  118. package/src/runtime/components/LayoutEdges.vue +27 -23
  119. package/src/runtime/components/LayoutFrame.vue +6 -5
  120. package/src/runtime/components/LayoutShapeSquare.vue +9 -3
  121. package/src/runtime/components/LayoutWindow.vue +110 -101
  122. package/src/runtime/composables/useFrames.ts +80 -50
  123. package/src/runtime/demo/App.vue +126 -36
  124. package/src/runtime/demo/DemoControls.vue +115 -6
  125. package/src/runtime/drag/CloseAction.ts +106 -44
  126. package/src/runtime/drag/DragActionHandler.ts +71 -20
  127. package/src/runtime/drag/FrameDragAction.ts +202 -0
  128. package/src/runtime/drag/SplitAction.ts +106 -34
  129. package/src/runtime/drag/createDefaultHandlers.ts +19 -0
  130. package/src/runtime/drag/defaultDragActions.ts +19 -0
  131. package/src/runtime/drag/types.ts +90 -20
  132. package/src/runtime/helpers/createZoneSideClipPath.ts +41 -0
  133. package/src/runtime/helpers/doEdgesOverlap.ts +11 -5
  134. package/src/runtime/helpers/getDockBoundaries.ts +36 -0
  135. package/src/runtime/helpers/getEdgeLength.ts +10 -0
  136. package/src/runtime/helpers/getIntersections.ts +2 -2
  137. package/src/runtime/helpers/getIntersectionsCss.ts +2 -2
  138. package/src/runtime/helpers/getMoveEdgeInfo.ts +2 -2
  139. package/src/runtime/helpers/getResizeLimit.ts +2 -2
  140. package/src/runtime/helpers/getShapeSquareCss.ts +2 -2
  141. package/src/runtime/helpers/getVisualEdgeCss.ts +2 -2
  142. package/src/runtime/helpers/getVisualEdges.ts +5 -4
  143. package/src/runtime/helpers/index.ts +4 -0
  144. package/src/runtime/helpers/isEdgeEqual.ts +2 -4
  145. package/src/runtime/helpers/isWindowEdge.ts +2 -2
  146. package/src/runtime/helpers/isWindowEdgePoint.ts +2 -2
  147. package/src/runtime/helpers/moveEdge.ts +2 -2
  148. package/src/runtime/helpers/numberToScaledPercent.ts +3 -3
  149. package/src/runtime/helpers/numberToScaledSize.ts +2 -2
  150. package/src/runtime/helpers/rotateFrames.ts +45 -0
  151. package/src/runtime/helpers/scaledPointToPx.ts +13 -0
  152. package/src/runtime/helpers/toWindowCoord.ts +2 -2
  153. package/src/runtime/layout/applyFrameChanges.ts +39 -0
  154. package/src/runtime/layout/createSplitDecoFromDrag.ts +12 -6
  155. package/src/runtime/layout/{createSplitDecoEdge.ts → createSplitDecoShapes.ts} +17 -7
  156. package/src/runtime/layout/debugFrame.ts +1 -1
  157. package/src/runtime/layout/findSafeSplitEdge.ts +3 -3
  158. package/src/runtime/layout/frameCreate.ts +2 -2
  159. package/src/runtime/layout/getCloseFrameInfo.ts +21 -8
  160. package/src/runtime/layout/getDragZones.ts +48 -0
  161. package/src/runtime/layout/getFillEmptySpaceInfo.ts +177 -0
  162. package/src/runtime/layout/getFrameCollapseInfo.ts +164 -0
  163. package/src/runtime/layout/getFrameDockInfo.ts +126 -0
  164. package/src/runtime/layout/getFrameDragZones.ts +100 -0
  165. package/src/runtime/layout/getFrameRearrangeInfo.ts +261 -0
  166. package/src/runtime/layout/getFrameSplitInfo.ts +21 -8
  167. package/src/runtime/layout/getFrameSwapInfo.ts +45 -0
  168. package/src/runtime/layout/getFrameTo.ts +2 -2
  169. package/src/runtime/layout/getFrameUncollapseInfo.ts +160 -0
  170. package/src/runtime/layout/getFrameUndockInfo.ts +97 -0
  171. package/src/runtime/layout/getFramesRedistributeInfo.ts +98 -0
  172. package/src/runtime/layout/getWindowDragZones.ts +59 -0
  173. package/src/runtime/layout/index.ts +14 -5
  174. package/src/runtime/layout/isPointInRect.ts +7 -0
  175. package/src/runtime/layout/resizeFrame.ts +2 -2
  176. package/src/runtime/settings.ts +69 -49
  177. package/src/runtime/types/index.ts +143 -28
  178. package/dist/runtime/layout/closeFrame.d.ts +0 -5
  179. package/dist/runtime/layout/closeFrame.js +0 -13
  180. package/dist/runtime/layout/closeFrames.d.ts +0 -2
  181. package/dist/runtime/layout/closeFrames.js +0 -8
  182. package/dist/runtime/layout/createSplitDecoEdge.d.ts +0 -2
  183. package/dist/runtime/layout/frameSplit.d.ts +0 -16
  184. package/dist/runtime/layout/frameSplit.js +0 -9
  185. package/dist/runtime/layout/isPointInFrame.d.ts +0 -2
  186. package/src/runtime/layout/closeFrame.ts +0 -33
  187. package/src/runtime/layout/closeFrames.ts +0 -14
  188. package/src/runtime/layout/frameSplit.ts +0 -31
  189. package/src/runtime/layout/isPointInFrame.ts +0 -7
@@ -1,7 +1,7 @@
1
- import { createSplitDecoEdge } from "./createSplitDecoEdge.js"
1
+ import { createSplitDecoShapes } from "./createSplitDecoShapes.js"
2
2
 
3
3
  import { dirToOrientation } from "../helpers/dirToOrientation.js"
4
- import { getMarginSize, getSnapPoint } from "../settings.js"
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 = getSnapPoint(),
13
- minSize: Size = getMarginSize()
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
- ;(deco as SplitDeco).shapes = createSplitDecoEdge(frames, deco, snapAmount, minSize)
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 { getMarginSize, getSnapPoint } from "../settings.js"
4
- import type { LayoutFrame, Point, RawSplitDeco, Size, SplitDecoShapes } from "../types/index.js"
3
+ import { settings } from "../settings.js"
4
+ import type { LayoutFrame, LayoutShape, Point, RawSplitDeco, Size } from "../types/index.js"
5
5
 
6
- export function createSplitDecoEdge(
6
+ export function createSplitDecoShapes(
7
7
  frames: Record<string, LayoutFrame>,
8
8
  deco: RawSplitDeco,
9
- snapAmount: Point = getSnapPoint(),
10
- minSize: Size = getMarginSize()
11
- ): SplitDecoShapes {
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 { edge, newFrame }
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 { getMarginSize, getSnapPoint } from "../settings.js"
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 = getSnapPoint(),
13
- minSize: Size = getMarginSize()
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 { getMaxInt } from "../settings.js"
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 = getMaxInt()
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 { getMarginSize } from "../settings.js"
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 = getMarginSize()
92
- ): { modifiedFrames: LayoutFrame[], deletedFrames: LayoutFrame[] }
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
+ }