@witchcraft/layout 0.4.3 → 0.4.4

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 (50) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/LayoutWindow.d.vue.ts +10 -1
  3. package/dist/runtime/components/LayoutWindow.vue +4 -4
  4. package/dist/runtime/components/LayoutWindow.vue.d.ts +10 -1
  5. package/dist/runtime/composables/useFrames.d.ts +37 -22
  6. package/dist/runtime/composables/useFrames.js +32 -8
  7. package/dist/runtime/demo/tailwind.css +1 -1
  8. package/dist/runtime/drag/DragActionHandler.d.ts +4 -1
  9. package/dist/runtime/drag/DragActionHandler.js +2 -2
  10. package/dist/runtime/helpers/findEdgesTouchingWindow.d.ts +7 -0
  11. package/dist/runtime/helpers/findEdgesTouchingWindow.js +20 -0
  12. package/dist/runtime/helpers/index.d.ts +8 -1
  13. package/dist/runtime/helpers/index.js +8 -1
  14. package/dist/runtime/layout/debugFrame.js +5 -1
  15. package/dist/runtime/layout/getFrameCollapseInfo.d.ts +1 -1
  16. package/dist/runtime/layout/getFrameCollapseInfo.js +9 -29
  17. package/dist/runtime/layout/getFrameDockInfo.d.ts +1 -1
  18. package/dist/runtime/layout/getFrameDockInfo.js +61 -44
  19. package/dist/runtime/layout/getFrameExpandInfo.d.ts +11 -0
  20. package/dist/runtime/layout/getFrameExpandInfo.js +49 -0
  21. package/dist/runtime/layout/getFrameShrinkInfo.d.ts +14 -0
  22. package/dist/runtime/layout/getFrameShrinkInfo.js +48 -0
  23. package/dist/runtime/layout/getFrameUncollapseInfo.d.ts +1 -1
  24. package/dist/runtime/layout/getFrameUncollapseInfo.js +6 -20
  25. package/dist/runtime/layout/getUpdateWindowSizeInfo.js +10 -12
  26. package/dist/runtime/layout/index.d.ts +3 -0
  27. package/dist/runtime/layout/index.js +3 -0
  28. package/dist/runtime/types/index.d.ts +70 -0
  29. package/dist/runtime/types/index.js +2 -1
  30. package/dist/runtime/types/vue.d.ts +12 -3
  31. package/package.json +3 -2
  32. package/src/runtime/components/LayoutWindow.vue +5 -5
  33. package/src/runtime/composables/useFrames.ts +46 -32
  34. package/src/runtime/demo/tailwind.css +4 -0
  35. package/src/runtime/drag/DragActionHandler.ts +3 -3
  36. package/src/runtime/helpers/findEdgesTouchingWindow.ts +30 -0
  37. package/src/runtime/helpers/index.ts +8 -1
  38. package/src/runtime/layout/debugFrame.ts +6 -1
  39. package/src/runtime/layout/getFrameCollapseInfo.ts +12 -41
  40. package/src/runtime/layout/getFrameDockInfo.ts +77 -57
  41. package/src/runtime/layout/getFrameExpandInfo.ts +84 -0
  42. package/src/runtime/layout/getFrameShrinkInfo.ts +87 -0
  43. package/src/runtime/layout/getFrameUncollapseInfo.ts +8 -30
  44. package/src/runtime/layout/getUpdateWindowSizeInfo.ts +16 -22
  45. package/src/runtime/layout/index.ts +3 -0
  46. package/src/runtime/types/index.ts +39 -1
  47. package/src/runtime/types/vue.ts +5 -3
  48. package/dist/runtime/drag/defaultDragActions.d.ts +0 -9
  49. package/dist/runtime/drag/defaultDragActions.js +0 -10
  50. package/src/runtime/drag/defaultDragActions.ts +0 -20
@@ -1,5 +1,5 @@
1
1
  import type { ComputedRef, InjectionKey, Ref } from "vue";
2
- import type { Direction, DragState, Edge, FrameId, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, Orientation, Point } from "./index.js";
2
+ import type { ActionHandler, Direction, DragState, Edge, FrameId, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, Orientation, Point } from "./index.js";
3
3
  export type LayoutContext = ComputedRef<{
4
4
  /** The owning window, needed so we can correctly scale coordinates. */
5
5
  win: LayoutWindow;
@@ -8,14 +8,23 @@ export type LayoutContext = ComputedRef<{
8
8
  }>;
9
9
  export declare const layoutContextInjectionKey: InjectionKey<LayoutContext>;
10
10
  export interface UseFramesContext {
11
+ actionHandler: ActionHandler;
11
12
  dragStart: {
12
13
  (e: PointerEvent, type: "edge", data: {
13
14
  edge?: Edge;
14
15
  intersection?: IntersectionEntry;
15
- }): void;
16
+ }, opts?: {
17
+ moveEvent?: string;
18
+ endEvent?: string;
19
+ context?: Record<string, unknown>;
20
+ }): Promise<any>;
16
21
  (e: PointerEvent, type: "frame", data: {
17
22
  frameId: FrameId;
18
- }): void;
23
+ }, opts?: {
24
+ moveEvent?: string;
25
+ endEvent?: string;
26
+ context?: Record<string, unknown>;
27
+ }): Promise<any>;
19
28
  };
20
29
  dragMove: (e: PointerEvent) => void;
21
30
  dragEnd: (e?: PointerEvent, options?: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/layout",
3
3
  "description": "Headless layout manager.",
4
- "version": "0.4.3",
4
+ "version": "0.4.4",
5
5
  "main": "./dist/runtime/index.js",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -49,6 +49,7 @@
49
49
  "@alanscodelog/semantic-release-config": "^6.0.2",
50
50
  "@alanscodelog/tsconfigs": "^6.3.0",
51
51
  "@alanscodelog/vite-config": "^0.0.7",
52
+ "@babel/runtime": "^7.20.7",
52
53
  "@commitlint/cli": "^19.8.1",
53
54
  "@nuxt/eslint-config": "^1.15.2",
54
55
  "@nuxt/kit": "^4.4.6",
@@ -145,6 +146,6 @@
145
146
  "doc:dev": "concurrently \"pnpm doc:watch\" \"pnpm doc:serve\"",
146
147
  "doc:check-invalid": "typedoc --listInvalidSymbolLinks",
147
148
  "actions:debug": "act -r -v -j release",
148
- "gen:exports": "indexit update --ignore **/*.d.ts **.vue src/assets/** -o '${path}.js'"
149
+ "gen:exports": "indexit update --ignore **/*.d.ts **.vue src/assets/** **/demo/** **/dist/** -o '${path}.js'"
149
150
  }
150
151
  }
@@ -7,7 +7,7 @@
7
7
  flex
8
8
  flex-col
9
9
  `,
10
- isDragging && `dragging cursor-pointer user-select-none`,
10
+ isDragging && `dragging cursor-pointer user-select-none pointer-events-none`,
11
11
  requestType && `request-${requestType}`,
12
12
  ($attrs as any).class
13
13
  )"
@@ -70,7 +70,7 @@
70
70
  <script lang="ts" setup>
71
71
  import { useGlobalResizeObserver } from "@witchcraft/ui/composables/useGlobalResizeObserver"
72
72
  import { twMerge } from "@witchcraft/ui/utils/twMerge"
73
- import { computed, provide, reactive, ref, useAttrs, watch } from "vue"
73
+ import { computed, markRaw, provide, reactive, ref, useAttrs, watch } from "vue"
74
74
 
75
75
 
76
76
  import { useFrames } from "../composables/useFrames.js"
@@ -90,7 +90,7 @@ const $attrs = useAttrs()
90
90
  const win = defineModel<LayoutWindow>("win", { required: true })
91
91
 
92
92
  const props = withDefaults(defineProps<{
93
- /** If you really need it, you can provide your own drag action handler, note the actionHandler's prop will no longer have any effect.*/
93
+ /** If you really need it, you can provide your own drag action handler (remember to markRaw it), note the actionHandler's prop will no longer have any effect.*/
94
94
  dragActionHandler?: DragActionHandler<any, any>
95
95
  /** Custom drag action handlers. Falls back to default split/close/frame handlers if not provided. */
96
96
  actionHandlers?: IDragAction[]
@@ -122,7 +122,7 @@ const windowEl = ref<HTMLElement | null>(null)
122
122
 
123
123
  const requestType = ref<string | undefined | string>()
124
124
 
125
- const dragActionHandler = props.dragActionHandler ?? new DragActionHandler(
125
+ const dragActionHandler = props.dragActionHandler ?? markRaw(new DragActionHandler(
126
126
  [
127
127
  ...(props.actionHandlers ?? createDefaultHandlers())
128
128
  ],
@@ -141,7 +141,7 @@ const dragActionHandler = props.dragActionHandler ?? new DragActionHandler(
141
141
  showDragging.value = true
142
142
  },
143
143
  }
144
- )
144
+ ))
145
145
  dragActionHandler.shapes = reactive([])
146
146
  dragActionHandler.textHints = reactive({ actions: [], errors: [] })
147
147
  const shapes = dragActionHandler.shapes
@@ -12,30 +12,11 @@ import { moveEdge } from "../helpers/moveEdge.js"
12
12
  import { toWindowCoord } from "../helpers/toWindowCoord.js"
13
13
  import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
14
14
  import { isPointInRect } from "../layout/isPointInRect.js"
15
- import type { Direction, DragChangeHandler, DragChangeResult, DragState, Edge, EdgeDragStartData, FrameDragStartData, FrameId, IntersectionEntry, LayoutFrame, LayoutWindow, Orientation, Point } from "../types/index.js"
15
+ import type { ActionHandler, Direction, DragState, Edge, EdgeDragStartData, FrameDragStartData, FrameId, IntersectionEntry, LayoutFrame, LayoutWindow, Orientation, Point } from "../types/index.js"
16
16
 
17
17
  export function useFrames(
18
18
  win: Ref<LayoutWindow>,
19
- handler: {
20
- eventHandler: (e: KeyboardEvent, state: DragState, forceRecalculateEdges: () => void) => void
21
- /**
22
- * Called when the drag coordinates change (during any event). Should return true to allow the edges to be updated/moved, or false to prevent it.
23
- *
24
- * Can return anything for the end event as it's ignored.
25
- *
26
- * Can be used to save some context/info to later apply safely during onDragApply.
27
- */
28
- onDragChange: (...args: Parameters<DragChangeHandler>) => DragChangeResult
29
- /**
30
- * Called when drag will be applied. If dragEnd was called with apply false, it will not be called.
31
- * Return false to not apply the regular drag end changes (i.e. return false to reset to the position before dragging).
32
- */
33
- onDragApply: ((state: DragState, forceRecalculateEdges: () => void) => boolean)
34
- /**
35
- * Called after visual edges are recalculated. Action handlers can annotate edges with error info.
36
- */
37
- annotateEdges?: (edges: Edge[], frames: LayoutFrame[]) => void
38
- }
19
+ handler: ActionHandler
39
20
  ) {
40
21
  const draggingEdges = ref<Edge[]>([])
41
22
 
@@ -66,6 +47,7 @@ export function useFrames(
66
47
  const isDraggingFromWindowEdge = ref<boolean>(false)
67
48
 
68
49
  const frameDragFrameId = ref<FrameId | undefined>()
50
+ const eventContext = ref<Record<string, unknown> | undefined>()
69
51
 
70
52
  const frames = computed(() =>
71
53
  isDragging.value && showDragging.value
@@ -126,14 +108,21 @@ export function useFrames(
126
108
  dragHoveredFrame: dragHoveredFrame.value,
127
109
  intersections: intersections.value,
128
110
  isDraggingFromWindowEdge: isDraggingFromWindowEdge.value,
111
+ eventContext: eventContext.value,
129
112
  win: win.value
130
113
  } satisfies DragState))
131
114
 
132
115
  let controller: AbortController
116
+ let dragResolve: ((result: any) => void) | undefined
117
+ let dragResult: any
133
118
 
134
119
  function resetState(): void {
135
120
  dragStartPoint = undefined
136
121
 
122
+ dragResolve?.(dragResult)
123
+ dragResolve = undefined
124
+ dragResult = undefined
125
+
137
126
  draggingEdges.value = []
138
127
  draggingIntersection.value = undefined
139
128
  isDragging.value = false
@@ -141,6 +130,7 @@ export function useFrames(
141
130
  dragDistance.value = -1
142
131
  touchingFrames.value = []
143
132
  frameDragFrameId.value = undefined
133
+ eventContext.value = undefined
144
134
  dragDirStore.reset()
145
135
  showDragging.value = false
146
136
  forceRecalculateEdges()
@@ -150,19 +140,38 @@ export function useFrames(
150
140
  e: PointerEvent,
151
141
  type: T,
152
142
  // someday this will work
153
- data: T extends "edge" ? EdgeDragStartData : FrameDragStartData
154
- ): void {
143
+ data: T extends "edge" ? EdgeDragStartData : FrameDragStartData,
144
+ {
145
+ moveEvent = "pointermove",
146
+ endEvent = "pointerup",
147
+ context
148
+ }: {
149
+ moveEvent?: string
150
+ endEvent?: string
151
+ context?: Record<string, unknown>
152
+ } = {}
153
+ ): Promise<any> {
154
+ // if already dragging, abort previous (resetState resolves the old promise)
155
+ if (controller) {
156
+ controller.abort()
157
+ }
158
+
159
+ const promise = new Promise<any>(resolve => {
160
+ dragResolve = resolve
161
+ })
162
+
155
163
  controller = new AbortController()
156
164
  controller.signal.addEventListener("abort", () => resetState())
157
165
 
158
166
  e.preventDefault()
159
- window.addEventListener("pointermove", dragMove, { signal: controller.signal })
160
- window.addEventListener("pointerup", dragEnd, { signal: controller.signal })
167
+ window.addEventListener(moveEvent, dragMove as EventListener, { signal: controller.signal })
168
+ window.addEventListener(endEvent, dragEnd as EventListener, { signal: controller.signal })
161
169
 
162
170
  const point = toWindowCoord(win.value, e)
163
171
  dragPoint.value = point
164
172
  dragStartPoint = { ...point }
165
173
  dragDirStore.update(point)
174
+ eventContext.value = context
166
175
 
167
176
  isDragging.value = type
168
177
  showDragging.value = true
@@ -174,7 +183,7 @@ export function useFrames(
174
183
  draggingIntersection.value = intersection
175
184
 
176
185
  draggingEdges.value = edge
177
- ? [edge]
186
+ ? [edge!]
178
187
  : [
179
188
  ...(draggingIntersection.value?.sharedEdges.horizontal ?? []),
180
189
  ...(draggingIntersection.value?.sharedEdges.vertical ?? [])
@@ -206,6 +215,7 @@ export function useFrames(
206
215
 
207
216
  const res = handler.onDragChange("start", e, state.value, forceRecalculateEdges, cancel)
208
217
  showDragging.value = res.showDragging ?? true
218
+ return promise
209
219
  }
210
220
 
211
221
  function dragMove(e: PointerEvent): void {
@@ -246,17 +256,20 @@ export function useFrames(
246
256
  dragPoint.value = point
247
257
  }
248
258
 
249
- const doApply = apply && handler.onDragApply(state.value, forceRecalculateEdges)
250
- if (doApply) {
251
- for (const frame of touchingFramesArrays.value.flat()) {
252
- win!.value.frames[frame.id] = frame
259
+ if (apply) {
260
+ const applyResult = handler.onDragApply(state.value, forceRecalculateEdges)
261
+ dragResult = applyResult.result
262
+ if (applyResult.apply) {
263
+ for (const frame of touchingFramesArrays.value.flat()) {
264
+ win!.value.frames[frame.id] = frame
265
+ }
253
266
  }
254
267
  }
255
268
 
256
269
  handler.onDragChange("end", e, state.value, forceRecalculateEdges, cancel)
257
270
 
258
271
  // this can get called from elsewhere
259
- // also takes care of cleanup
272
+ // also takes care of cleanup nd resolving promise
260
273
  controller?.abort()
261
274
  }
262
275
  function cancel(): void {
@@ -296,6 +309,7 @@ export function useFrames(
296
309
  forceRecalculateEdges,
297
310
  state,
298
311
  showDragging,
299
- frameDragFrameId
312
+ frameDragFrameId,
313
+ actionHandler: handler
300
314
  }
301
315
  }
@@ -5,3 +5,7 @@
5
5
 
6
6
  /* Only needed for the dev playground: the demo App.vue lives outside the Vite project root, so Tailwind's auto-scan doesn't find the [&.deco-...] classes it defines. */
7
7
  @source "../../../src/runtime/demo";
8
+
9
+ button {
10
+ cursor: pointer;
11
+ }
@@ -191,14 +191,14 @@ export class DragActionHandler<
191
191
  onDragApply(
192
192
  state: DragState,
193
193
  forceRecalculateEdges: () => void
194
- ): boolean {
194
+ ): { apply: boolean, result: any } {
195
195
  if (this.activeAction) {
196
196
  const res = this.actions[this.activeAction]!.onDragApply(state, forceRecalculateEdges)
197
197
  this.hooks.onEnd?.({ cancelled: false, applied: res })
198
- return false
198
+ return { apply: res, result: undefined }
199
199
  }
200
200
  this.hooks.onEnd?.({ cancelled: false, applied: false })
201
- return true
201
+ return { apply: true, result: undefined }
202
202
  }
203
203
 
204
204
  cancel(e: PointerEvent | KeyboardEvent | undefined, state: DragState): void {
@@ -0,0 +1,30 @@
1
+ import { settings } from "../settings.js"
2
+ import type { EdgeSide, LayoutFrame } from "../types/index.js"
3
+
4
+ /**
5
+ * Checks which window edges a frame touches and how.
6
+ *
7
+ * @returns Record mapping touched sides to `"full"` (spans the entire edge) or `"partial"` (touches but doesn't span fully). Only touched sides are included as keys.
8
+ */
9
+ export function findEdgesTouchingWindow(frame: LayoutFrame): Partial<Record<EdgeSide, "full" | "partial">> {
10
+ const maxInt = settings.maxInt
11
+ const result: Partial<Record<EdgeSide, "full" | "partial">> = {}
12
+
13
+ const spansVertical = frame.y === 0 && frame.y + frame.height === maxInt
14
+ const spansHorizontal = frame.x === 0 && frame.x + frame.width === maxInt
15
+
16
+ if (frame.x === 0) {
17
+ result.left = spansVertical ? "full" : "partial"
18
+ }
19
+ if (frame.x + frame.width === maxInt) {
20
+ result.right = spansVertical ? "full" : "partial"
21
+ }
22
+ if (frame.y === 0) {
23
+ result.top = spansHorizontal ? "full" : "partial"
24
+ }
25
+ if (frame.y + frame.height === maxInt) {
26
+ result.bottom = spansHorizontal ? "full" : "partial"
27
+ }
28
+
29
+ return result
30
+ }
@@ -12,6 +12,7 @@ export { assertValidWinAndFrameIds } from "./assertValidWinAndFrameIds.js"
12
12
  export { assertWindowHasActiveFrame } from "./assertWindowHasActiveFrame.js"
13
13
  export { cloneFrame } from "./cloneFrame.js"
14
14
  export { cloneFrames } from "./cloneFrames.js"
15
+ export { consoleDebugWindow } from "./consoleDebugWindow.js"
15
16
  export { containsEdge } from "./containsEdge.js"
16
17
  export { convertLayoutWindowToWorkspace } from "./convertLayoutWindowToWorkspace.js"
17
18
  export { copySize } from "./copySize.js"
@@ -24,17 +25,21 @@ export { doesEdgeContinueEdge } from "./doesEdgeContinueEdge.js"
24
25
  export { edgeToPoints } from "./edgeToPoints.js"
25
26
  export { findDraggableEdge } from "./findDraggableEdge.js"
26
27
  export { findFrameDraggableEdges } from "./findFrameDraggableEdges.js"
28
+ export { framesRedistributeFix } from "./framesRedistributeFix.js"
27
29
  export { frameToEdges } from "./frameToEdges.js"
28
30
  export { frameToPoints } from "./frameToPoints.js"
31
+ export { getDockBoundaries } from "./getDockBoundaries.js"
29
32
  export { getEdgeLength } from "./getEdgeLength.js"
30
33
  export { getEdgeOrientation } from "./getEdgeOrientation.js"
31
34
  export { getEdgeSharedDirection } from "./getEdgeSharedDirection.js"
32
35
  export { getEdgeSide } from "./getEdgeSide.js"
33
36
  export { getFrameById } from "./getFrameById.js"
34
37
  export { getFrameConstant } from "./getFrameConstant.js"
38
+ export { findEdgesTouchingWindow } from "./findEdgesTouchingWindow.js"
35
39
  export { getIntersections } from "./getIntersections.js"
36
40
  export { getIntersectionsCss } from "./getIntersectionsCss.js"
37
41
  export { getMoveEdgeInfo } from "./getMoveEdgeInfo.js"
42
+ export { getPinnedEdgesForCollapsedFrames } from "./getPinnedEdgesForCollapsedFrames.js"
38
43
  export { getResizeLimit } from "./getResizeLimit.js"
39
44
  export { getShapeRectCss } from "./getShapeRectCss.js"
40
45
  export { getSideTouching } from "./getSideTouching.js"
@@ -55,9 +60,10 @@ export { isWindowEdge } from "./isWindowEdge.js"
55
60
  export { isWindowEdgePoint } from "./isWindowEdgePoint.js"
56
61
  export { moveEdge } from "./moveEdge.js"
57
62
  export { numberToScaledPercent } from "./numberToScaledPercent.js"
58
- export { pxSizeToScaledSize } from "./pxSizeToScaledSize.js"
59
63
  export { oppositeSide } from "./oppositeSide.js"
64
+ export { pxSizeToScaledSize } from "./pxSizeToScaledSize.js"
60
65
  export { resizeByEdge } from "./resizeByEdge.js"
66
+ export { rotateLayout } from "./rotateFrames.js"
61
67
  export { scaledPointToPx } from "./scaledPointToPx.js"
62
68
  export { sideToDirection } from "./sideToDirection.js"
63
69
  export { sideToOrientation } from "./sideToOrientation.js"
@@ -67,3 +73,4 @@ export { toId } from "./toId.js"
67
73
  export { toWindowCoord } from "./toWindowCoord.js"
68
74
  export { unionEdges } from "./unionEdges.js"
69
75
  export { updateWindowWithEvent } from "./updateWindowSizeWithEvent.js"
76
+ export { validateLayoutShape } from "./validateLayoutShape.js"
@@ -1,6 +1,11 @@
1
+ import { keys } from "@alanscodelog/utils/keys"
2
+
1
3
  import type { LayoutFrame } from "../types/index.js"
2
4
 
5
+ const builtinProperties = ["id", "x", "y", "width", "height", "docked", "collapsed"]
3
6
  export function debugFrame(frame: LayoutFrame): string {
4
7
  const f = frame
5
- return `id: ${f.id.slice(0, 4)}, x: ${f.x}, y: ${f.y}, w: ${f.width}, h: ${f.height}\ndocked: ${f.docked}, collapsed: ${f.collapsed}`
8
+ const otherProperties = keys(f).filter(k => !builtinProperties.includes(k)).map(k => `${k}: ${f[k]}`)
9
+ const otherPropertiesString = otherProperties.length > 0 ? `, ${otherProperties.join(", ")}` : ""
10
+ return `id: ${f.id.slice(0, 4)}, x: ${f.x}, y: ${f.y}, w: ${f.width}, h: ${f.height}\ndocked: ${f.docked}, collapsed: ${f.collapsed}${otherPropertiesString}`
6
11
  }
@@ -2,13 +2,10 @@ import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
2
2
  import { walk } from "@alanscodelog/utils/walk"
3
3
 
4
4
  import { applyFrameChanges } from "./applyFrameChanges.js"
5
- import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
5
+ import { getFrameShrinkInfo } from "./getFrameShrinkInfo.js"
6
6
 
7
- import { framesRedistributeFix } from "../helpers/framesRedistributeFix.js"
8
- import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
9
- import { oppositeSide } from "../helpers/oppositeSide.js"
10
7
  import { settings } from "../settings.js"
11
- import type { EdgeSide, LayoutChange, LayoutWindow, Size } from "../types/index.js"
8
+ import type { LayoutChange, LayoutWindow, Size } from "../types/index.js"
12
9
  import { LAYOUT_ERROR } from "../types/index.js"
13
10
  import { KnownError } from "../utils/KnownError.js"
14
11
 
@@ -32,7 +29,8 @@ export function getFrameCollapseInfo(
32
29
  } = {}
33
30
  ): LayoutChange
34
31
  | KnownError<typeof LAYOUT_ERROR.CANT_COLLAPSE_NOT_DOCKED>
35
- | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES> {
32
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES>
33
+ | KnownError<typeof LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME> {
36
34
  collapseSizeScaled = collapseSizeScaled ?? settings.getCollapseSizeScaled(win)
37
35
  win = walk(win, undefined, { save: true }) as typeof win
38
36
  const frame = win.frames[frameId]
@@ -51,55 +49,28 @@ export function getFrameCollapseInfo(
51
49
 
52
50
  const isVertical = frame.docked === "left" || frame.docked === "right"
53
51
  const sizeKey = isVertical ? "width" : "height" as const
54
- const posKey = isVertical ? "x" : "y"
55
- const oppositePosKey = isVertical ? "y" : "x"
56
- const oppositeSizeKey = isVertical ? "height" : "width"
57
-
58
52
 
59
53
  const currentSize = frame[sizeKey]
60
- const collapseAmount = collapseSizeScaled[sizeKey]
61
- const shrinkAmount = currentSize - collapseAmount
62
-
63
- const dockedSide = frame.docked as EdgeSide
64
-
65
- const { applyFixes, toFix } = framesRedistributeFix(win, frame, dockedSide, posKey, sizeKey, "shrink")
66
-
67
- // note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
68
- const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
69
-
70
- const redistributeSide = oppositeSide(dockedSide)
54
+ const shrinkAmount = currentSize - collapseSizeScaled[sizeKey]
71
55
 
72
- const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, dockedSide, posKey, sizeKey)
56
+ const shrinkResult = getFrameShrinkInfo(win, frameId, shrinkAmount, frame.docked, { allowOutOfBounds: true })
73
57
 
74
- const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, -shrinkAmount, { allowOutOfBounds: true, pinnedEdgeCoordinates })
75
-
76
- if (changes instanceof KnownError) {
77
- // we should never get out of bounds and because this is a collapse there should always be space
78
- if (changes.code === LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS || changes.code === LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE) {
79
- changes.message = `This error should never happen, please file a bug report: ${changes.message}`
80
- throw changes
81
- }
82
- return changes
58
+ if (shrinkResult instanceof KnownError) {
59
+ return shrinkResult
83
60
  }
84
- applyFrameChanges(win, changes)
85
- pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
86
-
87
61
 
88
- applyFixes()
89
- pushIfNotIn(toExtract, toFix)
62
+ applyFrameChanges(win, shrinkResult)
63
+ pushIfNotIn(toExtract, shrinkResult.modified.map(_ => _.id))
90
64
 
91
- if (frame.docked === "right" || frame.docked === "bottom") {
92
- frame[posKey] = frame[posKey] + (frame[sizeKey] - collapseSizeScaled[sizeKey])
93
- }
94
- frame[sizeKey] = collapseSizeScaled[sizeKey]
95
65
  frame.collapsed = currentSize
96
66
  // when we collapse to 0 it's a special case where the frame will always fit the entire window edge
97
67
  // for proper uncollapsing later
98
68
  if (collapseSizeScaled[sizeKey] === 0) {
69
+ const oppositePosKey = isVertical ? "y" : "x"
70
+ const oppositeSizeKey = isVertical ? "height" : "width"
99
71
  frame[oppositePosKey] = 0
100
72
  frame[oppositeSizeKey] = settings.maxInt
101
73
  }
102
74
 
103
75
  return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
104
76
  }
105
-
@@ -3,10 +3,12 @@ import { walk } from "@alanscodelog/utils/walk"
3
3
 
4
4
  import { applyFrameChanges } from "./applyFrameChanges.js"
5
5
  import { getFillEmptySpaceInfo } from "./getFillEmptySpaceInfo.js"
6
+ import { getFrameShrinkInfo } from "./getFrameShrinkInfo.js"
6
7
  import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
7
8
  import { getFrameUndockInfo } from "./getFrameUndockInfo.js"
8
9
 
9
10
  import { cloneFrame } from "../helpers/cloneFrame.js"
11
+ import { findEdgesTouchingWindow } from "../helpers/findEdgesTouchingWindow.js"
10
12
  import { getDockBoundaries } from "../helpers/getDockBoundaries.js"
11
13
  import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
12
14
  import { oppositeSide } from "../helpers/oppositeSide.js"
@@ -33,7 +35,8 @@ export function getFrameDockInfo(
33
35
  | KnownError<typeof LAYOUT_ERROR.CANT_LEAVE_NO_UNDOCKED_FRAMES>
34
36
  | KnownError<typeof LAYOUT_ERROR.FRAME_ALREADY_DOCKED_ON_SIDE>
35
37
  | KnownError<typeof LAYOUT_ERROR.CANT_UNDOCK_COLLAPSED_FRAME>
36
- | KnownError<typeof LAYOUT_ERROR.NO_FILL_CANDIDATES> {
38
+ | KnownError<typeof LAYOUT_ERROR.NO_FILL_CANDIDATES>
39
+ | KnownError<typeof LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME> {
37
40
  // its easier to just clone the window and extract changes later
38
41
  // setting the var ensures we don't accidentally mutate the original
39
42
  win = walk(win, undefined, { save: true }) as typeof win
@@ -57,8 +60,9 @@ export function getFrameDockInfo(
57
60
 
58
61
  const isHorizontal = side === "left" || side === "right"
59
62
  const perpendicular = isHorizontal ? "width" : "height"
63
+ const posKey = isHorizontal ? "x" : "y"
64
+ const sizeKey = isHorizontal ? "width" : "height"
60
65
 
61
- const oldFrame = cloneFrame(frame)
62
66
  const otherFrameIds = Object.keys(win.frames).filter(_ => _ !== frameId)
63
67
  const nonDockedFrameIds = otherFrameIds.filter(id => !win.frames[id].docked)
64
68
  if (nonDockedFrameIds.length === 0) {
@@ -66,68 +70,84 @@ export function getFrameDockInfo(
66
70
  }
67
71
 
68
72
  // if its the only frame allow it to be as big as it likes
69
- const effectiveMaxPerpendicular = maxPerpendicularLength ?? settings.maxPerpendicularLengthScaled.width
70
- const perpendicularLength = otherFrameIds.length > 0 ? Math.min(frame[perpendicular], effectiveMaxPerpendicular) : frame[perpendicular]
73
+ const maxPerpendicular = maxPerpendicularLength ?? settings.maxPerpendicularLengthScaled.width
74
+ const perpendicularLength = otherFrameIds.length > 0 ? Math.min(frame[perpendicular], maxPerpendicular) : frame[perpendicular]
71
75
 
72
76
  frame.docked = side
73
77
  frame.collapsed = undefined
74
78
 
75
79
  const toExtract = [frame.id]
76
80
 
77
- // fills just the hole left by the frame when it was moved
78
- const changes = getFillEmptySpaceInfo(win, oldFrame, [], [frameId])
79
- if (changes instanceof Error) return changes
80
- applyFrameChanges(win, changes)
81
- pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
82
-
83
-
84
- // redistribute other non-docked frames to make room for the new dock.
85
- const sideToPushTowards = oppositeSide(side)
86
-
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 })
93
-
94
- if (redistributeChanges instanceof KnownError) {
95
- return redistributeChanges
81
+ if (frame[perpendicular] === perpendicularLength) {
82
+ return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
96
83
  }
97
- applyFrameChanges(win, redistributeChanges)
98
- pushIfNotIn(toExtract, redistributeChanges.modified.map(_ => _.id))
99
-
100
-
101
- const { minX, maxX, minY, maxY } = getDockBoundaries(win)
102
- switch (side) {
103
- case "left":
104
- frame.x = 0
105
- frame.y = minY
106
- frame.width = perpendicularLength
107
- frame.height = maxY - minY
108
- break
109
- case "right":
110
- frame.x = settings.maxInt - perpendicularLength
111
- frame.y = minY
112
- frame.width = perpendicularLength
113
- frame.height = maxY - minY
114
- break
115
- case "top":
116
- frame.x = minX
117
- frame.y = 0
118
- frame.width = maxX - minX
119
- frame.height = perpendicularLength
120
- break
121
- case "bottom":
122
- frame.x = minX
123
- frame.y = settings.maxInt - perpendicularLength
124
- frame.width = maxX - minX
125
- frame.height = perpendicularLength
126
- break
127
- }
128
-
129
84
 
130
- const res = toExtract.map(_ => win.frames[_])
85
+ // check if the frame touches the full edge it will be docked to (not just it's dock boundary)
86
+ // if so, we can use getFrameShrinkInfo instead of fill-empty-space + redistribute
87
+ // this feels better as otherwise, the other frames feel like they are unnecessarily moved to the other side
88
+ // this way where the frame *was* feels like it's taken into account
89
+ const touchesEntireEdge = findEdgesTouchingWindow(frame)[side] === "full"
90
+
91
+ if (touchesEntireEdge) {
92
+ const shrinkAmount = frame[sizeKey] - perpendicularLength
93
+ if (shrinkAmount > 0) {
94
+ const shrinkResult = getFrameShrinkInfo(win, frameId, shrinkAmount, side)
95
+ if (shrinkResult instanceof KnownError) {
96
+ return shrinkResult
97
+ }
98
+ applyFrameChanges(win, shrinkResult)
99
+ pushIfNotIn(toExtract, shrinkResult.modified.map(_ => _.id))
100
+ }
101
+ } else {
102
+ const oldFrame = cloneFrame(frame)
103
+
104
+ // fills just the hole left by the frame when it was moved
105
+ const changes = getFillEmptySpaceInfo(win, oldFrame, [], [frameId])
106
+ if (changes instanceof Error) return changes
107
+ applyFrameChanges(win, changes)
108
+ pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
109
+
110
+ // redistribute other non-docked frames to make room for the new dock.
111
+ const sideToPushTowards = oppositeSide(side)
112
+
113
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, side, posKey, sizeKey)
114
+
115
+ const redistributeChanges = getFramesRedistributeInfo(win, sideToPushTowards, nonDockedFrameIds, perpendicularLength, { pinnedEdgeCoordinates })
116
+
117
+ if (redistributeChanges instanceof KnownError) {
118
+ return redistributeChanges
119
+ }
120
+ applyFrameChanges(win, redistributeChanges)
121
+ pushIfNotIn(toExtract, redistributeChanges.modified.map(_ => _.id))
122
+
123
+ const { minX, maxX, minY, maxY } = getDockBoundaries(win)
124
+ switch (side) {
125
+ case "left":
126
+ frame.x = 0
127
+ frame.y = minY
128
+ frame.width = perpendicularLength
129
+ frame.height = maxY - minY
130
+ break
131
+ case "right":
132
+ frame.x = settings.maxInt - perpendicularLength
133
+ frame.y = minY
134
+ frame.width = perpendicularLength
135
+ frame.height = maxY - minY
136
+ break
137
+ case "top":
138
+ frame.x = minX
139
+ frame.y = 0
140
+ frame.width = maxX - minX
141
+ frame.height = perpendicularLength
142
+ break
143
+ case "bottom":
144
+ frame.x = minX
145
+ frame.y = settings.maxInt - perpendicularLength
146
+ frame.width = maxX - minX
147
+ frame.height = perpendicularLength
148
+ break
149
+ }
150
+ }
131
151
 
132
- return { modified: res, created: [], deleted: [] }
152
+ return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
133
153
  }