@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
@@ -4,9 +4,10 @@ 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
- import { settings } from "../settings.js"
9
- import type { EdgeSide, LayoutChange, LayoutWindow } from "../types/index.js"
10
+ import type { EdgeSide, LayoutChange, LayoutWindow, Size } from "../types/index.js"
10
11
  import { LAYOUT_ERROR } from "../types/index.js"
11
12
  import { KnownError } from "../utils/KnownError.js"
12
13
 
@@ -20,19 +21,33 @@ import { KnownError } from "../utils/KnownError.js"
20
21
  */
21
22
  export function getFrameUncollapseInfo(
22
23
  win: LayoutWindow,
23
- frameId: string
24
+ frameId: string,
25
+ {
26
+ restoreSize
27
+ }: {
28
+ /** Override the size to restore the frame to. */
29
+ restoreSize?: Size
30
+ } = {}
24
31
  ): LayoutChange
25
32
  | KnownError<typeof LAYOUT_ERROR.CANT_UNCOLLAPSE_NOT_COLLAPSED>
26
33
  | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS>
27
- | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE> {
34
+ | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE>
35
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES> {
28
36
  win = walk(win, undefined, { save: true })
29
37
  const frame = win.frames[frameId]
30
38
  if (!frame) { throw new Error(`Unknown frame ${frameId}`) }
31
39
  if (!frame.docked) { throw new Error(`Frame ${frameId} is not docked.`) }
32
40
  const toExtract = [frame.id]
33
41
 
34
- const storedSize = frame.collapsed
35
- if (storedSize === undefined) {
42
+
43
+ const isVertical = frame.docked === "left" || frame.docked === "right"
44
+ const sizeKey = isVertical ? "width" : "height" as const
45
+ const posKey = isVertical ? "x" : "y"
46
+ const currentSize = frame[sizeKey]
47
+
48
+ const storedSize = (restoreSize !== undefined ? restoreSize[sizeKey] : frame.collapsed)!
49
+
50
+ if (frame.collapsed === undefined) {
36
51
  return new KnownError(
37
52
  LAYOUT_ERROR.CANT_UNCOLLAPSE_NOT_COLLAPSED,
38
53
  `Frame ${frameId} is not collapsed.`,
@@ -40,93 +55,21 @@ export function getFrameUncollapseInfo(
40
55
  )
41
56
  }
42
57
 
43
- const isVertical = frame.docked === "left" || frame.docked === "right"
44
- const sizeKey = isVertical ? "width" : "height" as const
45
- const posKey = isVertical ? "x" : "y"
46
- const currentSize = frame[sizeKey]
47
58
  const expandAmount = storedSize - currentSize
48
59
 
60
+
49
61
  const dockedSide = frame.docked as EdgeSide
50
62
 
51
- /**
52
- * Mirrors the framesToFix logic in getFrameCollapseInfo:
53
- *
54
- * 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
- const framesToFix: ({
88
- id: string
89
- posKey: "x" | "y"
90
- sizeKey: "width" | "height"
91
- type: "start" | "end"
92
- })[] = []
93
-
94
- const opposite = oppositeSide(dockedSide)
95
- if (frame[sizeKey] !== 0) {
96
- for (const other of Object.values(win.frames)) {
97
- if (frame.id === other.id || !other.docked) continue
98
- if (other.width === 0 || other.height === 0) continue
99
-
100
- if (other.docked === opposite) continue
101
- if (dockedSide === "left" || dockedSide === "top") {
102
- if (other[posKey] !== 0) continue
103
- other[posKey] = frame[posKey] + frame[sizeKey]
104
- other[sizeKey] -= frame[sizeKey] // this is collapsed size
105
- framesToFix.push({
106
- id: other.id,
107
- posKey: posKey,
108
- sizeKey: sizeKey,
109
- type: "start"
110
- })
111
- } else {
112
- if (other[posKey] + other[sizeKey] !== settings.maxInt) continue
113
- other[sizeKey] -= frame[sizeKey]
114
- framesToFix.push({
115
- id: other.id,
116
- posKey: posKey,
117
- sizeKey: sizeKey,
118
- type: "end"
119
- })
120
- }
121
- }
122
- }
63
+ const { applyFixes, toFix } = framesRedistributeFix(win, frame, dockedSide, posKey, sizeKey, "expand")
123
64
 
124
65
  // note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
125
66
  const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
126
67
 
127
68
  const redistributeSide = oppositeSide(frame.docked)
128
69
 
129
- const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, expandAmount)
70
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, dockedSide, posKey, sizeKey)
71
+
72
+ const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, expandAmount, { pinnedEdgeCoordinates })
130
73
 
131
74
  if (changes instanceof KnownError) {
132
75
  return changes
@@ -135,19 +78,8 @@ export function getFrameUncollapseInfo(
135
78
  applyFrameChanges(win, changes)
136
79
  pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
137
80
 
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
-
150
- pushIfNotIn(toExtract, framesToFix.map(_ => _.id))
81
+ applyFixes()
82
+ pushIfNotIn(toExtract, toFix)
151
83
 
152
84
  frame[sizeKey] = storedSize
153
85
  frame.collapsed = undefined
@@ -158,3 +90,4 @@ export function getFrameUncollapseInfo(
158
90
 
159
91
  return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
160
92
  }
93
+
@@ -78,6 +78,9 @@ export function getFrameUndockInfo(
78
78
  : "y"
79
79
 
80
80
  const adjustmentValue = frameSide === "left" || frameSide === "top" ? 0 : maxInt
81
+ // for perpendicular frames (e.g. top/bottom when undocking left/right),
82
+ // expand along the dimension perpendicular to the undock axis
83
+ const perpendicularSizeKey = isVertical ? "width" : "height" as const
81
84
  const otherChanges = otherDockedFramesToResize.map(other => {
82
85
  // shrink the ends of the undocking frame
83
86
  if (other.docked === shrinkStartKey) {
@@ -86,10 +89,13 @@ export function getFrameUndockInfo(
86
89
  } else if (other.docked === shrinkEndKey) {
87
90
  frameClone[sizeKey] -= other[sizeKey]
88
91
  }
92
+ // same-axis frames shift their position; perpendicular frames expand their dimension to fill the space
93
+ const otherIsVertical = other.docked === "left" || other.docked === "right"
94
+ const isSameAxis = isVertical === otherIsVertical
89
95
  return {
90
96
  ...other,
91
97
  [adjustmentKey]: adjustmentValue,
92
- [secondaryAdjustmentKey]: other[secondaryAdjustmentKey] + other[adjustmentKey]
98
+ ...(isSameAxis ? { [secondaryAdjustmentKey]: other[secondaryAdjustmentKey] + other[adjustmentKey] } : { [perpendicularSizeKey]: maxInt })
93
99
  }
94
100
  })
95
101
 
@@ -13,7 +13,7 @@ import { KnownError } from "../utils/KnownError.js"
13
13
  *
14
14
  * 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.
15
15
  *
16
- * See {@link getFrameCollapseInfo} and {@link getFrameUncollapseInfo} for how to work around this (you must move frames like A to share the edge then move them back). 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.
16
+ * See {@link framesRedistributeFix} for the helper that is used internally to get around this.
17
17
  *
18
18
  * ┌────────────┐
19
19
  * │A │
@@ -22,7 +22,19 @@ import { KnownError } from "../utils/KnownError.js"
22
22
  * └─────────┘
23
23
  *
24
24
  * Also note that it automatically excludes all collapsed frames without an area (height or width === 0).
25
+ *
26
+ * ## Pinned Edges
27
+ *
28
+ * Pinned edges will not be moved, space will be redistributed amongst remaining space. If doing this would cause edges to get "flipped", function will return {@link LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES}.
29
+ *
30
+ * ┌──┬──────────────┐
31
+ * │ │ │
32
+ * │ ├──────────┬───┤
33
+ * │ │ │ │
34
+ * │ │ │ │
35
+ * └──┴──────────┴───┘
25
36
  */
37
+
26
38
  export function getFramesRedistributeInfo(
27
39
  win: LayoutWindow,
28
40
  /** Side to expand/shrink to. */
@@ -30,11 +42,22 @@ export function getFramesRedistributeInfo(
30
42
  frameIds: string[],
31
43
  /** This can be negative to expand the frames instead (e.g. when collapsing a docked frame). */
32
44
  amountScaled: number,
33
- /** Allow the resize span to exceed window bounds. */
34
- allowOutOfBounds = false
45
+ {
46
+ allowOutOfBounds = false,
47
+ pinnedEdgeCoordinates,
48
+ skipMinSizeCheck = false
49
+ }: {
50
+ /** Allow the resize span to exceed window bounds. */
51
+ allowOutOfBounds?: boolean
52
+ /** Frame IDs whose position and size must not change. Must be a subset of frameIds. */
53
+ pinnedEdgeCoordinates?: number[]
54
+ /** Skip the per-frame min size check. Frames may shrink very small but not to 0 or below. */
55
+ skipMinSizeCheck?: boolean
56
+ } = {}
35
57
  ): LayoutChange
36
58
  | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS>
37
- | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE> {
59
+ | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE>
60
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES> {
38
61
  const isLeftOrTop = side === "left" || side === "top"
39
62
  const isHorizontalSide = side === "left" || side === "right"
40
63
  const posKey = isHorizontalSide ? "x" : "y" as const
@@ -42,6 +65,13 @@ export function getFramesRedistributeInfo(
42
65
 
43
66
  const frames = frameIds.map(id => win.frames[id])
44
67
 
68
+ const pinnedEdges = new Set<number>()
69
+ if (pinnedEdgeCoordinates) {
70
+ for (const coord of pinnedEdgeCoordinates) {
71
+ pinnedEdges.add(coord)
72
+ }
73
+ }
74
+
45
75
  const edgeSet = new Set<number>()
46
76
  for (const frame of frames) {
47
77
  edgeSet.add(frame[posKey])
@@ -59,6 +89,7 @@ export function getFramesRedistributeInfo(
59
89
  const anchorEdge = isLeftOrTop ? uniqueEdges[0] : uniqueEdges[uniqueEdges.length - 1]
60
90
  const dirMultiplier = isLeftOrTop ? -1 : 1
61
91
 
92
+ // calculate new edges using proportional algo
62
93
  const newEdges: number[] = []
63
94
  for (let i = 0; i < uniqueEdges.length; i++) {
64
95
  const edge = uniqueEdges[i]
@@ -72,8 +103,31 @@ export function getFramesRedistributeInfo(
72
103
  const oppositeEdgeIndex = isLeftOrTop ? uniqueEdges.length - 1 : 0
73
104
  newEdges[oppositeEdgeIndex] = isLeftOrTop ? anchorEdge + newSpan : anchorEdge - newSpan
74
105
 
106
+ // if we have pinned edges, force them back to their original positions
107
+ if (pinnedEdges.size > 0) {
108
+ for (let i = 0; i < uniqueEdges.length; i++) {
109
+ if (pinnedEdges.has(uniqueEdges[i])) {
110
+ newEdges[i] = uniqueEdges[i]
111
+ }
112
+ }
113
+
114
+ // check for flipped frames - a non-pinned edge should not cross a pinned edge
115
+ for (let i = 0; i < uniqueEdges.length - 1; i++) {
116
+ const startEdge = uniqueEdges[i]
117
+ const endEdge = uniqueEdges[i + 1]
118
+ const newStart = newEdges[i]
119
+ const newEnd = newEdges[i + 1]
120
+
121
+ if (newStart > newEnd) {
122
+ const hasPinnedEdge = pinnedEdges.has(startEdge) || pinnedEdges.has(endEdge)
123
+ if (hasPinnedEdge) {
124
+ return new KnownError(LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES, `Redistribution would cause frames to cross pinned edge at ${pinnedEdges.has(startEdge) ? startEdge : endEdge}.`, { problemEdgeCoordinates: [startEdge, endEdge] })
125
+ }
126
+ }
127
+ }
128
+ }
75
129
 
76
- const minSize = settings.minSizeScaled[sizeKey]
130
+ const minSize = skipMinSizeCheck ? 0 : settings.minSizeScaled[sizeKey]
77
131
  const result: LayoutFrame[] = []
78
132
  for (const frame of frames) {
79
133
  if (frame.collapsed && (frame.width === 0 || frame.height === 0)) continue
@@ -83,6 +137,7 @@ export function getFramesRedistributeInfo(
83
137
  const newEnd = newEdges[uniqueEdges.indexOf(endEdge)]
84
138
  const newSize = newEnd - newStart
85
139
 
140
+
86
141
  if (newSize < minSize) {
87
142
  return new KnownError(LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE, `Redistribute would cause frame ${frame.id} to shrink to ${newSize} which is below minimum ${minSize}.`, { frameSizeNeeded: newSize, minFrameSize: minSize })
88
143
  }
@@ -0,0 +1,139 @@
1
+ import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
2
+ import { walk } from "@alanscodelog/utils/walk"
3
+
4
+ import { getMoveEdgeInfo } from "../helpers/getMoveEdgeInfo.js"
5
+ import { settings } from "../settings.js"
6
+ import type { LayoutChange, LayoutWindow } from "../types/index.js"
7
+ import { KnownError } from "../utils/KnownError.js"
8
+
9
+
10
+ /**
11
+ * Experimental frame "relaxation" algorithm.
12
+ *
13
+ * Useful for expanding frames that have gotten too small. Used internally by {@link getUpdateWindowSizeInfo}
14
+ */
15
+ export function getRelaxFramesInfo(
16
+ win: LayoutWindow,
17
+ passes: number = 2
18
+ ): LayoutChange<any> {
19
+ win = walk(win, undefined, { save: true })
20
+
21
+ const toExtract: string[] = []
22
+ const framesSortedHorizontal = Object.values(win.frames)
23
+ .filter(f => f.width > 0 && f.width < settings.minSizeScaled.width && !f.collapsed)
24
+ .sort((a, b) => a.x - b.x)
25
+ const framesSortedVertical = Object.values(win.frames)
26
+ .filter(f => f.height > 0 && f.height < settings.minSizeScaled.height && !f.collapsed)
27
+ .sort((a, b) => a.y - b.y)
28
+
29
+ for (let pass = 0; pass < passes; pass++) {
30
+ let anyChanged = false
31
+
32
+ for (const orientation of ["horizontal", "vertical"] as const) {
33
+ const sizeKey = orientation === "horizontal" ? "width" : "height" as const
34
+ const posKey = orientation === "horizontal" ? "x" : "y" as const
35
+ const minSize = settings.minSizeScaled[sizeKey]
36
+
37
+ const frames = orientation === "horizontal" ? framesSortedHorizontal : framesSortedVertical
38
+
39
+ if (frames.length === 0) { continue }
40
+
41
+
42
+ for (const frame of frames) {
43
+ const deficit = minSize - frame[sizeKey]
44
+
45
+
46
+ if (deficit <= 0) continue
47
+
48
+ // try expanding the right/bottom edge first
49
+ const endEdge = {
50
+ startX: frame.x + frame.width,
51
+ startY: frame.y + frame.height,
52
+ endX: frame.x + frame.width,
53
+ endY: frame.y + frame.height
54
+ }
55
+
56
+ const touchingEnd = Object.values(win.frames).filter(f =>
57
+ f.id !== frame.id
58
+ && f[posKey] === frame[posKey] + frame[sizeKey]
59
+ && !f.collapsed
60
+ )
61
+
62
+ if (touchingEnd.length > 0) {
63
+ const targetPos = {
64
+ x: orientation === "horizontal" ? frame.x + frame.width + deficit : frame.x,
65
+ y: orientation === "vertical" ? frame.y + frame.height + deficit : frame.y
66
+ }
67
+
68
+
69
+ const result = getMoveEdgeInfo(touchingEnd, endEdge, targetPos, settings.minSizeScaled, true, true)
70
+
71
+
72
+ if (!(result instanceof KnownError) && result.distance > 0) {
73
+ const expandBy = Math.min(deficit, result.distance)
74
+
75
+ frame[sizeKey] += expandBy
76
+ for (const f of touchingEnd) {
77
+ f[posKey] += expandBy
78
+ f[sizeKey] -= expandBy
79
+ }
80
+ pushIfNotIn(toExtract, [frame.id, ...touchingEnd.map(f => f.id)])
81
+ anyChanged = true
82
+
83
+ if (frame[sizeKey] >= minSize) continue
84
+ }
85
+ }
86
+
87
+ // if still too small, try expanding the left/top edge
88
+ if (frame[sizeKey] < minSize) {
89
+ const remainingDeficit = minSize - frame[sizeKey]
90
+
91
+ const startEdge = {
92
+ startX: frame.x,
93
+ startY: frame.y,
94
+ endX: frame.x,
95
+ endY: frame.y
96
+ }
97
+
98
+ const touchingStart = Object.values(win.frames).filter(f =>
99
+ f.id !== frame.id
100
+ && f[posKey] + f[sizeKey] === frame[posKey]
101
+ && !f.collapsed
102
+ )
103
+
104
+
105
+ if (touchingStart.length > 0) {
106
+ const targetPos = {
107
+ x: orientation === "horizontal" ? frame.x - remainingDeficit : frame.x,
108
+ y: orientation === "vertical" ? frame.y - remainingDeficit : frame.y
109
+ }
110
+
111
+
112
+ const result = getMoveEdgeInfo(touchingStart, startEdge, targetPos, settings.minSizeScaled, true, true)
113
+
114
+
115
+ if (!(result instanceof KnownError) && result.distance > 0) {
116
+ const expandBy = Math.min(remainingDeficit, result.distance)
117
+
118
+ frame[posKey] -= expandBy
119
+ frame[sizeKey] += expandBy
120
+ for (const f of touchingStart) {
121
+ f[sizeKey] -= expandBy
122
+ }
123
+ pushIfNotIn(toExtract, [frame.id, ...touchingStart.map(f => f.id)])
124
+ anyChanged = true
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ if (!anyChanged) { break }
132
+ }
133
+
134
+ return {
135
+ modified: toExtract.map(id => win.frames[id]),
136
+ created: [],
137
+ deleted: []
138
+ }
139
+ }
@@ -0,0 +1,105 @@
1
+ import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
2
+ import { walk } from "@alanscodelog/utils/walk"
3
+
4
+ import { applyFrameChanges } from "../layout/applyFrameChanges.js"
5
+ import { getFrameCollapseInfo } from "../layout/getFrameCollapseInfo.js"
6
+ import { getFrameUncollapseInfo } from "../layout/getFrameUncollapseInfo.js"
7
+ import { getRelaxFramesInfo } from "../layout/getRelaxFramesInfo.js"
8
+ import { settings } from "../settings.js"
9
+ import type { LayoutChange, LayoutWindow, PxSize } from "../types/index.js"
10
+ import { KnownError } from "../utils/KnownError.js"
11
+
12
+
13
+ /**
14
+ * Returns info to update the window size, keeping non-0 collapsed frames the same pixel size IF possible. Never returns an error.
15
+ *
16
+ * This sacrifices the min (percentage) size of other frames if needed, although not to the point of making them 0 size (which would break the layout).
17
+ *
18
+ * If there is absolutely no space, it will leave things as they are. This just means that the collapsed frame will shrink further than your ideal pixel width, but percentage size will stay the same.
19
+ *
20
+ * @experimental
21
+ */
22
+ export function getUpdateWindowSizeInfo(
23
+ win: LayoutWindow,
24
+ event: PxSize,
25
+ { relaxPasses = 2 }: { relaxPasses?: number } = {}
26
+ ): LayoutChange<any> {
27
+ win = walk(win, undefined, { save: true }) as LayoutWindow
28
+
29
+ win.pxWidth = event.pxWidth
30
+ win.pxHeight = event.pxHeight
31
+
32
+
33
+ const maxInt = settings.maxInt
34
+ const frames = Object.values(win.frames)
35
+
36
+ const dockedFrameIds = frames.filter(f => f.collapsed).map(f => f.id)
37
+
38
+ if (dockedFrameIds.length === 0) {
39
+ return {
40
+ modified: [],
41
+ created: [],
42
+ deleted: [],
43
+ window: { pxWidth: event.pxWidth, pxHeight: event.pxHeight }
44
+ }
45
+ }
46
+
47
+ const toExtract: string[] = []
48
+ for (const frameId of dockedFrameIds) {
49
+ const frame = win.frames[frameId]
50
+ const orientation = (frame.docked === "left" || frame.docked === "right") ? "horizontal" : "vertical"
51
+ const sizeKey = orientation === "horizontal" ? "width" : "height" as const
52
+ const targetPxSize = settings.collapseSizePx[sizeKey]
53
+ const targetScaled = Math.round((targetPxSize / (orientation === "horizontal" ? win.pxWidth : win.pxHeight)) * maxInt)
54
+
55
+
56
+ if (frame[sizeKey] === targetScaled) continue
57
+ if (frame[sizeKey] === 0) continue
58
+
59
+ let result: LayoutChange | KnownError<any>
60
+
61
+ // the logic needed to do this is nearly identical to the collapse/uncollapse logic
62
+ // so we abuse the functions a bit, in future might allow a more flexible collapse/uncollapse function
63
+ const wasCollapsed = frame.collapsed
64
+ const wasMinSize = { ...settings.minSize }
65
+ settings.minSize = 1
66
+ if (targetScaled < frame[sizeKey]) {
67
+ // clear temporarily so collapse doesn't error
68
+ frame.collapsed = undefined
69
+ result = getFrameCollapseInfo(win, frame.id, { collapseSizeScaled: { width: targetScaled, height: targetScaled } })
70
+ } else {
71
+ // set to something, could be anything
72
+ frame.collapsed = Infinity
73
+ result = getFrameUncollapseInfo(win, frame.id, { restoreSize: { width: targetScaled, height: targetScaled } })
74
+ }
75
+
76
+ if (!(result instanceof KnownError)) {
77
+ applyFrameChanges(win, result)
78
+ pushIfNotIn(toExtract, result.modified.map(f => f.id))
79
+ }
80
+
81
+ // restore original collapsed value
82
+ frame.collapsed = wasCollapsed
83
+ settings.minSize = wasMinSize
84
+
85
+
86
+ // attempt relaxation
87
+ const result2 = getRelaxFramesInfo(win, relaxPasses)
88
+ if (!(result2 instanceof KnownError)) {
89
+ applyFrameChanges(win, result2)
90
+ pushIfNotIn(toExtract, result2.modified.map(f => f.id))
91
+ }
92
+ }
93
+
94
+
95
+ return {
96
+ modified: toExtract.map(id => win.frames[id]),
97
+ created: [],
98
+ deleted: [],
99
+ window: {
100
+ pxWidth: event.pxWidth,
101
+ pxHeight: event.pxHeight
102
+ }
103
+ }
104
+ }
105
+
@@ -21,6 +21,7 @@ export { getFrameSwapInfo } from "./getFrameSwapInfo.js"
21
21
  export { getFrameTo } from "./getFrameTo.js"
22
22
  export { getFrameUncollapseInfo } from "./getFrameUncollapseInfo.js"
23
23
  export { getFrameUndockInfo } from "./getFrameUndockInfo.js"
24
+ export { getUpdateWindowSizeInfo } from "./getUpdateWindowSizeInfo.js"
24
25
  export { getWindowDragZones } from "./getWindowDragZones.js"
25
26
  export { isPointInRect } from "./isPointInRect.js"
26
27
  export { layoutAddWindow } from "./layoutAddWindow.js"
@@ -1,6 +1,7 @@
1
+ import { clampNumber } from "@alanscodelog/utils/clampNumber"
2
+
1
3
  import { findFramesTouchingEdge } from "./findFramesTouchingEdge.js"
2
4
 
3
- import { clampNumber } from "../helpers/clampNumber.js"
4
5
  import { frameToEdges } from "../helpers/frameToEdges.js"
5
6
  import { getEdgeOrientation } from "../helpers/getEdgeOrientation.js"
6
7
  import { getResizeLimit } from "../helpers/getResizeLimit.js"
@@ -19,7 +20,7 @@ export function resizeFrame(
19
20
  /** Scaled */
20
21
  minSize: Size = settings.minSizeScaled
21
22
  ): boolean {
22
- const originalDistance = distance
23
+ // const originalDistance = distance
23
24
  const frameEdges = frameToEdges(frame)
24
25
  const resizeEdges = dir === "up"
25
26
  ? [frameEdges.top]
@@ -85,7 +86,7 @@ export function resizeFrame(
85
86
  }
86
87
 
87
88
  if (!foundEdge) {
88
- distance = originalDistance
89
+ // distance = originalDistance
89
90
  continue
90
91
  }
91
92