@window-splitter/state 0.3.1 → 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.
package/src/index.ts CHANGED
@@ -65,6 +65,13 @@ export interface Constraints<T extends ParsedUnit | Unit = ParsedUnit> {
65
65
  defaultCollapsed?: boolean;
66
66
  /** The size of the panel once collapsed */
67
67
  collapsedSize?: T;
68
+ /**
69
+ * By default the layout will be stored in percentage values while at rest.
70
+ * This makes scaling the layout easier when the container is resized.
71
+ * However you might have a panel you want to stay at a static size when
72
+ * the container is resized.
73
+ */
74
+ isStaticAtRest?: boolean;
68
75
  }
69
76
 
70
77
  interface Order {
@@ -228,6 +235,7 @@ interface SetSizeEvent {
228
235
  /** Set the size of the whole group */
229
236
  type: "setSize";
230
237
  size: Rect;
238
+ handleOverflow?: boolean;
231
239
  }
232
240
 
233
241
  interface SetActualItemsSizeEvent {
@@ -406,23 +414,24 @@ type OnResizeSize = {
406
414
 
407
415
  export type OnResizeCallback = (size: OnResizeSize) => void;
408
416
 
409
- interface InitializePanelOptions {
417
+ type InitializePanelOptions = {
410
418
  min?: Unit;
411
419
  max?: Unit;
412
420
  default?: Unit;
413
- collapsible?: boolean;
414
- collapsed?: boolean;
415
421
  collapsedSize?: Unit;
416
- onCollapseChange?: {
417
- current: ((isCollapsed: boolean) => void) | null | undefined;
418
- };
419
- onResize?: {
420
- current: OnResizeCallback | null | undefined;
421
- };
422
- collapseAnimation?: PanelData["collapseAnimation"];
423
- defaultCollapsed?: boolean;
424
422
  id?: string;
425
- }
423
+ } & Partial<
424
+ Pick<
425
+ PanelData,
426
+ | "isStaticAtRest"
427
+ | "collapseAnimation"
428
+ | "defaultCollapsed"
429
+ | "onResize"
430
+ | "collapsible"
431
+ | "collapsed"
432
+ | "onCollapseChange"
433
+ >
434
+ >;
426
435
 
427
436
  type InitializePanelOptionsWithId = InitializePanelOptions & { id: string };
428
437
 
@@ -468,6 +477,7 @@ export function initializePanel(
468
477
  id: item.id,
469
478
  collapseAnimation: item.collapseAnimation,
470
479
  default: item.default ? parseUnit(item.default) : undefined,
480
+ isStaticAtRest: item.isStaticAtRest,
471
481
  } satisfies Omit<PanelData, "id" | "currentValue"> & { id?: string };
472
482
 
473
483
  return { ...data, currentValue: makePixelUnit(-1) } satisfies Omit<
@@ -498,7 +508,7 @@ export function parseUnit(unit: Unit | "1fr"): ParsedUnit {
498
508
  }
499
509
 
500
510
  if (unit.endsWith("%")) {
501
- return makePercentUnit(parseFloat(unit));
511
+ return makePercentUnit(parseFloat(unit) / 100);
502
512
  }
503
513
 
504
514
  throw new Error(`Invalid unit: ${unit}`);
@@ -527,7 +537,7 @@ function getUnitPixelValue(
527
537
  const parsed = unit === "1fr" ? parseUnit(unit) : unit;
528
538
  return parsed.type === "pixel"
529
539
  ? parsed.value
530
- : new Big(parsed.value).div(100).mul(getGroupSize(context));
540
+ : parsed.value.mul(getGroupSize(context));
531
541
  }
532
542
 
533
543
  /** Clamp a new `currentValue` given the panel's constraints. */
@@ -686,10 +696,7 @@ function panelHasSpace(
686
696
  );
687
697
  }
688
698
 
689
- return (
690
- item.currentValue.value.gt(getUnitPixelValue(context, item.min)) &&
691
- item.currentValue.value.lte(getUnitPixelValue(context, item.max))
692
- );
699
+ return item.currentValue.value.gt(getUnitPixelValue(context, item.min));
693
700
  }
694
701
 
695
702
  /** Search in a `direction` for a panel that still has space to expand. */
@@ -726,11 +733,9 @@ function getStaticWidth(context: GroupMachineContextValue) {
726
733
  for (const item of context.items) {
727
734
  if (isPanelHandle(item)) {
728
735
  width = width.add(item.size.value);
729
- } else if (
730
- isPanelData(item) &&
731
- item.collapsed &&
732
- item.currentValue.type === "pixel"
733
- ) {
736
+ } else if (item.collapsed && item.currentValue.type === "pixel") {
737
+ width = width.add(item.currentValue.value);
738
+ } else if (item.isStaticAtRest) {
734
739
  width = width.add(item.currentValue.value);
735
740
  }
736
741
  }
@@ -743,7 +748,7 @@ function formatUnit(unit: ParsedUnit): Unit {
743
748
  return `${unit.value.toNumber()}px`;
744
749
  }
745
750
 
746
- return `${unit.value.toNumber()}%`;
751
+ return `${unit.value.mul(100).toNumber()}%`;
747
752
  }
748
753
 
749
754
  export function getPanelGroupPixelSizes(context: GroupMachineContextValue) {
@@ -805,6 +810,11 @@ export function buildTemplate(context: GroupMachineContextValue) {
805
810
  item.currentValue.type === "pixel" &&
806
811
  item.currentValue.value.toNumber() !== -1
807
812
  ) {
813
+ if (item.isStaticAtRest) {
814
+ const max = item.max === "1fr" ? "100%" : formatUnit(item.max);
815
+ return `clamp(${min}, ${formatUnit(item.currentValue)}, ${max})`;
816
+ }
817
+
808
818
  return formatUnit(item.currentValue);
809
819
  } else if (item.currentValue.type === "percent") {
810
820
  const max = item.max === "1fr" ? "100%" : formatUnit(item.max);
@@ -812,11 +822,28 @@ export function buildTemplate(context: GroupMachineContextValue) {
812
822
  } else if (item.collapsible && item.collapsed) {
813
823
  return formatUnit(item.collapsedSize);
814
824
  } else if (item.default) {
815
- return formatUnit(item.default);
816
- }
825
+ const siblingHasFill = context.items.some(
826
+ (i) =>
827
+ isPanelData(i) &&
828
+ i.id !== item.id &&
829
+ !i.collapsed &&
830
+ (i.max === "1fr" ||
831
+ (i.max.type === "percent" && i.max.value.eq(100)))
832
+ );
833
+
834
+ // If a sibling has a fill, this item doesn't need to expand
835
+ // So we can just use the default value
836
+ if (siblingHasFill) {
837
+ return formatUnit(item.default);
838
+ }
817
839
 
818
- const max = item.max === "1fr" ? "1fr" : formatUnit(item.max);
819
- return `minmax(${min}, ${max})`;
840
+ // Use 1fr so that panel fills ths space if needed
841
+ const max = item.max === "1fr" ? "1fr" : formatUnit(item.max);
842
+ return `minmax(${formatUnit(item.default)}, ${max})`;
843
+ } else {
844
+ const max = item.max === "1fr" ? "1fr" : formatUnit(item.max);
845
+ return `minmax(${min}, ${max})`;
846
+ }
820
847
  }
821
848
 
822
849
  return formatUnit(item.size);
@@ -902,18 +929,18 @@ function createUnrestrainedPanel(_: GroupMachineContextValue, data: PanelData) {
902
929
  */
903
930
 
904
931
  /** Converts the items to pixels */
905
- export function prepareItems(context: GroupMachineContextValue) {
932
+ export function prepareItems(context: GroupMachineContextValue): Item[] {
906
933
  const staticWidth = getStaticWidth(context);
907
934
  const newItems = [];
908
935
 
909
936
  for (const item of context.items) {
910
937
  if (!item || !isPanelData(item)) {
911
- newItems.push(item);
938
+ newItems.push({ ...item });
912
939
  continue;
913
940
  }
914
941
 
915
942
  if (item.currentValue.type === "pixel") {
916
- newItems.push(item);
943
+ newItems.push({ ...item });
917
944
  continue;
918
945
  }
919
946
 
@@ -1014,6 +1041,10 @@ function updateLayout(
1014
1041
 
1015
1042
  const isInDragBugger =
1016
1043
  newDragOvershoot.abs().lt(COLLAPSE_THRESHOLD) &&
1044
+ // Let the panel expand at it's min size
1045
+ !panelAfter.currentValue.value
1046
+ .add(newDragOvershoot.abs())
1047
+ .gte(panelAfter.min.value) &&
1017
1048
  panelAfter.collapsible &&
1018
1049
  panelAfter.collapsed &&
1019
1050
  (isInLeftOvershoot || isInRightOvershoot);
@@ -1031,9 +1062,11 @@ function updateLayout(
1031
1062
 
1032
1063
  // Don't let the panel collapse until the threshold is reached
1033
1064
  if (
1065
+ !dragEvent.disregardCollapseBuffer &&
1034
1066
  panelBefore.collapsible &&
1035
- panelBefore.currentValue.value ===
1067
+ panelBefore.currentValue.value.eq(
1036
1068
  getUnitPixelValue(context, panelBefore.min)
1069
+ )
1037
1070
  ) {
1038
1071
  const potentialNewValue = panelBefore.currentValue.value.sub(
1039
1072
  newDragOvershoot.abs()
@@ -1099,8 +1132,6 @@ function updateLayout(
1099
1132
  .add(Math.abs(moveAmount))
1100
1133
  );
1101
1134
 
1102
- panelAfter.collapsed = false;
1103
-
1104
1135
  if (extra.gt(0)) {
1105
1136
  panelAfterNewValue = panelAfterNewValue.add(extra);
1106
1137
  }
@@ -1114,6 +1145,13 @@ function updateLayout(
1114
1145
  .minus(Math.abs(moveAmount))
1115
1146
  );
1116
1147
 
1148
+ if (panelBeforeNewValue.lt(panelBefore.min.value)) {
1149
+ // TODO this should probably distribute the space between the panels?
1150
+ return { dragOvershoot: newDragOvershoot };
1151
+ }
1152
+
1153
+ panelAfter.collapsed = false;
1154
+
1117
1155
  if (
1118
1156
  panelAfter.onCollapseChange?.current &&
1119
1157
  !panelAfter.collapseIsControlled &&
@@ -1123,9 +1161,9 @@ function updateLayout(
1123
1161
  }
1124
1162
  }
1125
1163
 
1126
- const panelBeforeIsAboutToCollapse =
1127
- panelBefore.currentValue.value ===
1128
- getUnitPixelValue(context, panelBefore.min);
1164
+ const panelBeforeIsAboutToCollapse = panelBefore.currentValue.value.eq(
1165
+ getUnitPixelValue(context, panelBefore.min)
1166
+ );
1129
1167
 
1130
1168
  // If the panel was expanded and now is at it's min size, collapse it
1131
1169
  if (
@@ -1170,8 +1208,11 @@ function updateLayout(
1170
1208
  );
1171
1209
 
1172
1210
  if (!leftoverSpace.eq(0)) {
1173
- panelBefore.currentValue.value =
1174
- panelBefore.currentValue.value.add(leftoverSpace);
1211
+ panelBefore.currentValue.value = clampUnit(
1212
+ context,
1213
+ panelBefore,
1214
+ panelBefore.currentValue.value.add(leftoverSpace)
1215
+ );
1175
1216
  }
1176
1217
 
1177
1218
  return { items: newItems };
@@ -1198,7 +1239,7 @@ function commitLayout(context: GroupMachineContextValue) {
1198
1239
  const staticWidth = getStaticWidth({ ...context, items: newItems });
1199
1240
 
1200
1241
  newItems.forEach((item, index) => {
1201
- if (item.type !== "panel" || item.collapsed) {
1242
+ if (item.type !== "panel" || item.collapsed || item.isStaticAtRest) {
1202
1243
  return;
1203
1244
  }
1204
1245
 
@@ -1317,6 +1358,81 @@ function applyDeltaInBothDirections(
1317
1358
  }
1318
1359
  }
1319
1360
 
1361
+ /**
1362
+ * A layout might overflow at small screen sizes.
1363
+ * This function tries to fix that by:
1364
+ *
1365
+ * 1. It will try to collapse a panel if it can.
1366
+ */
1367
+ function handleOverflow(context: GroupMachineContextValue) {
1368
+ // If we haven't measured yet we can't do anything
1369
+ if (
1370
+ context.items.some((i) => isPanelData(i) && i.currentValue.value.eq(-1))
1371
+ ) {
1372
+ return context;
1373
+ }
1374
+
1375
+ const groupSize = new Big(getGroupSize(context));
1376
+ const nonStaticWidth = groupSize.sub(getStaticWidth(context).toNumber());
1377
+ const pixelItems = context.items.map((i) => {
1378
+ if (isPanelHandle(i)) return i.size.value;
1379
+ if (i.collapsed) return getUnitPixelValue(context, i.currentValue);
1380
+
1381
+ const pixel =
1382
+ (i.currentValue.type === "pixel" && i.currentValue.value) ||
1383
+ i.currentValue.value.mul(nonStaticWidth);
1384
+
1385
+ return clampUnit(context, i, pixel);
1386
+ });
1387
+ const totalSize = pixelItems.reduce((acc, i) => acc.add(i), new Big(0));
1388
+ const overflow = totalSize.abs().sub(groupSize);
1389
+
1390
+ if (overflow.eq(0) || groupSize.eq(0)) {
1391
+ return context;
1392
+ }
1393
+
1394
+ let newContext = { ...context, items: prepareItems(context) };
1395
+
1396
+ const collapsiblePanel = newContext.items.find((i): i is PanelData =>
1397
+ Boolean(isPanelData(i) && i.collapsible)
1398
+ );
1399
+
1400
+ if (collapsiblePanel) {
1401
+ const collapsiblePanelIndex = newContext.items.findIndex(
1402
+ (i) => i.id === collapsiblePanel.id
1403
+ );
1404
+ const handleId = getHandleForPanelId(newContext, collapsiblePanel.id);
1405
+ const sizeChange = collapsiblePanel.currentValue.value.sub(
1406
+ getUnitPixelValue(newContext, collapsiblePanel.collapsedSize)
1407
+ );
1408
+
1409
+ // Try to collapse the panel
1410
+ newContext = {
1411
+ ...newContext,
1412
+ ...iterativelyUpdateLayout({
1413
+ handleId: handleId.item.id,
1414
+ delta: sizeChange,
1415
+ direction: (handleId.direction * -1) as -1 | 1,
1416
+ context: {
1417
+ ...newContext,
1418
+ // act like its the old size so the space is distributed correctly
1419
+ size: { width: totalSize.toNumber(), height: totalSize.toNumber() },
1420
+ },
1421
+ }),
1422
+ };
1423
+
1424
+ // Then remove all the overflow
1425
+ applyDeltaInBothDirections(
1426
+ newContext,
1427
+ newContext.items,
1428
+ collapsiblePanelIndex,
1429
+ overflow.neg()
1430
+ );
1431
+ }
1432
+
1433
+ return { ...newContext, items: commitLayout(newContext) };
1434
+ }
1435
+
1320
1436
  // #endregion
1321
1437
 
1322
1438
  // #region Machine
@@ -1332,6 +1448,22 @@ interface AnimationActorOutput {
1332
1448
  action: "expand" | "collapse";
1333
1449
  }
1334
1450
 
1451
+ function getDeltaForEvent(
1452
+ context: GroupMachineContextValue,
1453
+ event: CollapsePanelEvent | ExpandPanelEvent
1454
+ ) {
1455
+ const panel = getPanelWithId(context, event.panelId);
1456
+
1457
+ if (event.type === "expandPanel") {
1458
+ return new Big(
1459
+ panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)
1460
+ ).minus(panel.currentValue.value);
1461
+ }
1462
+
1463
+ const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
1464
+ return panel.currentValue.value.minus(collapsedSize);
1465
+ }
1466
+
1335
1467
  const animationActor = fromPromise<
1336
1468
  AnimationActorOutput | undefined,
1337
1469
  AnimationActorInput
@@ -1342,18 +1474,11 @@ const animationActor = fromPromise<
1342
1474
  const handle = getHandleForPanelId(context, event.panelId);
1343
1475
 
1344
1476
  let direction = new Big(handle.direction);
1345
- let fullDelta = new Big(0);
1346
-
1347
- if (event.type === "expandPanel") {
1348
- fullDelta = new Big(
1349
- panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)
1350
- ).minus(panel.currentValue.value);
1351
- } else {
1352
- const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
1477
+ const fullDelta = getDeltaForEvent(context, event);
1353
1478
 
1479
+ if (event.type === "collapsePanel") {
1354
1480
  panel.sizeBeforeCollapse = panel.currentValue.value.toNumber();
1355
1481
  direction = direction.mul(new Big(-1));
1356
- fullDelta = panel.currentValue.value.minus(collapsedSize);
1357
1482
  }
1358
1483
 
1359
1484
  const fps = 60;
@@ -1421,7 +1546,7 @@ export const groupMachine = createMachine(
1421
1546
  states: {
1422
1547
  idle: {
1423
1548
  on: {
1424
- setActualItemsSize: { actions: ["recordActualItemSize"] },
1549
+ setActualItemsSize: { actions: ["recordActualItemSize", "onResize"] },
1425
1550
  dragHandleStart: { target: "dragging" },
1426
1551
  setPanelPixelSize: {
1427
1552
  actions: [
@@ -1440,6 +1565,8 @@ export const groupMachine = createMachine(
1440
1565
  { target: "togglingCollapse" },
1441
1566
  ],
1442
1567
  expandPanel: [
1568
+ // This will match if we can't expand and the expansion won't happen
1569
+ { guard: "cannotExpandPanel" },
1443
1570
  {
1444
1571
  actions: "notifyCollapseToggle",
1445
1572
  guard: "shouldNotifyCollapseToggle",
@@ -1513,6 +1640,38 @@ export const groupMachine = createMachine(
1513
1640
  const panel = getPanelWithId(context, event.panelId);
1514
1641
  return panel.collapseIsControlled === true;
1515
1642
  },
1643
+ cannotExpandPanel: ({ context, event }) => {
1644
+ isEvent(event, ["expandPanel"]);
1645
+ const delta = getDeltaForEvent(context, event);
1646
+ const handle = getHandleForPanelId(context, event.panelId);
1647
+ const pixelItems = prepareItems(context);
1648
+
1649
+ let interimContext = { ...context, items: pixelItems };
1650
+ interimContext = {
1651
+ ...interimContext,
1652
+ ...iterativelyUpdateLayout({
1653
+ context: interimContext,
1654
+ handleId: handle.item.id,
1655
+ controlled: event.controlled,
1656
+ delta: delta,
1657
+ direction: handle.direction,
1658
+ }),
1659
+ };
1660
+ const updatedPanel = interimContext.items.find(
1661
+ (i) => i.id === event.panelId
1662
+ );
1663
+ const totalSize = interimContext.items.reduce(
1664
+ (acc, i) =>
1665
+ acc.add(isPanelData(i) ? i.currentValue.value : i.size.value),
1666
+ new Big(0)
1667
+ );
1668
+ const didExpand =
1669
+ updatedPanel &&
1670
+ isPanelData(updatedPanel) &&
1671
+ !updatedPanel.currentValue.value.eq(updatedPanel.collapsedSize.value);
1672
+
1673
+ return totalSize.gt(getGroupSize(context)) || !didExpand;
1674
+ },
1516
1675
  },
1517
1676
  actors: {
1518
1677
  animation: animationActor,
@@ -1558,11 +1717,14 @@ export const groupMachine = createMachine(
1558
1717
  return context.items;
1559
1718
  },
1560
1719
  }),
1561
- updateSize: assign({
1562
- size: ({ event }) => {
1563
- isEvent(event, ["setSize"]);
1564
- return event.size;
1565
- },
1720
+ updateSize: enqueueActions(({ context, event, enqueue }) => {
1721
+ isEvent(event, ["setSize"]);
1722
+
1723
+ if (event.handleOverflow) {
1724
+ enqueue.assign(handleOverflow({ ...context, size: event.size }));
1725
+ } else {
1726
+ enqueue.assign({ size: event.size });
1727
+ }
1566
1728
  }),
1567
1729
  recordActualItemSize: assign({
1568
1730
  items: ({ context, event }) => {
@@ -1741,12 +1903,19 @@ export const groupMachine = createMachine(
1741
1903
  onResize: ({ context }) => {
1742
1904
  for (const item of context.items) {
1743
1905
  if (isPanelData(item)) {
1906
+ const pixel = clampUnit(
1907
+ context,
1908
+ item,
1909
+ getUnitPixelValue(context, item.currentValue)
1910
+ );
1911
+ const groupSize = getGroupSize(context);
1912
+
1744
1913
  item.onResize?.current?.({
1745
- pixel: getUnitPixelValue(context, item.currentValue).toNumber(),
1746
- percentage: getUnitPercentageValue(
1747
- getGroupSize(context),
1748
- item.currentValue
1749
- ),
1914
+ pixel: pixel.toNumber(),
1915
+ percentage:
1916
+ groupSize > 0
1917
+ ? pixel.div(getGroupSize(context)).toNumber()
1918
+ : -1,
1750
1919
  });
1751
1920
  }
1752
1921
  }