@witchcraft/layout 0.3.1 → 0.4.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.
Files changed (169) hide show
  1. package/README.md +62 -70
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +6 -1
  4. package/dist/runtime/components/FrameDragHandle.vue +1 -1
  5. package/dist/runtime/components/LayoutDecosEdges.d.vue.ts +15 -0
  6. package/dist/runtime/components/LayoutDecosEdges.vue +34 -0
  7. package/dist/runtime/components/LayoutDecosEdges.vue.d.ts +15 -0
  8. package/dist/runtime/components/LayoutDecosRects.d.vue.ts +15 -0
  9. package/dist/runtime/components/LayoutDecosRects.vue +37 -0
  10. package/dist/runtime/components/LayoutDecosRects.vue.d.ts +15 -0
  11. package/dist/runtime/components/LayoutDragEdgeGrabbed.d.vue.ts +2 -0
  12. package/dist/runtime/components/LayoutDragEdgeGrabbed.vue +43 -0
  13. package/dist/runtime/components/LayoutDragEdgeGrabbed.vue.d.ts +2 -0
  14. package/dist/runtime/components/LayoutDragEdgeHandle.d.vue.ts +9 -0
  15. package/dist/runtime/components/LayoutDragEdgeHandle.vue +44 -0
  16. package/dist/runtime/components/LayoutDragEdgeHandle.vue.d.ts +9 -0
  17. package/dist/runtime/components/{LayoutDecos.d.vue.ts → LayoutDragEdgeVisible.d.vue.ts} +3 -2
  18. package/dist/runtime/components/LayoutDragEdgeVisible.vue +38 -0
  19. package/dist/runtime/components/{LayoutDecos.vue.d.ts → LayoutDragEdgeVisible.vue.d.ts} +3 -2
  20. package/dist/runtime/components/LayoutDragEdges.d.vue.ts +21 -0
  21. package/dist/runtime/components/LayoutDragEdges.vue +67 -0
  22. package/dist/runtime/components/LayoutDragEdges.vue.d.ts +21 -0
  23. package/dist/runtime/components/LayoutEdgesActiveFrame.d.vue.ts +2 -0
  24. package/dist/runtime/components/LayoutEdgesActiveFrame.vue +35 -0
  25. package/dist/runtime/components/LayoutEdgesActiveFrame.vue.d.ts +2 -0
  26. package/dist/runtime/components/LayoutFrame.d.vue.ts +7 -6
  27. package/dist/runtime/components/LayoutFrame.vue +18 -14
  28. package/dist/runtime/components/LayoutFrame.vue.d.ts +7 -6
  29. package/dist/runtime/components/LayoutIntersectionHandle.d.vue.ts +10 -0
  30. package/dist/runtime/components/LayoutIntersectionHandle.vue +36 -0
  31. package/dist/runtime/components/LayoutIntersectionHandle.vue.d.ts +10 -0
  32. package/dist/runtime/components/LayoutIntersectionVisible.d.vue.ts +6 -0
  33. package/dist/runtime/components/LayoutIntersectionVisible.vue +34 -0
  34. package/dist/runtime/components/LayoutIntersectionVisible.vue.d.ts +6 -0
  35. package/dist/runtime/components/LayoutIntersections.d.vue.ts +21 -0
  36. package/dist/runtime/components/LayoutIntersections.vue +52 -0
  37. package/dist/runtime/components/LayoutIntersections.vue.d.ts +21 -0
  38. package/dist/runtime/components/{LayoutShapeSquare.d.vue.ts → LayoutShapeRect.d.vue.ts} +2 -2
  39. package/dist/runtime/components/{LayoutShapeSquare.vue.d.ts → LayoutShapeRect.vue.d.ts} +2 -2
  40. package/dist/runtime/components/LayoutWindow.d.vue.ts +252 -14
  41. package/dist/runtime/components/LayoutWindow.vue +27 -48
  42. package/dist/runtime/components/LayoutWindow.vue.d.ts +252 -14
  43. package/dist/runtime/composables/useFrames.d.ts +859 -2
  44. package/dist/runtime/composables/useFrames.js +2 -0
  45. package/dist/runtime/demo/App.vue +122 -74
  46. package/dist/runtime/demo/DemoControls.vue +7 -5
  47. package/dist/runtime/demo/index.html +12 -0
  48. package/dist/runtime/demo/tailwind.css +1 -1
  49. package/dist/runtime/drag/CloseAction.d.ts +1 -2
  50. package/dist/runtime/drag/CloseAction.js +1 -1
  51. package/dist/runtime/drag/DragActionHandler.d.ts +5 -3
  52. package/dist/runtime/drag/DragActionHandler.js +40 -6
  53. package/dist/runtime/drag/FrameDragAction.js +6 -7
  54. package/dist/runtime/drag/SplitAction.d.ts +1 -2
  55. package/dist/runtime/drag/SplitAction.js +1 -1
  56. package/dist/runtime/drag/createDefaultHandlers.d.ts +1 -1
  57. package/dist/runtime/drag/defaultDragActions.d.ts +1 -1
  58. package/dist/runtime/helpers/consoleDebugWindow.d.ts +43 -0
  59. package/dist/runtime/helpers/consoleDebugWindow.js +222 -0
  60. package/dist/runtime/helpers/convertLayoutWindowToWorkspace.js +2 -2
  61. package/dist/runtime/helpers/framesRedistributeFix.d.ts +87 -0
  62. package/dist/runtime/helpers/framesRedistributeFix.js +54 -0
  63. package/dist/runtime/helpers/getMoveEdgeInfo.d.ts +6 -2
  64. package/dist/runtime/helpers/getMoveEdgeInfo.js +11 -2
  65. package/dist/runtime/helpers/getPinnedEdgesForCollapsedFrames.d.ts +5 -0
  66. package/dist/runtime/helpers/getPinnedEdgesForCollapsedFrames.js +18 -0
  67. package/dist/runtime/helpers/getShapeRectCss.d.ts +7 -0
  68. package/dist/runtime/helpers/{getShapeSquareCss.js → getShapeRectCss.js} +1 -1
  69. package/dist/runtime/helpers/index.d.ts +3 -3
  70. package/dist/runtime/helpers/index.js +3 -3
  71. package/dist/runtime/helpers/moveEdge.js +3 -1
  72. package/dist/runtime/helpers/pxSizeToScaledSize.d.ts +2 -0
  73. package/dist/runtime/helpers/{numberToScaledSize.js → pxSizeToScaledSize.js} +1 -1
  74. package/dist/runtime/helpers/updateWindowSizeWithEvent.d.ts +3 -0
  75. package/dist/runtime/helpers/validateLayoutShape.d.ts +5 -0
  76. package/dist/runtime/helpers/validateLayoutShape.js +28 -0
  77. package/dist/runtime/layout/applyFrameChanges.js +6 -0
  78. package/dist/runtime/layout/createSplitDecoShapes.js +1 -1
  79. package/dist/runtime/layout/getFrameCollapseInfo.d.ts +5 -2
  80. package/dist/runtime/layout/getFrameCollapseInfo.js +16 -46
  81. package/dist/runtime/layout/getFrameDockInfo.d.ts +1 -1
  82. package/dist/runtime/layout/getFrameDockInfo.js +5 -1
  83. package/dist/runtime/layout/getFrameUncollapseInfo.d.ts +5 -2
  84. package/dist/runtime/layout/getFrameUncollapseInfo.js +16 -50
  85. package/dist/runtime/layout/getFrameUndockInfo.js +4 -1
  86. package/dist/runtime/layout/getFramesRedistributeInfo.d.ts +20 -4
  87. package/dist/runtime/layout/getFramesRedistributeInfo.js +31 -2
  88. package/dist/runtime/layout/getRelaxFramesInfo.d.ts +7 -0
  89. package/dist/runtime/layout/getRelaxFramesInfo.js +91 -0
  90. package/dist/runtime/layout/getUpdateWindowSizeInfo.d.ts +13 -0
  91. package/dist/runtime/layout/getUpdateWindowSizeInfo.js +65 -0
  92. package/dist/runtime/layout/index.d.ts +1 -0
  93. package/dist/runtime/layout/index.js +1 -0
  94. package/dist/runtime/layout/resizeFrame.js +1 -3
  95. package/dist/runtime/settings.d.ts +26 -19
  96. package/dist/runtime/settings.js +57 -22
  97. package/dist/runtime/types/index.d.ts +1243 -30
  98. package/dist/runtime/types/index.js +22 -8
  99. package/dist/runtime/types/vue.d.ts +41 -0
  100. package/dist/runtime/{drag/types.js → types/vue.js} +1 -0
  101. package/dist/runtime/utils/cssToKey.d.ts +5 -0
  102. package/dist/runtime/utils/cssToKey.js +3 -0
  103. package/package.json +11 -12
  104. package/src/module.ts +6 -1
  105. package/src/runtime/components/FrameDragHandle.vue +1 -1
  106. package/src/runtime/components/LayoutDecosEdges.vue +38 -0
  107. package/src/runtime/components/LayoutDecosRects.vue +42 -0
  108. package/src/runtime/components/LayoutDragEdgeGrabbed.vue +50 -0
  109. package/src/runtime/components/LayoutDragEdgeHandle.vue +52 -0
  110. package/src/runtime/components/LayoutDragEdgeVisible.vue +45 -0
  111. package/src/runtime/components/LayoutDragEdges.vue +78 -0
  112. package/src/runtime/components/LayoutEdgesActiveFrame.vue +43 -0
  113. package/src/runtime/components/LayoutFrame.vue +22 -15
  114. package/src/runtime/components/LayoutIntersectionHandle.vue +45 -0
  115. package/src/runtime/components/LayoutIntersectionVisible.vue +41 -0
  116. package/src/runtime/components/LayoutIntersections.vue +64 -0
  117. package/src/runtime/components/{LayoutShapeSquare.vue → LayoutShapeRect.vue} +2 -2
  118. package/src/runtime/components/LayoutWindow.vue +29 -49
  119. package/src/runtime/composables/useFrames.ts +7 -2
  120. package/src/runtime/demo/App.vue +128 -78
  121. package/src/runtime/demo/DemoControls.vue +7 -5
  122. package/src/runtime/demo/index.html +12 -0
  123. package/src/runtime/demo/tailwind.css +3 -0
  124. package/src/runtime/drag/CloseAction.ts +2 -3
  125. package/src/runtime/drag/DragActionHandler.ts +48 -10
  126. package/src/runtime/drag/FrameDragAction.ts +4 -5
  127. package/src/runtime/drag/SplitAction.ts +4 -3
  128. package/src/runtime/drag/createDefaultHandlers.ts +2 -1
  129. package/src/runtime/drag/defaultDragActions.ts +2 -1
  130. package/src/runtime/helpers/consoleDebugWindow.ts +331 -0
  131. package/src/runtime/helpers/convertLayoutWindowToWorkspace.ts +2 -2
  132. package/src/runtime/helpers/framesRedistributeFix.ts +162 -0
  133. package/src/runtime/helpers/getMoveEdgeInfo.ts +22 -5
  134. package/src/runtime/helpers/getPinnedEdgesForCollapsedFrames.ts +29 -0
  135. package/src/runtime/helpers/{getShapeSquareCss.ts → getShapeRectCss.ts} +3 -3
  136. package/src/runtime/helpers/index.ts +4 -3
  137. package/src/runtime/helpers/moveEdge.ts +3 -1
  138. package/src/runtime/helpers/{numberToScaledSize.ts → pxSizeToScaledSize.ts} +2 -2
  139. package/src/runtime/helpers/updateWindowSizeWithEvent.ts +3 -0
  140. package/src/runtime/helpers/validateLayoutShape.ts +46 -0
  141. package/src/runtime/layout/applyFrameChanges.ts +5 -0
  142. package/src/runtime/layout/createSplitDecoShapes.ts +1 -1
  143. package/src/runtime/layout/getFrameCollapseInfo.ts +26 -85
  144. package/src/runtime/layout/getFrameDockInfo.ts +8 -1
  145. package/src/runtime/layout/getFrameUncollapseInfo.ts +29 -96
  146. package/src/runtime/layout/getFrameUndockInfo.ts +7 -1
  147. package/src/runtime/layout/getFramesRedistributeInfo.ts +60 -5
  148. package/src/runtime/layout/getRelaxFramesInfo.ts +139 -0
  149. package/src/runtime/layout/getUpdateWindowSizeInfo.ts +105 -0
  150. package/src/runtime/layout/index.ts +1 -0
  151. package/src/runtime/layout/resizeFrame.ts +4 -3
  152. package/src/runtime/settings.ts +78 -25
  153. package/src/runtime/types/index.ts +205 -33
  154. package/src/runtime/types/vue.ts +41 -0
  155. package/src/runtime/utils/cssToKey.ts +8 -0
  156. package/dist/runtime/components/LayoutDecos.vue +0 -26
  157. package/dist/runtime/components/LayoutEdges.d.vue.ts +0 -17
  158. package/dist/runtime/components/LayoutEdges.vue +0 -144
  159. package/dist/runtime/components/LayoutEdges.vue.d.ts +0 -17
  160. package/dist/runtime/drag/types.d.ts +0 -161
  161. package/dist/runtime/helpers/clampNumber.d.ts +0 -1
  162. package/dist/runtime/helpers/clampNumber.js +0 -3
  163. package/dist/runtime/helpers/getShapeSquareCss.d.ts +0 -7
  164. package/dist/runtime/helpers/numberToScaledSize.d.ts +0 -2
  165. package/src/runtime/components/LayoutDecos.vue +0 -28
  166. package/src/runtime/components/LayoutEdges.vue +0 -170
  167. package/src/runtime/drag/types.ts +0 -177
  168. package/src/runtime/helpers/clampNumber.ts +0 -9
  169. /package/dist/runtime/components/{LayoutShapeSquare.vue → LayoutShapeRect.vue} +0 -0
@@ -0,0 +1,162 @@
1
+ import { oppositeSide } from "./oppositeSide.js"
2
+
3
+ import { settings } from "../settings.js"
4
+ import type { EdgeSide, LayoutFrame, LayoutWindow } from "../types/index.js"
5
+
6
+ /**
7
+ * Prepares frames that need to be temporarily repositioned before redistribution
8
+ * and returns functions to apply/revert the fixes after redistribution. Used mostly internally for collapse/uncollapse of docked frames and similar operations.
9
+ *
10
+ * It exists because if you try to resize a layout like this to the right, you will get issues with frame edges not lining up as frame B would be resized less than A. If you had a frame at the empty space that you excluded, it's right edge would no longer align with B's.
11
+ *
12
+ * This helper takes frames like A, moves the edge so it's shared then moved them back. Just excluding them from the calcuation usually leads to subtler errors. Such as if there are frames after A and B to the right, excluding A would mean A's right edge would stop aligning with those other frames.
13
+ *
14
+ * ┌────────────┐
15
+ * │A │
16
+ * └──┬─────────┤
17
+ * │B │
18
+ * └─────────┘
19
+ *
20
+ * It has two modes, shrink and expand meant for collapse/uncollapse operations.
21
+ *
22
+ * ## Expand
23
+ *
24
+ * When multiple frames are docked* they may share an edge like this:
25
+ * For example, if we collapse A, we need to allow everything else to expand,
26
+ * but if we expand D it will overflow the bounds of the window and we'd get an error.
27
+ * To avoid this, we shrink these frames towards the opposite side first.
28
+ *
29
+ * collapsed size/amount
30
+ * ├──┘
31
+ * │ shrink amount
32
+ * │ ├───┘
33
+ * ├──────┬───────────┐
34
+ * │A*│ │B* │
35
+ * │ ├──────┬────┤
36
+ * │ │ │E │C* │
37
+ * │ │ │ │
38
+ * ├──┴───┴──────┤ │
39
+ * │D* │ │
40
+ * └──────┬──────┴────┘
41
+ * │we would shrink D to here
42
+ *
43
+ * D.x = A.x + A.width, and then subtract the difference from it's width
44
+ *
45
+ * ***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.
46
+ *
47
+ * When redistribute expands it again by shrinkAmount it will end up at A.x + collapseSize.
48
+ *
49
+ * This works fine when collapseSize is 0, but otherwise breaks so we keep a list of
50
+ * 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).
51
+ *
52
+ * ## Shrink
53
+ *
54
+ * Mirrors collapse, the only difference being we can skip the fixes if the frame was fully collapsed to 0.
55
+ *
56
+ * Why is this? Well take this example where A is docked and collapsed and B is docked, in the following two cases we would have no problems expanding as B ends where A begins, but if B shares the edge and A is not fully collapsed it is no longer aligned with the "end" of A.
57
+ *
58
+ * Ok:
59
+ * A - fully collapsed to 0
60
+ * ~ ┌──────────┐
61
+ * │ │B* │
62
+ * │ ├──────────┤
63
+ * │ │ │
64
+ * │ │ │
65
+ * │ └──────────┘
66
+ *
67
+ * Ok:
68
+ * A collapsed to non-zero size
69
+ * Note B still shares A's right edge
70
+ * ┌──┬─────────┐
71
+ * │A~│B* │
72
+ * │ ├─────────┤
73
+ * │ │ │
74
+ * │ │ │
75
+ * └──┴─────────┘
76
+ *
77
+ * Causes issues:
78
+ * A collapsed to non-zero size with B docked first
79
+ * B looses alignment with A causing issues when redistributing
80
+ * ┌────────────┐
81
+ * │B* │
82
+ * ├──┬─────────┤
83
+ * │A~│ │
84
+ * │ │ │
85
+ * └──┴─────────┘
86
+ *
87
+ */
88
+
89
+ export function framesRedistributeFix(
90
+ win: LayoutWindow,
91
+ frame: LayoutFrame,
92
+ dockedSide: EdgeSide,
93
+ posKey: "x" | "y",
94
+ sizeKey: "width" | "height",
95
+ mode: "shrink" | "expand" = "shrink"
96
+ ): {
97
+ applyFixes: () => void
98
+ toFix: string[]
99
+ } {
100
+ const framesToFix: ({
101
+ id: string
102
+ posKey: "x" | "y"
103
+ sizeKey: "width" | "height"
104
+ type: "start" | "end"
105
+ })[] = []
106
+
107
+ const opposite = oppositeSide(dockedSide)
108
+
109
+ // expand skips if frame was fully collapsed to 0
110
+ if (mode === "expand" && frame[sizeKey] === 0) {
111
+ return {
112
+ applyFixes: () => {},
113
+ toFix: []
114
+ }
115
+ }
116
+
117
+ for (const other of Object.values(win.frames)) {
118
+ if (frame.id === other.id || !other.docked) continue
119
+
120
+ // expand skips zero-area frames
121
+ if (mode === "expand" && (other.width === 0 || other.height === 0)) continue
122
+
123
+ if (other.docked === opposite) continue
124
+ if (dockedSide === "left" || dockedSide === "top") {
125
+ if (other[posKey] !== 0) continue
126
+ other[posKey] = frame[posKey] + frame[sizeKey]
127
+ other[sizeKey] -= frame[sizeKey]
128
+ framesToFix.push({
129
+ id: other.id,
130
+ posKey,
131
+ sizeKey,
132
+ type: "start"
133
+ })
134
+ } else {
135
+ if (other[posKey] + other[sizeKey] !== settings.maxInt) continue
136
+ other[sizeKey] -= frame[sizeKey]
137
+ framesToFix.push({
138
+ id: other.id,
139
+ posKey,
140
+ sizeKey,
141
+ type: "end"
142
+ })
143
+ }
144
+ }
145
+
146
+ return {
147
+ applyFixes: () => {
148
+ for (const fix of framesToFix) {
149
+ const other = win.frames[fix.id]
150
+ if (fix.type === "start") {
151
+ const sizeDiff = other[fix.posKey]
152
+ other[fix.posKey] = 0
153
+ other[fix.sizeKey] += sizeDiff
154
+ } else {
155
+ const sizeDiff = settings.maxInt - (other[fix.posKey] + other[fix.sizeKey])
156
+ other[fix.sizeKey] += sizeDiff
157
+ }
158
+ }
159
+ },
160
+ toFix: framesToFix.map(f => f.id)
161
+ }
162
+ }
@@ -1,13 +1,18 @@
1
- import { clampNumber } from "./clampNumber.js"
1
+ import { clampNumber } from "@alanscodelog/utils/clampNumber"
2
+
2
3
  import { getEdgeOrientation } from "./getEdgeOrientation.js"
3
4
  import { getResizeLimit } from "./getResizeLimit.js"
4
5
 
5
6
  import { settings } from "../settings.js"
6
7
  import type {
7
8
  Direction,
8
- Edge, LayoutFrame,
9
+ Edge,
10
+ LayoutFrame,
9
11
  Point,
10
- Size } from "../types/index.js"
12
+ Size
13
+ } from "../types/index.js"
14
+ import { LAYOUT_ERROR } from "../types/index.js"
15
+ import { KnownError } from "../utils/KnownError.js"
11
16
 
12
17
  export function getMoveEdgeInfo(
13
18
  touchingFrames: LayoutFrame[],
@@ -15,7 +20,9 @@ export function getMoveEdgeInfo(
15
20
  /** Window scaled/snaped position. See {@link toWindowCoord} */
16
21
  position: Point,
17
22
  margin: Size = settings.minSizeScaled,
18
- clamp = true
23
+ clamp = true,
24
+ /** Skip the collapsed frame check. Used internally during window resize adjustments. */
25
+ ignoreCollapsedCheck = false
19
26
  ): {
20
27
  x: number
21
28
  y: number
@@ -23,7 +30,17 @@ export function getMoveEdgeInfo(
23
30
  isPassedLimit: boolean
24
31
  pos: number
25
32
  distance: number
26
- } {
33
+ } | KnownError<typeof LAYOUT_ERROR.CANT_RESIZE_COLLAPSED_FRAME> {
34
+ // prevent moving edges that touch collapsed frames
35
+ if (!ignoreCollapsedCheck) {
36
+ for (const frame of touchingFrames) {
37
+ // we check with a falsy check on PURPOSE, frames collapsed to 0 aren't an issue, they are ignored anyways
38
+ if (frame.collapsed) {
39
+ return new KnownError(LAYOUT_ERROR.CANT_RESIZE_COLLAPSED_FRAME, `Cannot resize non-0 collapsed frame ${frame.id}.`, { frame })
40
+ }
41
+ }
42
+ }
43
+
27
44
  const { x: posX, y: posY } = position
28
45
 
29
46
  const edgeDirection = getEdgeOrientation(edge)
@@ -0,0 +1,29 @@
1
+ export function getPinnedEdgesForCollapsedFrames(
2
+ win: { frames: Record<string, any> },
3
+ frame: { id: string },
4
+ dockedSide: string,
5
+ posKey: string,
6
+ sizeKey: string
7
+ ): number[] {
8
+ const pinnedEdgeCoordinates: number[] = []
9
+ const isHorizontal = dockedSide === "left" || dockedSide === "right"
10
+
11
+ for (const other of Object.values(win.frames)) {
12
+ if (frame.id === other.id || !other.collapsed) continue
13
+
14
+ const otherIsHorizontal = other.docked === "left" || other.docked === "right"
15
+
16
+ // only pin collapsed frames in the same direction
17
+ if (isHorizontal === otherIsHorizontal) {
18
+ if (other.docked === "left" || other.docked === "top") {
19
+ const edge = other[posKey] + other[sizeKey]
20
+ pinnedEdgeCoordinates.push(edge)
21
+ } else {
22
+ const edge = other[posKey]
23
+ pinnedEdgeCoordinates.push(edge)
24
+ }
25
+ }
26
+ }
27
+
28
+ return pinnedEdgeCoordinates
29
+ }
@@ -1,8 +1,8 @@
1
1
  import { settings } from "../settings.js"
2
- import type { BaseSquare } from "../types/index.js"
2
+ import type { BaseRect } from "../types/index.js"
3
3
 
4
- export function getShapeSquareCss(
5
- obj: BaseSquare,
4
+ export function getShapeRectCss(
5
+ obj: BaseRect,
6
6
  pad?: string
7
7
  ): {
8
8
  x: string
@@ -1,3 +1,5 @@
1
+ export { clampNumber } from "@alanscodelog/utils/clampNumber"
2
+
1
3
  /* Autogenerated Index */
2
4
 
3
5
  export { addPointsToIntersection } from "./addPointsToIntersection.js"
@@ -8,7 +10,6 @@ export { assertLayoutHasActiveWindow } from "./assertLayoutHasActiveWindow.js"
8
10
  export { assertValidWinAndFrame } from "./assertValidWinAndFrame.js"
9
11
  export { assertValidWinAndFrameIds } from "./assertValidWinAndFrameIds.js"
10
12
  export { assertWindowHasActiveFrame } from "./assertWindowHasActiveFrame.js"
11
- export { clampNumber } from "./clampNumber.js"
12
13
  export { cloneFrame } from "./cloneFrame.js"
13
14
  export { cloneFrames } from "./cloneFrames.js"
14
15
  export { containsEdge } from "./containsEdge.js"
@@ -35,7 +36,7 @@ export { getIntersections } from "./getIntersections.js"
35
36
  export { getIntersectionsCss } from "./getIntersectionsCss.js"
36
37
  export { getMoveEdgeInfo } from "./getMoveEdgeInfo.js"
37
38
  export { getResizeLimit } from "./getResizeLimit.js"
38
- export { getShapeSquareCss } from "./getShapeSquareCss.js"
39
+ export { getShapeRectCss } from "./getShapeRectCss.js"
39
40
  export { getSideTouching } from "./getSideTouching.js"
40
41
  export { getVisualEdgeCss } from "./getVisualEdgeCss.js"
41
42
  export { getVisualEdges } from "./getVisualEdges.js"
@@ -54,7 +55,7 @@ export { isWindowEdge } from "./isWindowEdge.js"
54
55
  export { isWindowEdgePoint } from "./isWindowEdgePoint.js"
55
56
  export { moveEdge } from "./moveEdge.js"
56
57
  export { numberToScaledPercent } from "./numberToScaledPercent.js"
57
- export { numberToScaledSize } from "./numberToScaledSize.js"
58
+ export { pxSizeToScaledSize } from "./pxSizeToScaledSize.js"
58
59
  export { oppositeSide } from "./oppositeSide.js"
59
60
  export { resizeByEdge } from "./resizeByEdge.js"
60
61
  export { scaledPointToPx } from "./scaledPointToPx.js"
@@ -16,6 +16,8 @@ export function moveEdge(
16
16
  margin: Size = settings.minSizeScaled
17
17
  ): void {
18
18
  if (!edge || !touchingFrames) return
19
- const { pos, dir, distance } = getMoveEdgeInfo(touchingFrames, edge, position, margin)
19
+ const result = getMoveEdgeInfo(touchingFrames, edge, position, margin)
20
+ if (result instanceof Error) return
21
+ const { pos, dir, distance } = result
20
22
  resizeByEdge(touchingFrames, edge, dir, pos, distance)
21
23
  }
@@ -5,8 +5,8 @@ import { numberToScaledPercent } from "./numberToScaledPercent.js"
5
5
  import { settings } from "../settings.js"
6
6
  import type { LayoutWindow, PxSize, Size } from "../types/index.js"
7
7
 
8
- export function numberToScaledSize(
9
- win: LayoutWindow,
8
+ export function pxSizeToScaledSize(
9
+ win: Pick<LayoutWindow, "pxWidth" | "pxHeight">,
10
10
  size: PxSize | number,
11
11
  scale: number = settings.maxInt
12
12
  ): Size {
@@ -1,6 +1,9 @@
1
1
  import type { LayoutWindow, PxPos, PxSize } from "../types/index.js"
2
2
 
3
3
  const props = ["pxWidth", "pxHeight", "pxX", "pxY"] as const
4
+ /**
5
+ * See also the experimental {@link getUpdateWindowSizeInfo} for the function that will likely replace this.
6
+ */
4
7
  export function updateWindowWithEvent(win: LayoutWindow, event: PxPos & PxSize): void {
5
8
  for (const prop of props) {
6
9
  if (event[prop] && win[prop] !== event[prop]) {
@@ -0,0 +1,46 @@
1
+ import type { LayoutFrame } from "../types/index.js"
2
+
3
+ /**
4
+ * For testing, validates the resulting layout is valid (frames do not overlap, have no gaps between them and form a rectangular area.
5
+ */
6
+ export function validateLayoutShape(frames: Pick<LayoutFrame, "x" | "y" | "width" | "height">[]): boolean {
7
+ const n = frames.length
8
+ if (n === 0) return false
9
+
10
+ // check area mismatch (non-rectangular, or has gaps)
11
+ let totalArea = 0
12
+ let minX = Infinity, minY = Infinity
13
+ let maxX = -Infinity, maxY = -Infinity
14
+
15
+ for (const f of frames) {
16
+ totalArea += f.width * f.height
17
+ minX = Math.min(minX, f.x)
18
+ minY = Math.min(minY, f.y)
19
+ maxX = Math.max(maxX, f.x + f.width)
20
+ maxY = Math.max(maxY, f.y + f.height)
21
+ }
22
+
23
+ const boundingArea = (maxX - minX) * (maxY - minY)
24
+
25
+ if (Math.abs(totalArea - boundingArea) > 0.001) {
26
+ return false
27
+ }
28
+
29
+ // check overlaps
30
+ for (let i = 0; i < n; i++) {
31
+ for (let j = i + 1; j < n; j++) {
32
+ const a = frames[i]
33
+ const b = frames[j]
34
+
35
+
36
+ if (a.x < b.x + b.width
37
+ && a.x + a.width > b.x
38
+ && a.y < b.y + b.height
39
+ && a.y + a.height > b.y) {
40
+ return false
41
+ }
42
+ }
43
+ }
44
+
45
+ return true
46
+ }
@@ -36,4 +36,9 @@ export function applyFrameChanges(
36
36
  for (const frame of change.deleted) {
37
37
  delete win.frames[frame.id]
38
38
  }
39
+ if (change.window) {
40
+ for (const [key, value] of Object.entries(change.window)) {
41
+ ;(win as any)[key] = value as any
42
+ }
43
+ }
39
44
  }
@@ -39,6 +39,6 @@ export function createSplitDecoShapes(
39
39
 
40
40
  return [
41
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" } }
42
+ { type: "rect", data: newFrame, attrs: { class: classes.splitNewFrame ?? "deco-split-new-frame bg-blue-500/50" } }
43
43
  ]
44
44
  }
@@ -4,6 +4,8 @@ import { walk } from "@alanscodelog/utils/walk"
4
4
  import { applyFrameChanges } from "./applyFrameChanges.js"
5
5
  import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
6
6
 
7
+ import { framesRedistributeFix } from "../helpers/framesRedistributeFix.js"
8
+ import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
7
9
  import { oppositeSide } from "../helpers/oppositeSide.js"
8
10
  import { settings } from "../settings.js"
9
11
  import type { EdgeSide, LayoutChange, LayoutWindow, Size } from "../types/index.js"
@@ -21,9 +23,17 @@ import { KnownError } from "../utils/KnownError.js"
21
23
  */
22
24
  export function getFrameCollapseInfo(
23
25
  win: LayoutWindow,
24
- frameId: string
26
+ frameId: string,
27
+ {
28
+ collapseSizeScaled
29
+ }: {
30
+ /** Override the target collapse size. If provided, the frame will collapse to this size instead of settings.collapseSizeScaled. */
31
+ collapseSizeScaled?: Size
32
+ } = {}
25
33
  ): LayoutChange
26
- | KnownError<typeof LAYOUT_ERROR.CANT_COLLAPSE_NOT_DOCKED> {
34
+ | KnownError<typeof LAYOUT_ERROR.CANT_COLLAPSE_NOT_DOCKED>
35
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES> {
36
+ collapseSizeScaled = collapseSizeScaled ?? settings.getCollapseSizeScaled(win)
27
37
  win = walk(win, undefined, { save: true }) as typeof win
28
38
  const frame = win.frames[frameId]
29
39
  if (!frame) throw new Error(`Unknown frame ${frameId}`)
@@ -45,120 +55,51 @@ export function getFrameCollapseInfo(
45
55
  const oppositePosKey = isVertical ? "y" : "x"
46
56
  const oppositeSizeKey = isVertical ? "height" : "width"
47
57
 
48
- const collapseSize: Size = settings.collapseSizeScaled
49
58
 
50
59
  const currentSize = frame[sizeKey]
51
- const collapseAmount = collapseSize[sizeKey]
60
+ const collapseAmount = collapseSizeScaled[sizeKey]
52
61
  const shrinkAmount = currentSize - collapseAmount
53
62
 
54
63
  const dockedSide = frame.docked as EdgeSide
55
64
 
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
- }
65
+ const { applyFixes, toFix } = framesRedistributeFix(win, frame, dockedSide, posKey, sizeKey, "shrink")
119
66
 
120
67
  // note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
121
68
  const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
122
69
 
123
70
  const redistributeSide = oppositeSide(dockedSide)
124
71
 
125
- const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, -shrinkAmount, true)
72
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, dockedSide, posKey, sizeKey)
73
+
74
+ const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, -shrinkAmount, { allowOutOfBounds: true, pinnedEdgeCoordinates })
126
75
 
127
76
  if (changes instanceof KnownError) {
128
77
  // 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) {
78
+ if (changes.code === LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS || changes.code === LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE) {
130
79
  changes.message = `This error should never happen, please file a bug report: ${changes.message}`
131
80
  throw changes
132
81
  }
133
- return changes as any // in case of other errors
82
+ return changes
134
83
  }
135
84
  applyFrameChanges(win, changes)
136
85
  pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
137
86
 
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))
87
+
88
+ applyFixes()
89
+ pushIfNotIn(toExtract, toFix)
150
90
 
151
91
  if (frame.docked === "right" || frame.docked === "bottom") {
152
- frame[posKey] = frame[posKey] + (frame[sizeKey] - collapseSize[sizeKey])
92
+ frame[posKey] = frame[posKey] + (frame[sizeKey] - collapseSizeScaled[sizeKey])
153
93
  }
154
- frame[sizeKey] = collapseSize[sizeKey]
94
+ frame[sizeKey] = collapseSizeScaled[sizeKey]
155
95
  frame.collapsed = currentSize
156
96
  // when we collapse to 0 it's a special case where the frame will always fit the entire window edge
157
97
  // for proper uncollapsing later
158
- if (collapseSize[sizeKey] === 0) {
98
+ if (collapseSizeScaled[sizeKey] === 0) {
159
99
  frame[oppositePosKey] = 0
160
100
  frame[oppositeSizeKey] = settings.maxInt
161
101
  }
162
102
 
163
103
  return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
164
104
  }
105
+
@@ -8,6 +8,7 @@ import { getFrameUndockInfo } from "./getFrameUndockInfo.js"
8
8
 
9
9
  import { cloneFrame } from "../helpers/cloneFrame.js"
10
10
  import { getDockBoundaries } from "../helpers/getDockBoundaries.js"
11
+ import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
11
12
  import { oppositeSide } from "../helpers/oppositeSide.js"
12
13
  import { settings } from "../settings.js"
13
14
  import type { EdgeSide, LayoutChange, LayoutWindow } from "../types/index.js"
@@ -28,6 +29,7 @@ export function getFrameDockInfo(
28
29
  | LayoutChange
29
30
  | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS>
30
31
  | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE>
32
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES>
31
33
  | KnownError<typeof LAYOUT_ERROR.CANT_LEAVE_NO_UNDOCKED_FRAMES>
32
34
  | KnownError<typeof LAYOUT_ERROR.FRAME_ALREADY_DOCKED_ON_SIDE>
33
35
  | KnownError<typeof LAYOUT_ERROR.CANT_UNDOCK_COLLAPSED_FRAME>
@@ -82,7 +84,12 @@ export function getFrameDockInfo(
82
84
  // redistribute other non-docked frames to make room for the new dock.
83
85
  const sideToPushTowards = oppositeSide(side)
84
86
 
85
- const redistributeChanges = getFramesRedistributeInfo(win, sideToPushTowards, nonDockedFrameIds, perpendicularLength)
87
+ const posKey = isHorizontal ? "x" : "y"
88
+ const sizeKey = isHorizontal ? "width" : "height"
89
+
90
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, side, posKey, sizeKey)
91
+
92
+ const redistributeChanges = getFramesRedistributeInfo(win, sideToPushTowards, nonDockedFrameIds, perpendicularLength, { pinnedEdgeCoordinates })
86
93
 
87
94
  if (redistributeChanges instanceof KnownError) {
88
95
  return redistributeChanges