@witchcraft/layout 0.4.2 → 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 (65) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/LayoutDragEdgeGrabbed.vue +1 -1
  3. package/dist/runtime/components/LayoutDragEdgeHandle.vue +1 -2
  4. package/dist/runtime/components/LayoutWindow.d.vue.ts +12 -0
  5. package/dist/runtime/components/LayoutWindow.vue +5 -4
  6. package/dist/runtime/components/LayoutWindow.vue.d.ts +12 -0
  7. package/dist/runtime/composables/useFrames.d.ts +38 -22
  8. package/dist/runtime/composables/useFrames.js +44 -8
  9. package/dist/runtime/demo/tailwind.css +1 -1
  10. package/dist/runtime/drag/CloseAction.d.ts +3 -0
  11. package/dist/runtime/drag/CloseAction.js +5 -0
  12. package/dist/runtime/drag/DragActionHandler.d.ts +4 -1
  13. package/dist/runtime/drag/DragActionHandler.js +2 -2
  14. package/dist/runtime/drag/FrameDragAction.d.ts +3 -0
  15. package/dist/runtime/drag/FrameDragAction.js +5 -0
  16. package/dist/runtime/drag/SplitAction.d.ts +3 -0
  17. package/dist/runtime/drag/SplitAction.js +8 -3
  18. package/dist/runtime/helpers/findEdgesTouchingWindow.d.ts +7 -0
  19. package/dist/runtime/helpers/findEdgesTouchingWindow.js +20 -0
  20. package/dist/runtime/helpers/index.d.ts +8 -1
  21. package/dist/runtime/helpers/index.js +8 -1
  22. package/dist/runtime/layout/debugFrame.js +5 -1
  23. package/dist/runtime/layout/getFrameCollapseInfo.d.ts +1 -1
  24. package/dist/runtime/layout/getFrameCollapseInfo.js +9 -29
  25. package/dist/runtime/layout/getFrameDockInfo.d.ts +1 -1
  26. package/dist/runtime/layout/getFrameDockInfo.js +61 -44
  27. package/dist/runtime/layout/getFrameExpandInfo.d.ts +11 -0
  28. package/dist/runtime/layout/getFrameExpandInfo.js +49 -0
  29. package/dist/runtime/layout/getFrameShrinkInfo.d.ts +14 -0
  30. package/dist/runtime/layout/getFrameShrinkInfo.js +48 -0
  31. package/dist/runtime/layout/getFrameUncollapseInfo.d.ts +1 -1
  32. package/dist/runtime/layout/getFrameUncollapseInfo.js +6 -20
  33. package/dist/runtime/layout/getUpdateWindowSizeInfo.js +10 -12
  34. package/dist/runtime/layout/index.d.ts +3 -0
  35. package/dist/runtime/layout/index.js +3 -0
  36. package/dist/runtime/settings.js +1 -1
  37. package/dist/runtime/types/index.d.ts +72 -0
  38. package/dist/runtime/types/index.js +2 -1
  39. package/dist/runtime/types/vue.d.ts +12 -3
  40. package/package.json +3 -2
  41. package/src/runtime/components/LayoutDragEdgeGrabbed.vue +1 -1
  42. package/src/runtime/components/LayoutDragEdgeHandle.vue +1 -2
  43. package/src/runtime/components/LayoutWindow.vue +8 -6
  44. package/src/runtime/composables/useFrames.ts +61 -32
  45. package/src/runtime/demo/tailwind.css +4 -0
  46. package/src/runtime/drag/CloseAction.ts +7 -0
  47. package/src/runtime/drag/DragActionHandler.ts +3 -3
  48. package/src/runtime/drag/FrameDragAction.ts +9 -0
  49. package/src/runtime/drag/SplitAction.ts +11 -3
  50. package/src/runtime/helpers/findEdgesTouchingWindow.ts +30 -0
  51. package/src/runtime/helpers/index.ts +8 -1
  52. package/src/runtime/layout/debugFrame.ts +6 -1
  53. package/src/runtime/layout/getFrameCollapseInfo.ts +12 -41
  54. package/src/runtime/layout/getFrameDockInfo.ts +77 -57
  55. package/src/runtime/layout/getFrameExpandInfo.ts +84 -0
  56. package/src/runtime/layout/getFrameShrinkInfo.ts +87 -0
  57. package/src/runtime/layout/getFrameUncollapseInfo.ts +8 -30
  58. package/src/runtime/layout/getUpdateWindowSizeInfo.ts +16 -22
  59. package/src/runtime/layout/index.ts +3 -0
  60. package/src/runtime/settings.ts +1 -1
  61. package/src/runtime/types/index.ts +41 -1
  62. package/src/runtime/types/vue.ts +5 -3
  63. package/dist/runtime/drag/defaultDragActions.d.ts +0 -9
  64. package/dist/runtime/drag/defaultDragActions.js +0 -10
  65. package/src/runtime/drag/defaultDragActions.ts +0 -20
@@ -13,6 +13,9 @@ import type { KnownError } from "../utils/KnownError.js"
13
13
 
14
14
  export class FrameDragAction implements IDragAction {
15
15
  name = "frameDrag" as const
16
+
17
+ minDragDistance = 5
18
+
16
19
  state: {
17
20
  lastReturn?: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError
18
21
  } = {
@@ -52,12 +55,15 @@ export class FrameDragAction implements IDragAction {
52
55
  hooks: FrameDragAction["hooks"] = {},
53
56
  config?: {
54
57
  debug?: boolean | string
58
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
59
+ minDragDistance?: number
55
60
  }
56
61
  ) {
57
62
  if (handleEvent !== undefined) this.handleEvent = handleEvent
58
63
  if (modifyDecos !== undefined) this.modifyDecos = modifyDecos
59
64
  this.hooks = hooks
60
65
  if (config?.debug) this.debug = true
66
+ if (config?.minDragDistance !== undefined) this.minDragDistance = config.minDragDistance
61
67
 
62
68
  this.reset()
63
69
  }
@@ -149,6 +155,9 @@ export class FrameDragAction implements IDragAction {
149
155
  _e: PointerEvent | undefined,
150
156
  state: DragState
151
157
  ): ActionDragChangeResult {
158
+ if (state.dragDistance <= this.minDragDistance) {
159
+ return { updateEdges: false, shapes: [], showDragging: false }
160
+ }
152
161
  const { win, draggingFrameId, dragHoveredFrame } = state
153
162
  const matchedZone = getDragZones(state, settings.zoneSizes)
154
163
 
@@ -16,6 +16,7 @@ export type DragChangeType = "start" | "move" | "end"
16
16
 
17
17
  export class SplitAction implements IDragAction {
18
18
  name = "split" as const
19
+ minDragDistance = 5
19
20
 
20
21
  state: {
21
22
  allowed: true
@@ -57,6 +58,8 @@ export class SplitAction implements IDragAction {
57
58
  config?: {
58
59
  debug?: boolean | string
59
60
  splitHints?: Partial<SplitAction["splitHints"]>
61
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
62
+ minDragDistance?: number
60
63
  }
61
64
  ) {
62
65
  if (handleEvent !== undefined) this.handleEvent = handleEvent
@@ -65,6 +68,7 @@ export class SplitAction implements IDragAction {
65
68
  this.hooks = hooks
66
69
  this.reset()
67
70
  if (config?.debug) this.debug = true
71
+ if (config?.minDragDistance !== undefined) this.minDragDistance = config.minDragDistance
68
72
  if (config?.splitHints?.actions) this.splitHints.actions = config.splitHints.actions
69
73
  if (config?.splitHints?.transformError) this.splitHints.transformError = config.splitHints.transformError
70
74
  }
@@ -106,7 +110,8 @@ export class SplitAction implements IDragAction {
106
110
  canHandleRequest(e: PointerEvent | KeyboardEvent, state: DragState): boolean {
107
111
  const { draggingEdges } = state
108
112
  if (draggingEdges.length !== 1) return false
109
- this.setTextHints(state.isDragging === "edge" ? true : undefined)
113
+ // hint should not be shown when dragging from window edge but we should still handle the event
114
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? true : undefined)
110
115
  if (this.handleEvent(e, state)) {
111
116
  this.hooks.onStart?.()
112
117
  return true
@@ -154,7 +159,7 @@ export class SplitAction implements IDragAction {
154
159
  dragDirections[oppositeOrientation]!,
155
160
  dragPoint!
156
161
  )
157
- this.setTextHints(canSplit)
162
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? canSplit : undefined)
158
163
  if (!(canSplit instanceof Error)) {
159
164
  this.state.allowed = true
160
165
  this.state.res = canSplit
@@ -173,7 +178,10 @@ export class SplitAction implements IDragAction {
173
178
  _e: PointerEvent | undefined,
174
179
  state: DragState
175
180
  ): ActionDragChangeResult {
176
- const { dragHoveredFrame } = state
181
+ const { dragHoveredFrame, dragDistance } = state
182
+ if (dragDistance <= this.minDragDistance) {
183
+ return { updateEdges: false, shapes: [], showDragging: false }
184
+ }
177
185
  let ok = false
178
186
  if (dragHoveredFrame) {
179
187
  if (this.state.lastPoint?.x === state.dragPoint?.x && this.state.lastPoint?.y === state.dragPoint?.y && this.state.lastReturn) {
@@ -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
  }
@@ -0,0 +1,84 @@
1
+ import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
2
+ import { walk } from "@alanscodelog/utils/walk"
3
+
4
+ import { applyFrameChanges } from "./applyFrameChanges.js"
5
+ import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
6
+
7
+ import { findEdgesTouchingWindow } from "../helpers/findEdgesTouchingWindow.js"
8
+ import { framesRedistributeFix } from "../helpers/framesRedistributeFix.js"
9
+ import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
10
+ import { oppositeSide } from "../helpers/oppositeSide.js"
11
+ import type { EdgeSide, LayoutChange, LayoutWindow } from "../types/index.js"
12
+ import { LAYOUT_ERROR } from "../types/index.js"
13
+ import { KnownError } from "../utils/KnownError.js"
14
+
15
+ /**
16
+ * Returns a {@link LayoutChange} with the information necessary to expand a frame touching a window edge by a given amount.
17
+ *
18
+ * Changes can be applied to a window with {@link applyFrameChanges}.
19
+ *
20
+ * It expands the frame by redistributing space from neighboring frames.
21
+ */
22
+ export function getFrameExpandInfo(
23
+ win: LayoutWindow,
24
+ frameId: string,
25
+ amount: number,
26
+ expandSide: EdgeSide
27
+ ): LayoutChange
28
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS>
29
+ | KnownError<typeof LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE>
30
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES>
31
+ | KnownError<typeof LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME> {
32
+ win = walk(win, undefined, { save: true })
33
+ const frame = win.frames[frameId]
34
+ if (!frame) { throw new Error(`Unknown frame ${frameId}`) }
35
+ if (!frame.docked) { throw new Error(`Frame ${frameId} is not docked.`) }
36
+ const toExtract = [frame.id]
37
+
38
+ if (Object.keys(win.frames).length === 1) {
39
+ return new KnownError(LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME, `Frame ${frameId} is the only frame, cannot expand.`, { frame })
40
+ }
41
+
42
+ // verify the frame actually touches the specified edge
43
+ if (!(expandSide in findEdgesTouchingWindow(frame))) {
44
+ throw new Error(`Frame ${frameId} does not touch the ${expandSide} edge.`)
45
+ }
46
+
47
+
48
+ const isVertical = frame.docked === "left" || frame.docked === "right"
49
+ const sizeKey = isVertical ? "width" : "height" as const
50
+ const posKey = isVertical ? "x" : "y"
51
+ const currentSize = frame[sizeKey]
52
+
53
+
54
+ const dockedSide = frame.docked as EdgeSide
55
+
56
+ const { applyFixes, toFix } = framesRedistributeFix(win, frame, dockedSide, posKey, sizeKey, "expand")
57
+
58
+ // note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
59
+ const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
60
+
61
+ const redistributeSide = oppositeSide(frame.docked)
62
+
63
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, dockedSide, posKey, sizeKey)
64
+
65
+ const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, amount, { pinnedEdgeCoordinates })
66
+
67
+ if (changes instanceof KnownError) {
68
+ return changes
69
+ }
70
+
71
+ applyFrameChanges(win, changes)
72
+ pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
73
+
74
+ applyFixes()
75
+ pushIfNotIn(toExtract, toFix)
76
+
77
+ frame[sizeKey] = currentSize + amount
78
+
79
+ if (frame.docked === "right" || frame.docked === "bottom") {
80
+ frame[posKey] -= amount
81
+ }
82
+
83
+ return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
84
+ }
@@ -0,0 +1,87 @@
1
+ import { pushIfNotIn } from "@alanscodelog/utils/pushIfNotIn"
2
+ import { walk } from "@alanscodelog/utils/walk"
3
+
4
+ import { applyFrameChanges } from "./applyFrameChanges.js"
5
+ import { getFramesRedistributeInfo } from "./getFramesRedistributeInfo.js"
6
+
7
+ import { findEdgesTouchingWindow } from "../helpers/findEdgesTouchingWindow.js"
8
+ import { framesRedistributeFix } from "../helpers/framesRedistributeFix.js"
9
+ import { getPinnedEdgesForCollapsedFrames } from "../helpers/getPinnedEdgesForCollapsedFrames.js"
10
+ import { oppositeSide } from "../helpers/oppositeSide.js"
11
+ import type { EdgeSide, LayoutChange, LayoutWindow } from "../types/index.js"
12
+ import { LAYOUT_ERROR } from "../types/index.js"
13
+ import { KnownError } from "../utils/KnownError.js"
14
+
15
+ /**
16
+ * Returns a {@link LayoutChange} with the information necessary to shrink a frame touching a window edge by a given amount.
17
+ *
18
+ * Changes can be applied to a window with {@link applyFrameChanges}.
19
+ *
20
+ * It shrinks the frame by redistributing the freed space to neighboring frames.
21
+ */
22
+ export function getFrameShrinkInfo(
23
+ win: LayoutWindow,
24
+ frameId: string,
25
+ amount: number,
26
+ shrinkSide: EdgeSide,
27
+ {
28
+ allowOutOfBounds = false
29
+ }: {
30
+ /** Allow the resize span to exceed window bounds. */
31
+ allowOutOfBounds?: boolean
32
+ } = {}
33
+ ): LayoutChange
34
+ | KnownError<typeof LAYOUT_ERROR.REDISTRIBUTE_WOULD_RESULT_IN_INVALID_FRAMES>
35
+ | KnownError<typeof LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME> {
36
+ win = walk(win, undefined, { save: true }) as typeof win
37
+ const frame = win.frames[frameId]
38
+ if (!frame) throw new Error(`Unknown frame ${frameId}`)
39
+
40
+ if (Object.keys(win.frames).length === 1) {
41
+ return new KnownError(LAYOUT_ERROR.CANT_RESIZE_SINGLE_FRAME, `Frame ${frameId} is the only frame, cannot shrink.`, { frame })
42
+ }
43
+
44
+ // verify the frame actually touches the specified edge
45
+ if (!(shrinkSide in findEdgesTouchingWindow(frame))) {
46
+ throw new Error(`Frame ${frameId} does not touch the ${shrinkSide} edge.`)
47
+ }
48
+
49
+ const isVertical = shrinkSide === "left" || shrinkSide === "right"
50
+ const sizeKey = isVertical ? "width" : "height" as const
51
+ const posKey = isVertical ? "x" : "y"
52
+
53
+ const toExtract = [frame.id]
54
+
55
+ const { applyFixes, toFix } = framesRedistributeFix(win, frame, shrinkSide, posKey, sizeKey, "shrink")
56
+
57
+ // note fully collapsed frames without an area are already excluded by getFramesRedistributeInfo
58
+ const otherFrameIds = Object.keys(win.frames).filter(id => id !== frameId)
59
+
60
+ const redistributeSide = oppositeSide(shrinkSide)
61
+
62
+ const pinnedEdgeCoordinates: number[] = getPinnedEdgesForCollapsedFrames(win, frame, shrinkSide, posKey, sizeKey)
63
+
64
+ const changes = getFramesRedistributeInfo(win, redistributeSide, otherFrameIds, -amount, { allowOutOfBounds, pinnedEdgeCoordinates })
65
+
66
+ if (changes instanceof KnownError) {
67
+ // we should never get out of bounds and because this is a shrink there should always be space
68
+ if (changes.code === LAYOUT_ERROR.REDISTRIBUTE_OUT_OF_BOUNDS || changes.code === LAYOUT_ERROR.NO_SPACE_TO_REDISTRIBUTE) {
69
+ changes.message = `This error should never happen, please file a bug report: ${changes.message}`
70
+ throw changes
71
+ }
72
+ return changes
73
+ }
74
+ applyFrameChanges(win, changes)
75
+ pushIfNotIn(toExtract, changes.modified.map(_ => _.id))
76
+
77
+
78
+ applyFixes()
79
+ pushIfNotIn(toExtract, toFix)
80
+
81
+ if (shrinkSide === "right" || shrinkSide === "bottom") {
82
+ frame[posKey] = frame[posKey] + amount
83
+ }
84
+ frame[sizeKey] = frame[sizeKey] - amount
85
+
86
+ return { modified: toExtract.map(_ => win.frames[_]), created: [], deleted: [] }
87
+ }