|
| 1 | +import type { Pane } from "paneforge"; |
| 2 | +import { clearCookie, setCookie } from "./util"; |
| 3 | +import { watch } from "runed"; |
| 4 | + |
| 5 | +export const ROOT_LAYOUT_KEY = "diff-viewer-root-layout"; |
| 6 | +export interface PersistentLayoutState { |
| 7 | + sidebarWidth: number; |
| 8 | +} |
| 9 | + |
| 10 | +export class LayoutState { |
| 11 | + sidebarCollapsed = $state(false); |
| 12 | + |
| 13 | + windowInnerWidth: number | undefined = $state(); |
| 14 | + sidebarPane: Pane | undefined = $state(); |
| 15 | + lastSidebarWidth: number | undefined = $state(); |
| 16 | + |
| 17 | + minSidebarWidth = $derived.by(() => { |
| 18 | + return this.getContainerProportion(200, 0); |
| 19 | + }); |
| 20 | + defaultSidebarWidth = $derived.by(() => { |
| 21 | + if (this.lastSidebarWidth !== undefined) { |
| 22 | + return this.lastSidebarWidth; |
| 23 | + } |
| 24 | + return this.getContainerProportion(350, 0.25); |
| 25 | + }); |
| 26 | + |
| 27 | + defaultMainWidth = $derived.by(() => { |
| 28 | + if (this.lastSidebarWidth !== undefined) { |
| 29 | + return 100 - this.lastSidebarWidth; |
| 30 | + } |
| 31 | + return undefined; |
| 32 | + }); |
| 33 | + |
| 34 | + constructor(persistentState: PersistentLayoutState | null) { |
| 35 | + this.loadFrom(persistentState); |
| 36 | + |
| 37 | + // Maintain sidebar size when resizing window |
| 38 | + watch.pre( |
| 39 | + () => this.windowInnerWidth, |
| 40 | + (newValue, oldValue) => { |
| 41 | + if (oldValue !== undefined && newValue !== undefined && this.sidebarPane) { |
| 42 | + const oldPx = (this.sidebarPane.getSize() / 100) * oldValue; |
| 43 | + const newProportion = this.getProportion(oldPx, newValue); |
| 44 | + this.sidebarPane.resize(newProportion); |
| 45 | + } |
| 46 | + }, |
| 47 | + ); |
| 48 | + } |
| 49 | + |
| 50 | + private loadFrom(persistentState: PersistentLayoutState | null) { |
| 51 | + if (persistentState === null) { |
| 52 | + return; |
| 53 | + } |
| 54 | + |
| 55 | + const sidebarWidth = persistentState.sidebarWidth; |
| 56 | + if (Number.isFinite(sidebarWidth) && sidebarWidth >= 0 && sidebarWidth <= 100) { |
| 57 | + this.lastSidebarWidth = sidebarWidth; |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + toggleSidebar() { |
| 62 | + this.sidebarCollapsed = !this.sidebarCollapsed; |
| 63 | + } |
| 64 | + |
| 65 | + private getContainerProportion(px: number, defaultValue: number) { |
| 66 | + if (this.windowInnerWidth === undefined) { |
| 67 | + return defaultValue; |
| 68 | + } |
| 69 | + return this.getProportion(px, this.windowInnerWidth); |
| 70 | + } |
| 71 | + |
| 72 | + private getProportion(px: number, max: number) { |
| 73 | + return Math.max(0, Math.min(100, (px / max) * 100)); |
| 74 | + } |
| 75 | + |
| 76 | + resetLayout() { |
| 77 | + clearCookie(ROOT_LAYOUT_KEY); |
| 78 | + this.lastSidebarWidth = undefined; |
| 79 | + if (this.sidebarPane) { |
| 80 | + this.sidebarPane.resize(this.defaultSidebarWidth); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + onSidebarResize(size: number, prevSize: number | undefined) { |
| 85 | + if (prevSize === undefined) { |
| 86 | + // Prevent initial resize from triggering update loop |
| 87 | + return; |
| 88 | + } |
| 89 | + |
| 90 | + /* |
| 91 | + TODO: |
| 92 | + *also* persist size in px to avoid sidebar changing size when reopening with |
| 93 | + a different sized window |
| 94 | +
|
| 95 | + need to keep the proportion for SSR as paneforge does not currently provide |
| 96 | + a way to preset a size in px (it generally works in proportions only) |
| 97 | +
|
| 98 | + this means there may be a shift on hydration when a new window uses an old cookie |
| 99 | +
|
| 100 | + see GH:svecosystem/paneforge/issues/91 |
| 101 | + */ |
| 102 | + this.lastSidebarWidth = size; |
| 103 | + const rootLayout: PersistentLayoutState = { |
| 104 | + sidebarWidth: this.lastSidebarWidth, |
| 105 | + }; |
| 106 | + setCookie(ROOT_LAYOUT_KEY, JSON.stringify(rootLayout)); |
| 107 | + } |
| 108 | +} |
0 commit comments