@witchcraft/layout 0.3.0 → 0.4.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 (167) hide show
  1. package/README.md +79 -74
  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/getFramesRedistributeInfo.d.ts +20 -4
  86. package/dist/runtime/layout/getFramesRedistributeInfo.js +31 -2
  87. package/dist/runtime/layout/getRelaxFramesInfo.d.ts +7 -0
  88. package/dist/runtime/layout/getRelaxFramesInfo.js +91 -0
  89. package/dist/runtime/layout/getUpdateWindowSizeInfo.d.ts +13 -0
  90. package/dist/runtime/layout/getUpdateWindowSizeInfo.js +65 -0
  91. package/dist/runtime/layout/index.d.ts +1 -0
  92. package/dist/runtime/layout/index.js +1 -0
  93. package/dist/runtime/layout/resizeFrame.js +1 -3
  94. package/dist/runtime/settings.d.ts +26 -19
  95. package/dist/runtime/settings.js +57 -22
  96. package/dist/runtime/types/index.d.ts +1243 -30
  97. package/dist/runtime/types/index.js +22 -8
  98. package/dist/runtime/types/vue.d.ts +41 -0
  99. package/dist/runtime/{drag/types.js → types/vue.js} +1 -0
  100. package/dist/runtime/utils/cssToKey.d.ts +5 -0
  101. package/dist/runtime/utils/cssToKey.js +3 -0
  102. package/package.json +11 -12
  103. package/src/module.ts +6 -1
  104. package/src/runtime/components/FrameDragHandle.vue +1 -1
  105. package/src/runtime/components/LayoutDecosEdges.vue +38 -0
  106. package/src/runtime/components/LayoutDecosRects.vue +42 -0
  107. package/src/runtime/components/LayoutDragEdgeGrabbed.vue +50 -0
  108. package/src/runtime/components/LayoutDragEdgeHandle.vue +52 -0
  109. package/src/runtime/components/LayoutDragEdgeVisible.vue +45 -0
  110. package/src/runtime/components/LayoutDragEdges.vue +78 -0
  111. package/src/runtime/components/LayoutEdgesActiveFrame.vue +43 -0
  112. package/src/runtime/components/LayoutFrame.vue +22 -15
  113. package/src/runtime/components/LayoutIntersectionHandle.vue +45 -0
  114. package/src/runtime/components/LayoutIntersectionVisible.vue +41 -0
  115. package/src/runtime/components/LayoutIntersections.vue +64 -0
  116. package/src/runtime/components/{LayoutShapeSquare.vue → LayoutShapeRect.vue} +2 -2
  117. package/src/runtime/components/LayoutWindow.vue +29 -49
  118. package/src/runtime/composables/useFrames.ts +7 -2
  119. package/src/runtime/demo/App.vue +128 -78
  120. package/src/runtime/demo/DemoControls.vue +7 -5
  121. package/src/runtime/demo/index.html +12 -0
  122. package/src/runtime/demo/tailwind.css +3 -0
  123. package/src/runtime/drag/CloseAction.ts +2 -3
  124. package/src/runtime/drag/DragActionHandler.ts +48 -10
  125. package/src/runtime/drag/FrameDragAction.ts +4 -5
  126. package/src/runtime/drag/SplitAction.ts +4 -3
  127. package/src/runtime/drag/createDefaultHandlers.ts +2 -1
  128. package/src/runtime/drag/defaultDragActions.ts +2 -1
  129. package/src/runtime/helpers/consoleDebugWindow.ts +331 -0
  130. package/src/runtime/helpers/convertLayoutWindowToWorkspace.ts +2 -2
  131. package/src/runtime/helpers/framesRedistributeFix.ts +162 -0
  132. package/src/runtime/helpers/getMoveEdgeInfo.ts +22 -5
  133. package/src/runtime/helpers/getPinnedEdgesForCollapsedFrames.ts +29 -0
  134. package/src/runtime/helpers/{getShapeSquareCss.ts → getShapeRectCss.ts} +3 -3
  135. package/src/runtime/helpers/index.ts +4 -3
  136. package/src/runtime/helpers/moveEdge.ts +3 -1
  137. package/src/runtime/helpers/{numberToScaledSize.ts → pxSizeToScaledSize.ts} +2 -2
  138. package/src/runtime/helpers/updateWindowSizeWithEvent.ts +3 -0
  139. package/src/runtime/helpers/validateLayoutShape.ts +46 -0
  140. package/src/runtime/layout/applyFrameChanges.ts +5 -0
  141. package/src/runtime/layout/createSplitDecoShapes.ts +1 -1
  142. package/src/runtime/layout/getFrameCollapseInfo.ts +26 -85
  143. package/src/runtime/layout/getFrameDockInfo.ts +8 -1
  144. package/src/runtime/layout/getFrameUncollapseInfo.ts +29 -96
  145. package/src/runtime/layout/getFramesRedistributeInfo.ts +60 -5
  146. package/src/runtime/layout/getRelaxFramesInfo.ts +139 -0
  147. package/src/runtime/layout/getUpdateWindowSizeInfo.ts +105 -0
  148. package/src/runtime/layout/index.ts +1 -0
  149. package/src/runtime/layout/resizeFrame.ts +4 -3
  150. package/src/runtime/settings.ts +78 -25
  151. package/src/runtime/types/index.ts +205 -33
  152. package/src/runtime/types/vue.ts +41 -0
  153. package/src/runtime/utils/cssToKey.ts +8 -0
  154. package/dist/runtime/components/LayoutDecos.vue +0 -26
  155. package/dist/runtime/components/LayoutEdges.d.vue.ts +0 -17
  156. package/dist/runtime/components/LayoutEdges.vue +0 -144
  157. package/dist/runtime/components/LayoutEdges.vue.d.ts +0 -17
  158. package/dist/runtime/drag/types.d.ts +0 -161
  159. package/dist/runtime/helpers/clampNumber.d.ts +0 -1
  160. package/dist/runtime/helpers/clampNumber.js +0 -3
  161. package/dist/runtime/helpers/getShapeSquareCss.d.ts +0 -7
  162. package/dist/runtime/helpers/numberToScaledSize.d.ts +0 -2
  163. package/src/runtime/components/LayoutDecos.vue +0 -28
  164. package/src/runtime/components/LayoutEdges.vue +0 -170
  165. package/src/runtime/drag/types.ts +0 -177
  166. package/src/runtime/helpers/clampNumber.ts +0 -9
  167. /package/dist/runtime/components/{LayoutShapeSquare.vue → LayoutShapeRect.vue} +0 -0
@@ -1,5 +1,4 @@
1
1
  import { DragActionHandler } from "./DragActionHandler.js"
2
- import type { ActionDragChangeResult, DragState, IDragAction } from "./types.js"
3
2
 
4
3
  import { dirToOrientation } from "../helpers/dirToOrientation.js"
5
4
  import { getEdgeOrientation } from "../helpers/getEdgeOrientation.js"
@@ -7,7 +6,7 @@ import { oppositeSide } from "../helpers/oppositeSide.js"
7
6
  import { applyFrameChanges } from "../layout/applyFrameChanges.js"
8
7
  import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
9
8
  import { getCloseFrameInfo } from "../layout/getCloseFrameInfo.js"
10
- import type { CloseDeco } from "../types/index.js"
9
+ import type { ActionDragChangeResult, CloseDeco, DragState, IDragAction } from "../types/index.js"
11
10
  import type { KnownError } from "../utils/KnownError.js"
12
11
 
13
12
  export type CloseInfo = Exclude<ReturnType<typeof getCloseFrameInfo>, KnownError>
@@ -94,7 +93,7 @@ export class CloseAction implements IDragAction {
94
93
  force,
95
94
  shapes: [
96
95
  {
97
- type: "square",
96
+ type: "rect",
98
97
  data: { x: _.x, y: _.y, width: _.width, height: _.height },
99
98
  attrs: {
100
99
  class: force
@@ -1,8 +1,10 @@
1
1
  import { get, type RecordFromArray } from "@alanscodelog/utils"
2
2
 
3
- import type { DragChangeHandler, DragChangeResult, DragState, IDragAction } from "./types.js"
4
-
5
- import type { LayoutShape } from "../types/index.js"
3
+ import { isWindowEdge } from "../helpers/isWindowEdge.js"
4
+ import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
5
+ import type { DragChangeHandler, DragChangeResult, DragState, Edge, IDragAction, LayoutFrame, LayoutShape } from "../types/index.js"
6
+ import { LAYOUT_ERROR } from "../types/index.js"
7
+ import { KnownError } from "../utils/KnownError.js"
6
8
 
7
9
  /**
8
10
  * Handles the lifecycle of a drag actions {@link IDragAction} and provides additional hooks.
@@ -21,10 +23,18 @@ export class DragActionHandler<
21
23
 
22
24
  boundCancel: (e: PointerEvent | KeyboardEvent | undefined, state: DragState) => void
23
25
 
24
- defaultOnDragChange: DragChangeHandler = (type, _e, state, _forceRecalculateEdges, _cancel) => ({
25
- updateEdges: type === "move" ? !state.isDraggingFromWindowEdge : true,
26
- shapes: []
27
- })
26
+ defaultOnDragChange: DragChangeHandler = (type, _e, state, _forceRecalculateEdges, _cancel) => {
27
+ const isTouchingCollapsedFrameEdge = type === "move"
28
+ && state.isDragging === "edge"
29
+ && state.touchingFramesArrays.some(frames =>
30
+ // we check with a falsy check on PURPOSE, frames collapsed to 0 aren't an issue, they are ignored anyways
31
+ Object.values(frames).some(f => f.collapsed)
32
+ )
33
+ return {
34
+ updateEdges: type === "move" ? !(state.isDraggingFromWindowEdge || isTouchingCollapsedFrameEdge) : true,
35
+ shapes: []
36
+ }
37
+ }
28
38
 
29
39
  hooks: {
30
40
  /** Called while dragging during dragChange events. You can use this to update the dragging edges. */
@@ -54,6 +64,8 @@ export class DragActionHandler<
54
64
  * Default onDragChange handler for when no action can handle the request.
55
65
  *
56
66
  * Should return true to allow the edges to be moved, or false to prevent it.
67
+ *
68
+ * The default prevents movement when the edge is a window edge and when the edge is touching a collapsed frame.
57
69
  */
58
70
  defaultOnDragChange?: DragChangeHandler
59
71
  ) {
@@ -150,6 +162,32 @@ export class DragActionHandler<
150
162
  }
151
163
  }
152
164
 
165
+ annotateEdges(edges: Edge[], frames: LayoutFrame[]): void {
166
+ for (const edge of edges) {
167
+ // edge already has an error
168
+ if (edge.error) continue
169
+ for (const action of Object.values<IDragAction>(this.actions)) {
170
+ action.annotateEdge?.(edge, frames)
171
+ if (edge.error) break
172
+ }
173
+ // window edges don't resize anything
174
+ if (!edge.error && !isWindowEdge(edge)) {
175
+ const touching = findFramesTouchingEdge(edge, frames)
176
+ if (touching) {
177
+ // we check with a falsy check on PURPOSE, frames collapsed to 0 aren't an issue, they are ignored anyways
178
+ const collapsedFrame = touching.find(f => f.frame.collapsed)
179
+ if (collapsedFrame) {
180
+ edge.error = new KnownError(
181
+ LAYOUT_ERROR.CANT_RESIZE_COLLAPSED_FRAME,
182
+ "Cannot Move: Can't Resize Collapsed Frame",
183
+ { frame: (collapsedFrame.frame as LayoutFrame) }
184
+ )
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+
153
191
  onDragApply(
154
192
  state: DragState,
155
193
  forceRecalculateEdges: () => void
@@ -177,14 +215,14 @@ export class DragActionHandler<
177
215
  type: "before" | "after" | string,
178
216
  state: DragState,
179
217
  pluginState: Record<string, any> = {},
180
- /** Object key to filter the state by, e.g. state.win.frames. If boolean is ignored. The idea is you pass this.debug and users can set this.debug to a string to filter. */
218
+ /** Object key to filter the state by, e.g. state.win.frames. If false is ignored. The idea is you pass this.debug and users can set this.debug to a string to filter. */
181
219
  key?: string | boolean
182
220
  ): void {
221
+ if (key === false) return
183
222
  let res = { state, pluginState }
184
- let pickedRes: any = {}
185
223
  if (typeof key === "string" && key !== "") {
186
- pickedRes = {} as any
187
224
  const paths = key.split(",").map(_ => _.trim()).filter(_ => _)
225
+ const pickedRes: Record<string, any> = {}
188
226
  for (const path of paths) {
189
227
  pickedRes[path] = get(res, path.split("."))
190
228
  }
@@ -96,12 +96,11 @@ export class FrameDragAction implements IDragAction {
96
96
  state: DragState,
97
97
  result: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError | undefined
98
98
  ): FrameDragDeco[] {
99
- let decos: FrameDragDeco[] = []
100
99
  const isError = result instanceof Error
101
100
  const frame = state.dragHoveredFrame
102
101
 
103
102
  if (!matchedZone || !frame) {
104
- decos = []
103
+ const decos: FrameDragDeco[] = []
105
104
  this.modifyDecos(decos)
106
105
  return decos
107
106
  }
@@ -112,13 +111,13 @@ export class FrameDragAction implements IDragAction {
112
111
  ? `deco-frame-drag deco-frame-drag-error deco-frame-drag-${matchedZone.type}-${matchedZone.side} bg-red-500/50`
113
112
  : `deco-frame-drag deco-frame-drag-${matchedZone.type}-${matchedZone.side} bg-blue-500/50`
114
113
 
115
- decos = [{
114
+ const decos: FrameDragDeco[] = [{
116
115
  id: frame.id,
117
116
  type: "drop",
118
117
  position: matchedZone.side ?? "center",
119
118
  shapes: [
120
119
  {
121
- type: "square",
120
+ type: "rect",
122
121
  data: matchedZone,
123
122
  attrs: clipPath
124
123
  ? { class: classes, style: `clip-path:${clipPath}` }
@@ -160,7 +159,7 @@ export class FrameDragAction implements IDragAction {
160
159
  // for the hints
161
160
  if (!(this.state.lastReturn instanceof Error)) this.state.lastReturn.info = "swap"
162
161
  }
163
- } else if (matchedZone.type === "window") {
162
+ } else if (matchedZone.type === "window" && dragHoveredFrame && !dragHoveredFrame.docked) {
164
163
  this.state.lastReturn = getFrameDockInfo(win, draggingFrameId!, matchedZone.side)
165
164
  // for the hints
166
165
  if (!(this.state.lastReturn instanceof Error)) this.state.lastReturn.info = "dock"
@@ -1,16 +1,17 @@
1
1
  import { DragActionHandler } from "./DragActionHandler.js"
2
- import type { ActionDragChangeResult, DragState, IDragAction } from "./types.js"
3
2
 
4
3
  import { getEdgeOrientation } from "../helpers/getEdgeOrientation.js"
5
4
  import { oppositeSide } from "../helpers/oppositeSide.js"
6
5
  import { applyFrameChanges } from "../layout/applyFrameChanges.js"
7
6
  import { createSplitDecoFromDrag } from "../layout/createSplitDecoFromDrag.js"
8
7
  import { getFrameSplitInfo } from "../layout/getFrameSplitInfo.js"
9
- import type { LayoutShape, Point, SplitDeco } from "../types/index.js"
8
+ import type { ActionDragChangeResult, DragState, IDragAction, LayoutShape, Point, SplitDeco } from "../types/index.js"
10
9
  import type { KnownError } from "../utils/KnownError.js"
11
10
 
12
11
  export type SplitInfo = Exclude<ReturnType<typeof getFrameSplitInfo>, KnownError>
13
12
 
13
+ // note that we dont annotateEdges because is possible to forexample drag from a window edge to a frame that can be split, moving over a collapsed frame that was sharing the edge but can't be split
14
+
14
15
  export class SplitAction implements IDragAction {
15
16
  name = "split" as const
16
17
 
@@ -180,7 +181,7 @@ export class SplitAction implements IDragAction {
180
181
  if (!ok) {
181
182
  if (dragHoveredFrame && dragHoveredFrame.docked) {
182
183
  const errorDeco: LayoutShape = {
183
- type: "square",
184
+ type: "rect",
184
185
  data: {
185
186
  x: dragHoveredFrame.x,
186
187
  y: dragHoveredFrame.y,
@@ -1,7 +1,8 @@
1
1
  import { CloseAction } from "./CloseAction"
2
2
  import { FrameDragAction } from "./FrameDragAction.js"
3
3
  import { SplitAction } from "./SplitAction.js"
4
- import type { IDragAction } from "./types.js"
4
+
5
+ import type { IDragAction } from "../types/index.js"
5
6
 
6
7
  /**
7
8
  * Creates the default drag actions (Split, Close, FrameDrag).
@@ -1,7 +1,8 @@
1
1
  import { CloseAction } from "./CloseAction"
2
2
  import { FrameDragAction } from "./FrameDragAction.js"
3
3
  import { SplitAction } from "./SplitAction.js"
4
- import type { IDragAction } from "./types.js"
4
+
5
+ import type { IDragAction } from "../types/index.js"
5
6
 
6
7
  /**
7
8
  * Creates the default drag actions (Split, Close, FrameDrag).
@@ -0,0 +1,331 @@
1
+ import { castType } from "@alanscodelog/utils/castType"
2
+
3
+ import { settings } from "../settings.js"
4
+ import type { LayoutWindow, Point, Size } from "../types/index.js"
5
+
6
+ /**
7
+ * See also {@link createDebugLabeler} for customizing the labeling function, for
8
+ * example to debug the pixel sizes instead of position/size:
9
+ *
10
+ * ```ts
11
+ * consoleDebugWindow(clone, 50, createDebugLabeler({
12
+ * showPxSize: true,
13
+ * showSize: false,
14
+ * showPos: false,
15
+ * getWin: () => clone
16
+ * }))
17
+ *
18
+ * @experimental
19
+ */
20
+ export function consoleDebugWindow(
21
+ win: LayoutWindow,
22
+ targetWidth = 100,
23
+ labelFunc = createDebugLabeler()
24
+ ) {
25
+ const consoleCharAspect = 0.5
26
+
27
+ const windowAspect = win.pxWidth / win.pxHeight
28
+
29
+ const effectiveRatio = (1 / windowAspect) * consoleCharAspect
30
+ // eslint-disable-next-line no-console
31
+ console.log(boxesToText(Object.values(win.frames), targetWidth, {
32
+ heightScale: effectiveRatio,
33
+ labelFunc
34
+ }))
35
+ }
36
+
37
+ type Border = {
38
+ horizontal: string
39
+ vertical: string
40
+ topLeft: string
41
+ topRight: string
42
+ bottomLeft: string
43
+ bottomRight: string
44
+ cross: string
45
+ fill: string
46
+ labelBg: string
47
+ }
48
+
49
+ export function boxesToText<T extends Size & Point & { id?: string }>(
50
+ boxes: T[],
51
+ targetWidth = 100,
52
+ {
53
+ border = {} as any,
54
+ heightScale = 1,
55
+ labelFunc = () => ""
56
+ }: {
57
+ border?: Partial<Border>
58
+ heightScale?: number
59
+ labelFunc?: (box: T) => string
60
+ } = {}
61
+ ) {
62
+ border = {
63
+ horizontal: "─",
64
+ vertical: "│",
65
+ topLeft: "┌",
66
+ topRight: "┐",
67
+ bottomLeft: "└",
68
+ bottomRight: "┘",
69
+ cross: "┼",
70
+ fill: " ",
71
+ labelBg: " ",
72
+ ...border
73
+ }
74
+ castType<Border>(border)
75
+
76
+ const normBoxes = boxes.map(box => ({
77
+ ...box,
78
+ original: box,
79
+ id: box.id,
80
+ x: box.x / 100,
81
+ y: box.y / 100,
82
+ width: box.width / 100,
83
+ height: box.height / 100
84
+ }))
85
+
86
+ let minX = Infinity, minY = Infinity
87
+ let maxX = -Infinity, maxY = -Infinity
88
+
89
+ normBoxes.forEach(box => {
90
+ minX = Math.min(minX, box.x)
91
+ minY = Math.min(minY, box.y)
92
+ maxX = Math.max(maxX, box.x + box.width)
93
+ maxY = Math.max(maxY, box.y + box.height)
94
+ })
95
+
96
+ const availableWidth = targetWidth
97
+ const availableHeight = Math.floor(availableWidth * heightScale)
98
+
99
+ const totalWidth = maxX - minX || 1
100
+ const totalHeight = maxY - minY || 1
101
+
102
+ /* eslint-disable @typescript-eslint/naming-convention */
103
+ const scaleX_final = availableWidth / totalWidth
104
+ const scaleY_final = availableHeight / totalHeight
105
+ /* eslint-enable @typescript-eslint/naming-convention */
106
+
107
+ const scaledBoxes = normBoxes.map(box => {
108
+ const idPart = box.id !== undefined && box.id !== null && box.id !== ""
109
+ ? String(box.id)
110
+ : "??"
111
+ const funcLabel = labelFunc(box.original)
112
+ const label = funcLabel && funcLabel.trim()
113
+ ? `${idPart} ${funcLabel}`
114
+ : idPart
115
+
116
+ return {
117
+ id: box.id,
118
+ x: (box.x - minX) * scaleX_final,
119
+ y: (box.y - minY) * scaleY_final,
120
+ width: box.width * scaleX_final,
121
+ height: box.height * scaleY_final,
122
+ label: label,
123
+ original: box
124
+ }
125
+ })
126
+
127
+ const cols = Math.min(availableWidth, Math.floor(totalWidth * scaleX_final))
128
+ const rows = Math.min(availableHeight, Math.floor(totalHeight * scaleY_final))
129
+
130
+ if (cols < 1 || rows < 1) {
131
+ return new Error("Grid too small to display")
132
+ }
133
+
134
+ type CellData = {
135
+ char: string
136
+ priority: number
137
+ type: "horizontal" | "vertical" | "corner" | "cross" | "fill" | "label"
138
+ boxIndex: number
139
+ }
140
+
141
+ const grid: CellData[][] = Array(rows).fill(null).map(() =>
142
+ Array(cols).fill(null).map(() => ({
143
+ char: border.fill,
144
+ priority: 0,
145
+ type: "fill" as const,
146
+ boxIndex: -1
147
+ }))
148
+ )
149
+
150
+ function setCell(x: number, y: number, char: string, priority: number, type: CellData["type"], boxIndex: number) {
151
+ if (x < 0 || y < 0 || x >= cols || y >= rows) return
152
+
153
+ const current = grid[y][x]
154
+
155
+ if (boxIndex > current.boxIndex
156
+ || (boxIndex === current.boxIndex && priority > current.priority)) {
157
+ grid[y][x] = { char, priority, type, boxIndex }
158
+ }
159
+ }
160
+
161
+ function getCell(x: number, y: number): CellData | undefined {
162
+ if (x < 0 || y < 0 || x >= cols || y >= rows) return undefined
163
+ return grid[y][x]
164
+ }
165
+
166
+ for (let boxIndex = scaledBoxes.length - 1; boxIndex >= 0; boxIndex--) {
167
+ const box = scaledBoxes[boxIndex]
168
+ const x1 = Math.round(box.x)
169
+ const y1 = Math.round(box.y)
170
+ const x2 = Math.round(box.x + box.width)
171
+ const y2 = Math.round(box.y + box.height)
172
+
173
+ const clampedX1 = Math.max(0, Math.min(cols - 1, x1))
174
+ const clampedY1 = Math.max(0, Math.min(rows - 1, y1))
175
+ const clampedX2 = Math.max(0, Math.min(cols - 1, x2))
176
+ const clampedY2 = Math.max(0, Math.min(rows - 1, y2))
177
+
178
+ if (clampedX1 >= clampedX2 || clampedY1 >= clampedY2) continue
179
+
180
+ const edgePriority = 10
181
+
182
+ if (clampedY1 >= 0 && clampedY1 < rows) {
183
+ for (let x = clampedX1 + 1; x < clampedX2; x++) {
184
+ const cell = getCell(x, clampedY1)
185
+ if (cell && cell.type === "vertical" && cell.boxIndex !== boxIndex) {
186
+ setCell(x, clampedY1, border.cross, edgePriority, "cross", boxIndex)
187
+ } else {
188
+ setCell(x, clampedY1, border.horizontal, edgePriority, "horizontal", boxIndex)
189
+ }
190
+ }
191
+ }
192
+
193
+ if (clampedY2 >= 0 && clampedY2 < rows && clampedY2 !== clampedY1) {
194
+ for (let x = clampedX1 + 1; x < clampedX2; x++) {
195
+ const cell = getCell(x, clampedY2)
196
+ if (cell && cell.type === "vertical" && cell.boxIndex !== boxIndex) {
197
+ setCell(x, clampedY2, border.cross, edgePriority, "cross", boxIndex)
198
+ } else {
199
+ setCell(x, clampedY2, border.horizontal, edgePriority, "horizontal", boxIndex)
200
+ }
201
+ }
202
+ }
203
+
204
+ if (clampedX1 >= 0 && clampedX1 < cols) {
205
+ for (let y = clampedY1 + 1; y < clampedY2; y++) {
206
+ const cell = getCell(clampedX1, y)
207
+ if (cell && (cell.type === "horizontal" || cell.type === "cross") && cell.boxIndex !== boxIndex) {
208
+ setCell(clampedX1, y, border.cross, edgePriority, "cross", boxIndex)
209
+ } else {
210
+ setCell(clampedX1, y, border.vertical, edgePriority, "vertical", boxIndex)
211
+ }
212
+ }
213
+ }
214
+
215
+ if (clampedX2 >= 0 && clampedX2 < cols && clampedX2 !== clampedX1) {
216
+ for (let y = clampedY1 + 1; y < clampedY2; y++) {
217
+ const cell = getCell(clampedX2, y)
218
+ if (cell && (cell.type === "horizontal" || cell.type === "cross") && cell.boxIndex !== boxIndex) {
219
+ setCell(clampedX2, y, border.cross, edgePriority, "cross", boxIndex)
220
+ } else {
221
+ setCell(clampedX2, y, border.vertical, edgePriority, "vertical", boxIndex)
222
+ }
223
+ }
224
+ }
225
+
226
+ const cornerPriority = 20
227
+ const corners = [
228
+ { x: clampedX1, y: clampedY1, char: border.topLeft, type: "corner" as const },
229
+ { x: clampedX2, y: clampedY1, char: border.topRight, type: "corner" as const },
230
+ { x: clampedX1, y: clampedY2, char: border.bottomLeft, type: "corner" as const },
231
+ { x: clampedX2, y: clampedY2, char: border.bottomRight, type: "corner" as const }
232
+ ]
233
+
234
+ corners.forEach(corner => {
235
+ if (corner.x < 0 || corner.y < 0 || corner.x >= cols || corner.y >= rows) return
236
+
237
+ const cell = getCell(corner.x, corner.y)
238
+ if (cell && cell.boxIndex !== boxIndex && cell.type !== "fill") {
239
+ if (cell.type === "horizontal" || cell.type === "vertical" || cell.type === "cross") {
240
+ setCell(corner.x, corner.y, border.cross, cornerPriority, "cross", boxIndex)
241
+ } else {
242
+ setCell(corner.x, corner.y, corner.char, cornerPriority, corner.type, boxIndex)
243
+ }
244
+ } else {
245
+ setCell(corner.x, corner.y, corner.char, cornerPriority, corner.type, boxIndex)
246
+ }
247
+ })
248
+
249
+ const labelPriority = 30
250
+ if (box.label && box.label.trim().length > 0) {
251
+ const labelText = String(box.label)
252
+
253
+ const boxWidth = clampedX2 - clampedX1
254
+ const boxHeight = clampedY2 - clampedY1
255
+
256
+ const interiorWidth = boxWidth - 2
257
+ const interiorHeight = boxHeight - 2
258
+
259
+ if (interiorWidth >= 1 && interiorHeight >= 0) {
260
+ let displayText = labelText
261
+ if (displayText.length > interiorWidth) {
262
+ displayText = displayText.substring(0, interiorWidth)
263
+ }
264
+
265
+ const interiorLeft = clampedX1 + 1
266
+ const interiorRight = clampedX2 - 1
267
+ const interiorTop = clampedY1 + 1
268
+ const interiorBottom = clampedY2 - 1
269
+
270
+ const interiorCenterX = Math.floor((interiorLeft + interiorRight) / 2)
271
+ const interiorCenterY = Math.floor((interiorTop + interiorBottom) / 2)
272
+
273
+ let startX = interiorCenterX - Math.floor(displayText.length / 2)
274
+
275
+ if (startX < interiorLeft) {
276
+ startX = interiorLeft
277
+ }
278
+ if (startX + displayText.length > interiorRight) {
279
+ startX = interiorRight - displayText.length
280
+ }
281
+
282
+ for (let i = 0; i < displayText.length; i++) {
283
+ const xPos = startX + i
284
+ const yPos = interiorCenterY
285
+
286
+ if (xPos >= interiorLeft && xPos < interiorRight
287
+ && yPos >= interiorTop && yPos < interiorBottom) {
288
+ setCell(xPos, yPos, border.labelBg, labelPriority, "label", boxIndex)
289
+ setCell(xPos, yPos, displayText[i], labelPriority + 1, "label", boxIndex)
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ const displayGrid = grid.map(row => row.map(cell => cell.char).join("")).join("\n")
297
+ return displayGrid
298
+ }
299
+
300
+
301
+ export function createDebugLabeler({
302
+ showPxSize = false,
303
+ showCollapsed = true,
304
+ showDocked = true,
305
+ showSize = true,
306
+ showPos = true,
307
+ getWin
308
+ }: {
309
+ showPxSize?: boolean
310
+ showCollapsed?: boolean
311
+ showDocked?: boolean
312
+ showSize?: boolean
313
+ showPos?: boolean
314
+ getWin?: (f: LayoutWindow["frames"][string]) => Pick<LayoutWindow, "pxWidth" | "pxHeight">
315
+ } = {}) {
316
+ return function (f: LayoutWindow["frames"][string]) {
317
+ if (showPxSize && !getWin) throw new Error("showPxSize is true but getWin is not provided")
318
+ const win = showPxSize ? getWin?.(f) : undefined
319
+
320
+ const widthPx = showPxSize ? Math.round(f.width / settings.maxInt * win!.pxWidth) : undefined
321
+ const heightPx = showPxSize ? Math.round(f.height / settings.maxInt * win!.pxHeight) : undefined
322
+ const isCollapsed = showCollapsed && typeof f.collapsed === "number" ? "~" : ""
323
+ const isDocked = showDocked && f.docked ? "*" : ""
324
+ const size = showSize ? ` ${f.width}x${f.height}` : ""
325
+ const pos = showPos ? ` ${f.x},${f.y}` : ""
326
+ const pxSize = showPxSize ? ` ${widthPx}x${heightPx}px` : ""
327
+
328
+ return `${isCollapsed}${isDocked}${size}${pos}${pxSize}`
329
+ }
330
+ }
331
+
@@ -1,4 +1,4 @@
1
- import { cloneDeep } from "es-toolkit/object"
1
+ import { walk } from "@alanscodelog/utils/walk"
2
2
 
3
3
  import type {
4
4
  LayoutWindow,
@@ -8,7 +8,7 @@ import type {
8
8
  export function convertLayoutWindowToWorkspace(layout: LayoutWindow | Workspace): Workspace {
9
9
  // clone to avoid accidental property removal issues
10
10
  // cast to be able to delete the properties
11
- const l = cloneDeep(layout) as Partial<LayoutWindow>
11
+ const l = walk(layout, undefined, { save: true }) as Partial<LayoutWindow>
12
12
  delete l.id
13
13
  delete l.pxHeight
14
14
  delete l.pxWidth