gloomberb 0.16.2 → 0.16.3
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/package.json +1 -1
- package/src/brokers/account-cache.ts +45 -0
- package/src/cli/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/market.ts +170 -54
- package/src/cli/commands/overview.ts +62 -19
- package/src/cli/commands/ticker.ts +138 -43
- package/src/cli/desktop-pane-shot.ts +3 -0
- package/src/cli/help.ts +26 -6
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index.ts +12 -7
- package/src/cli/options.ts +5 -1
- package/src/cli/pane-functions/capabilities.ts +5 -0
- package/src/cli/pane-functions/dom.ts +69 -16
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +163 -31
- package/src/cli/pane-functions/index.ts +9 -2
- package/src/cli/pane-functions/report.ts +8 -1
- package/src/cli/pane-functions/resolver.ts +28 -2
- package/src/cli/pane-functions/screenshot.ts +4 -0
- package/src/cli/registry.ts +6 -3
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +13 -3
- package/src/components/data-table/export.ts +6 -51
- package/src/components/data-table/view.tsx +3 -1
- package/src/components/layout/shell/active-drag.ts +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +12 -7
- package/src/components/layout/shell/drag/live.ts +122 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +37 -51
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/layers.tsx +419 -179
- package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- package/src/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- package/src/market-data/market/freshness.ts +45 -0
- package/src/market-data/quotes/freshness.ts +5 -0
- package/src/plugins/builtin/analytics/pane-model.ts +10 -6
- package/src/plugins/builtin/attention/pane.tsx +1 -1
- package/src/plugins/builtin/catalysts/detail.tsx +1 -1
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/holders/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +26 -5
- package/src/plugins/builtin/holders/pane.tsx +22 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- package/src/plugins/builtin/members/pane.tsx +10 -0
- package/src/plugins/builtin/perps/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
- package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
- package/src/plugins/builtin/shared/members-menu.tsx +260 -0
- package/src/plugins/builtin/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +5 -5
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- package/src/renderers/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -0
- package/src/renderers/dom/host/box.tsx +113 -17
- package/src/renderers/dom/host/drag-shield.ts +24 -0
- package/src/renderers/dom/host/live-frame.ts +54 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/styles.css +13 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/financial-routes.ts +2 -2
- package/src/sources/provider-router/financials.ts +70 -7
- package/src/sources/provider-router/primary.ts +2 -2
- package/src/state/pane-scroll-registry.tsx +67 -35
- package/src/test-support/margin-account.ts +108 -0
- package/src/time-series/resolution.ts +15 -0
- package/src/types/financials.ts +32 -3
- package/src/ui/host.tsx +12 -0
- package/src/ui/index.tsx +8 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/price-history-integrity.ts +19 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
floatAtRect,
|
|
4
4
|
getRememberedFloatingRect,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type DockGeometryOptions,
|
|
8
8
|
type DockLeafLayout,
|
|
9
9
|
type DropTarget,
|
|
10
|
+
type FloatingRect,
|
|
10
11
|
type LayoutBounds,
|
|
11
12
|
type ResolvedPane,
|
|
12
13
|
} from "../../../layout/pane-manager";
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
resolveHoverOverlay,
|
|
24
25
|
resolvePaneDragFloatingRect,
|
|
25
26
|
resolveSnapGuide,
|
|
27
|
+
type DragPreview,
|
|
26
28
|
} from "./drag";
|
|
27
29
|
import type { ShellDragRuntimeState, ShellMouseEvent } from "./drag/runtime";
|
|
28
30
|
import type { WindowEditState } from "../window-edit/mode";
|
|
@@ -65,37 +67,74 @@ export function useShellActiveDrag({
|
|
|
65
67
|
windowMode,
|
|
66
68
|
}: UseShellActiveDragOptions) {
|
|
67
69
|
const {
|
|
68
|
-
|
|
69
|
-
dockPreviewRef,
|
|
70
|
+
cancelActiveDrag,
|
|
70
71
|
dragRef,
|
|
71
|
-
|
|
72
|
+
live,
|
|
73
|
+
setHoverTarget,
|
|
72
74
|
updateDividerPreview,
|
|
73
75
|
updateDockPreview,
|
|
74
76
|
updateDragFloatingRect,
|
|
75
77
|
} = dragRuntime;
|
|
78
|
+
// What one drag keeps between moves: a docked pane's floating size, and the
|
|
79
|
+
// last drop it simulated, so a move over the same target computes nothing.
|
|
80
|
+
const memoRef = useRef<{
|
|
81
|
+
drag: unknown;
|
|
82
|
+
baseRect: FloatingRect | null;
|
|
83
|
+
dropKey: string | null;
|
|
84
|
+
drop: DragPreview | null;
|
|
85
|
+
}>({ drag: null, baseRect: null, dropKey: null, drop: null });
|
|
76
86
|
|
|
77
87
|
return useCallback((event: ShellMouseEvent) => {
|
|
78
88
|
const shellY = event.y - appHeaderHeight;
|
|
79
89
|
const preciseX = event.preciseX ?? event.x;
|
|
80
90
|
const preciseShellY = (event.preciseY ?? event.y) - appHeaderHeight;
|
|
91
|
+
// Drawn a moment ahead where the host predicts the pointer; hit tests and
|
|
92
|
+
// the release use where it is.
|
|
93
|
+
const drawX = event.predictedX ?? preciseX;
|
|
94
|
+
const drawShellY = event.predictedY != null ? event.predictedY - appHeaderHeight : preciseShellY;
|
|
81
95
|
const hitX = precisePointer ? preciseX : event.x;
|
|
82
96
|
const hitShellY = precisePointer ? preciseShellY : shellY;
|
|
83
97
|
const dragThreshold = precisePointer ? PRECISE_PANE_DRAG_THRESHOLD : PANE_DRAG_THRESHOLD;
|
|
84
98
|
const drag = dragRef.current;
|
|
85
99
|
if (!drag) return;
|
|
86
100
|
const baseLayout = windowMode?.previewLayout ?? visibleLayout;
|
|
101
|
+
const memo = memoRef.current;
|
|
102
|
+
if (memo.drag !== drag) {
|
|
103
|
+
memo.drag = drag;
|
|
104
|
+
memo.baseRect = null;
|
|
105
|
+
memo.dropKey = null;
|
|
106
|
+
memo.drop = null;
|
|
107
|
+
}
|
|
108
|
+
const dividerRatioAt = (x: number, y: number) => {
|
|
109
|
+
if (drag.type !== "divider") return 0;
|
|
110
|
+
const total = drag.axis === "horizontal" ? drag.bounds.width : drag.bounds.height;
|
|
111
|
+
const delta = drag.axis === "horizontal" ? x - drag.startX : y - drag.startY;
|
|
112
|
+
return Math.max(0.1, Math.min(0.9, drag.startRatio + (delta / Math.max(1, total))));
|
|
113
|
+
};
|
|
114
|
+
const dragBaseRect = () => {
|
|
115
|
+
if (drag.type !== "pane-drag") return null;
|
|
116
|
+
if (drag.mode === "floating") return drag.origRect;
|
|
117
|
+
memo.baseRect ??= getRememberedFloatingRect(baseLayout, drag.paneId, width, contentHeight, paneMap.get(drag.paneId)?.def);
|
|
118
|
+
return memo.baseRect;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (event.type === "drag-cancel") {
|
|
122
|
+
// The window lost focus or the system took the pointer: put everything back.
|
|
123
|
+
cancelActiveDrag();
|
|
124
|
+
event.stopPropagation();
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
87
128
|
|
|
88
129
|
if (event.type === "drag") {
|
|
89
130
|
if (drag.type === "divider") {
|
|
90
|
-
const
|
|
91
|
-
const delta = drag.axis === "horizontal" ? preciseX - drag.startX : preciseShellY - drag.startY;
|
|
92
|
-
const nextRatio = Math.max(0.1, Math.min(0.9, drag.startRatio + (delta / Math.max(1, total))));
|
|
131
|
+
const nextRatio = dividerRatioAt(drawX, drawShellY);
|
|
93
132
|
const nextRect = resolveDividerPreviewRect(drag.axis, drag.bounds, nextRatio, nativePaneChrome === true);
|
|
94
133
|
updateDividerPreview({ pathKey: drag.path.join("."), rect: nextRect, ratio: nextRatio });
|
|
95
134
|
} else if (drag.type === "pane-drag") {
|
|
96
135
|
if (!isMeaningfulPaneDrag(drag.startX, drag.startY, preciseX, preciseShellY, dragThreshold)) {
|
|
97
136
|
updateDockPreview(null);
|
|
98
|
-
|
|
137
|
+
setHoverTarget(null);
|
|
99
138
|
if (drag.mode === "floating") {
|
|
100
139
|
updateDragFloatingRect({ paneId: drag.paneId, rect: drag.origRect });
|
|
101
140
|
}
|
|
@@ -104,25 +143,22 @@ export function useShellActiveDrag({
|
|
|
104
143
|
return;
|
|
105
144
|
}
|
|
106
145
|
|
|
107
|
-
const
|
|
108
|
-
const baseRect = drag.mode === "docked"
|
|
109
|
-
? getRememberedFloatingRect(baseLayout, drag.paneId, width, contentHeight, pane?.def)
|
|
110
|
-
: drag.origRect;
|
|
111
|
-
const nextRect = resolvePaneDragFloatingRect(drag, baseRect, preciseX, preciseShellY, width, contentHeight);
|
|
146
|
+
const nextRect = resolvePaneDragFloatingRect(drag, dragBaseRect()!, drawX, drawShellY, width, contentHeight);
|
|
112
147
|
updateDragFloatingRect({ paneId: drag.paneId, rect: nextRect });
|
|
113
|
-
setDragCursor({ x: hitX, y: hitShellY });
|
|
114
148
|
|
|
115
149
|
const hoveredOverlay = resolveHoverOverlay(hitX, hitShellY, dockLeafLayouts, drag.paneId);
|
|
150
|
+
setHoverTarget(hoveredOverlay?.targetId ?? null);
|
|
116
151
|
if (hoveredOverlay) {
|
|
117
152
|
const hoveredCell = hoveredOverlay.cells.find((cell) => pointInRect(cell.rect, hitX, hitShellY));
|
|
118
153
|
if (hoveredCell) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
const dropKey = `${hoveredOverlay.targetId}:${hoveredCell.position}`;
|
|
155
|
+
if (memo.dropKey !== dropKey) {
|
|
156
|
+
const target: DropTarget = { kind: "leaf", targetId: hoveredOverlay.targetId, position: hoveredCell.position };
|
|
157
|
+
const simulation = simulateDrop(baseLayout, drag.paneId, target, bounds, dockGeometryOptions);
|
|
158
|
+
memo.dropKey = dropKey;
|
|
159
|
+
memo.drop = simulation.previewRect ? { kind: "dock", target, rect: simulation.previewRect } : null;
|
|
125
160
|
}
|
|
161
|
+
updateDockPreview(memo.drop);
|
|
126
162
|
} else {
|
|
127
163
|
updateDockPreview(null);
|
|
128
164
|
}
|
|
@@ -143,9 +179,8 @@ export function useShellActiveDrag({
|
|
|
143
179
|
|
|
144
180
|
if (event.type === "up" || event.type === "drag-end") {
|
|
145
181
|
if (drag.type === "divider") {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const nextLayout = resizeSplitAtPath(baseLayout, drag.path, preview.ratio);
|
|
182
|
+
if (live.get().divider) {
|
|
183
|
+
const nextLayout = resizeSplitAtPath(baseLayout, drag.path, dividerRatioAt(preciseX, preciseShellY));
|
|
149
184
|
if (windowMode) {
|
|
150
185
|
updateWindowModePreviewLayout(nextLayout);
|
|
151
186
|
} else {
|
|
@@ -157,15 +192,11 @@ export function useShellActiveDrag({
|
|
|
157
192
|
const movedEnough = isMeaningfulPaneDrag(drag.startX, drag.startY, preciseX, preciseShellY, dragThreshold);
|
|
158
193
|
if (!movedEnough) {
|
|
159
194
|
updateDockPreview(null);
|
|
160
|
-
|
|
195
|
+
setHoverTarget(null);
|
|
161
196
|
updateDragFloatingRect(null);
|
|
162
197
|
} else {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
? getRememberedFloatingRect(baseLayout, drag.paneId, width, contentHeight, pane?.def)
|
|
166
|
-
: drag.origRect;
|
|
167
|
-
const releaseRect = resolvePaneDragFloatingRect(drag, baseRect, preciseX, preciseShellY, width, contentHeight);
|
|
168
|
-
const nextLayout = finalizePaneDragRelease(baseLayout, drag.paneId, releaseRect, dockPreviewRef.current);
|
|
198
|
+
const releaseRect = resolvePaneDragFloatingRect(drag, dragBaseRect()!, preciseX, preciseShellY, width, contentHeight);
|
|
199
|
+
const nextLayout = finalizePaneDragRelease(baseLayout, drag.paneId, releaseRect, live.get().dockPreview);
|
|
169
200
|
if (windowMode) {
|
|
170
201
|
updateWindowModePreviewLayout(nextLayout, drag.paneId);
|
|
171
202
|
} else {
|
|
@@ -173,7 +204,7 @@ export function useShellActiveDrag({
|
|
|
173
204
|
}
|
|
174
205
|
focusPane(drag.paneId);
|
|
175
206
|
updateDockPreview(null);
|
|
176
|
-
|
|
207
|
+
setHoverTarget(null);
|
|
177
208
|
updateDragFloatingRect(null);
|
|
178
209
|
}
|
|
179
210
|
} else if (drag.type === "float-resize") {
|
|
@@ -185,27 +216,28 @@ export function useShellActiveDrag({
|
|
|
185
216
|
persistLayout(nextLayout);
|
|
186
217
|
}
|
|
187
218
|
updateDragFloatingRect(null);
|
|
188
|
-
|
|
219
|
+
setHoverTarget(null);
|
|
189
220
|
}
|
|
190
221
|
dragRef.current = null;
|
|
222
|
+
live.set({ paneDrag: null });
|
|
191
223
|
event.stopPropagation();
|
|
192
224
|
event.preventDefault();
|
|
193
225
|
}
|
|
194
226
|
}, [
|
|
195
227
|
appHeaderHeight,
|
|
196
228
|
bounds,
|
|
229
|
+
cancelActiveDrag,
|
|
197
230
|
contentHeight,
|
|
198
231
|
dockGeometryOptions,
|
|
199
|
-
dividerPreviewRef,
|
|
200
232
|
dockLeafLayouts,
|
|
201
|
-
dockPreviewRef,
|
|
202
233
|
dragRef,
|
|
203
234
|
focusPane,
|
|
235
|
+
live,
|
|
204
236
|
nativePaneChrome,
|
|
205
237
|
paneMap,
|
|
206
238
|
persistLayout,
|
|
207
239
|
precisePointer,
|
|
208
|
-
|
|
240
|
+
setHoverTarget,
|
|
209
241
|
snapGuides,
|
|
210
242
|
updateDividerPreview,
|
|
211
243
|
updateDockPreview,
|
|
@@ -160,15 +160,16 @@ function shouldHideCursor({
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
export function useShellCursorOcclusionGuard({
|
|
163
|
-
|
|
163
|
+
resolveOcclusionRects,
|
|
164
164
|
shellRef,
|
|
165
165
|
}: {
|
|
166
|
-
|
|
166
|
+
/** Called on every terminal frame. */
|
|
167
|
+
resolveOcclusionRects: () => readonly ShellCursorOcclusionRect[];
|
|
167
168
|
shellRef: RefObject<unknown>;
|
|
168
169
|
}) {
|
|
169
170
|
const nativeRenderer = useNativeRenderer();
|
|
170
|
-
const stateRef = useRef({
|
|
171
|
-
stateRef.current = {
|
|
171
|
+
const stateRef = useRef({ resolveOcclusionRects, shellRef });
|
|
172
|
+
stateRef.current = { resolveOcclusionRects, shellRef };
|
|
172
173
|
|
|
173
174
|
useEffect(() => {
|
|
174
175
|
if (
|
|
@@ -186,7 +187,7 @@ export function useShellCursorOcclusionGuard({
|
|
|
186
187
|
const shellBounds = readRenderableBounds(stateRef.current.shellRef.current);
|
|
187
188
|
if (!shellBounds) return;
|
|
188
189
|
|
|
189
|
-
const terminalRects = stateRef.current.
|
|
190
|
+
const terminalRects = stateRef.current.resolveOcclusionRects().map((rect) => toTerminalRect(shellBounds, rect));
|
|
190
191
|
if (terminalRects.length === 0) return;
|
|
191
192
|
|
|
192
193
|
const cursorOwnerPaneId = readRenderablePaneId(nativeRenderer.currentFocusedEditor);
|
|
@@ -212,6 +212,16 @@ function makeOverlayCellRects(rect: LayoutBounds): HoverOverlay["cells"] {
|
|
|
212
212
|
return cells;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** The drop grid drawn over a docked pane while another pane is dragged onto it. */
|
|
216
|
+
export function hoverOverlayForLeaf(leaf: DockLeafLayout): HoverOverlay {
|
|
217
|
+
const overlayRect = compactOverlayRect(leaf.rect);
|
|
218
|
+
return {
|
|
219
|
+
targetId: leaf.instanceId,
|
|
220
|
+
rect: overlayRect,
|
|
221
|
+
cells: makeOverlayCellRects(overlayRect),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
215
225
|
export function resolveHoverOverlay(
|
|
216
226
|
x: number,
|
|
217
227
|
y: number,
|
|
@@ -220,13 +230,8 @@ export function resolveHoverOverlay(
|
|
|
220
230
|
): HoverOverlay | null {
|
|
221
231
|
const targetLeaf = leaves.find((leaf) => leaf.instanceId !== draggedPaneId && pointInRect(leaf.rect, x, y));
|
|
222
232
|
if (!targetLeaf) return null;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
targetId: targetLeaf.instanceId,
|
|
227
|
-
rect: overlayRect,
|
|
228
|
-
cells: makeOverlayCellRects(overlayRect),
|
|
229
|
-
};
|
|
233
|
+
const overlay = hoverOverlayForLeaf(targetLeaf);
|
|
234
|
+
return pointInRect(overlay.rect, x, y) ? overlay : null;
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
export function resolveDividerPreviewRect(
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useSyncExternalStore, type RefObject } from "react";
|
|
2
|
+
import type { DockLeafLayout, FloatingRect } from "../../../../layout/pane-manager";
|
|
3
|
+
import type { BoxRenderable, LiveBoxFrame } from "../../../../ui";
|
|
4
|
+
import type { DividerPreviewState } from "../native/window-state";
|
|
5
|
+
import { hoverOverlayForLeaf, type DragPreview, type HoverOverlay } from "./index";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where an in-progress drag is right now: the pane on the move, its floating
|
|
9
|
+
* rect, the divider line and the drop target. It lives outside React state,
|
|
10
|
+
* so the shell renders nothing while a drag follows the pointer.
|
|
11
|
+
*/
|
|
12
|
+
export interface LiveDragGeometry {
|
|
13
|
+
/** The pane being moved, from mouse down to release. */
|
|
14
|
+
paneDrag: { paneId: string; mode: "docked" | "floating" } | null;
|
|
15
|
+
/** The floating rect being moved or resized, already kept inside the shell. */
|
|
16
|
+
floating: { paneId: string; rect: FloatingRect } | null;
|
|
17
|
+
divider: DividerPreviewState | null;
|
|
18
|
+
/** The tiled pane whose drop grid is under the pointer during a pane move. */
|
|
19
|
+
hoverTargetId: string | null;
|
|
20
|
+
/** Where the release would dock or snap. */
|
|
21
|
+
dockPreview: DragPreview | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Two kinds of change. `set` is something React draws: a drag starting or
|
|
26
|
+
* ending, or a new drop target; it reaches every subscriber. `move` is the
|
|
27
|
+
* desktop following the pointer: it reaches only the elements that restyle
|
|
28
|
+
* themselves to follow it (`subscribeMotion`), so a move renders nothing. The
|
|
29
|
+
* terminal redraws cells, so it sends every change through `set`.
|
|
30
|
+
*/
|
|
31
|
+
export interface LiveDragStore {
|
|
32
|
+
get(): LiveDragGeometry;
|
|
33
|
+
set(patch: Partial<LiveDragGeometry>): void;
|
|
34
|
+
move(patch: Partial<LiveDragGeometry>): void;
|
|
35
|
+
subscribe(listener: () => void): () => void;
|
|
36
|
+
subscribeMotion(listener: () => void): () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const IDLE_DRAG: LiveDragGeometry = { paneDrag: null, floating: null, divider: null, hoverTargetId: null, dockPreview: null };
|
|
40
|
+
|
|
41
|
+
export function createLiveDragStore(): LiveDragStore {
|
|
42
|
+
let geometry = IDLE_DRAG;
|
|
43
|
+
const listeners = new Set<() => void>();
|
|
44
|
+
const motionListeners = new Set<() => void>();
|
|
45
|
+
const apply = (patch: Partial<LiveDragGeometry>): boolean => {
|
|
46
|
+
let changed = false;
|
|
47
|
+
for (const key of Object.keys(patch) as Array<keyof LiveDragGeometry>) {
|
|
48
|
+
if (patch[key] !== geometry[key]) changed = true;
|
|
49
|
+
}
|
|
50
|
+
if (changed) geometry = { ...geometry, ...patch };
|
|
51
|
+
return changed;
|
|
52
|
+
};
|
|
53
|
+
const notify = (targets: Set<() => void>) => {
|
|
54
|
+
for (const listener of targets) listener();
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get: () => geometry,
|
|
58
|
+
set(patch) {
|
|
59
|
+
if (!apply(patch)) return;
|
|
60
|
+
notify(motionListeners);
|
|
61
|
+
notify(listeners);
|
|
62
|
+
},
|
|
63
|
+
move(patch) {
|
|
64
|
+
if (apply(patch)) notify(motionListeners);
|
|
65
|
+
},
|
|
66
|
+
subscribe(listener) {
|
|
67
|
+
listeners.add(listener);
|
|
68
|
+
return () => { listeners.delete(listener); };
|
|
69
|
+
},
|
|
70
|
+
subscribeMotion(listener) {
|
|
71
|
+
motionListeners.add(listener);
|
|
72
|
+
return () => { motionListeners.delete(listener); };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Re-renders the caller only when `select` returns something new; keep its result stable. */
|
|
78
|
+
export function useLiveDrag<T>(store: LiveDragStore, select: (geometry: LiveDragGeometry) => T): T {
|
|
79
|
+
const getSnapshot = useCallback(() => select(store.get()), [select, store]);
|
|
80
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sameFrame(a: LiveBoxFrame | null, b: LiveBoxFrame | null): boolean {
|
|
84
|
+
if (a === null || b === null) return a === b;
|
|
85
|
+
return a.dx === b.dx && a.dy === b.dy;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Desktop: keeps `box` where the drag has it by restyling its element on every
|
|
90
|
+
* move (`setLiveFrame`), with no render. `frameOf` must be stable while its
|
|
91
|
+
* inputs are; a new one redraws the box once.
|
|
92
|
+
*/
|
|
93
|
+
export function useLiveBoxFrame(
|
|
94
|
+
store: LiveDragStore,
|
|
95
|
+
box: RefObject<BoxRenderable | null>,
|
|
96
|
+
frameOf: ((geometry: LiveDragGeometry) => LiveBoxFrame | null) | null,
|
|
97
|
+
): void {
|
|
98
|
+
// What the element carries now, across re-subscriptions.
|
|
99
|
+
const drawn = useRef<LiveBoxFrame | null>(null);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
if (!frameOf) return;
|
|
102
|
+
const follow = () => {
|
|
103
|
+
const next = frameOf(store.get());
|
|
104
|
+
if (sameFrame(drawn.current, next)) return;
|
|
105
|
+
drawn.current = next;
|
|
106
|
+
box.current?.setLiveFrame?.(next);
|
|
107
|
+
};
|
|
108
|
+
follow();
|
|
109
|
+
return store.subscribeMotion(follow);
|
|
110
|
+
}, [box, frameOf, store]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The drop grid under the pointer during a pane move. Re-renders only when the pointer enters another pane's grid. */
|
|
114
|
+
export function useLiveHoverOverlay(store: LiveDragStore, leaves: DockLeafLayout[]): HoverOverlay | null {
|
|
115
|
+
const targetId = useLiveDrag(store, selectHoverTarget);
|
|
116
|
+
return useMemo(() => {
|
|
117
|
+
const leaf = targetId ? leaves.find((entry) => entry.instanceId === targetId) : undefined;
|
|
118
|
+
return leaf ? hoverOverlayForLeaf(leaf) : null;
|
|
119
|
+
}, [leaves, targetId]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selectHoverTarget = (geometry: LiveDragGeometry) => (geometry.paneDrag ? geometry.hoverTargetId : null);
|
|
@@ -1,28 +1,76 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef, type CSSProperties, type RefObject } from "react";
|
|
2
|
+
import { Box, useUiCapabilities, type BoxRenderable, type LiveBoxFrame } from "../../../../ui";
|
|
2
3
|
import { colors } from "../../../../theme/colors";
|
|
3
|
-
import type {
|
|
4
|
+
import type { DockLeafLayout, LayoutBounds } from "../../../../layout/pane-manager";
|
|
4
5
|
import type { DragPreview, HoverOverlay } from "./index";
|
|
6
|
+
import { useLiveBoxFrame, useLiveDrag, useLiveHoverOverlay, type LiveDragGeometry, type LiveDragStore } from "./live";
|
|
7
|
+
import { placeStyle } from "./slide";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const CELL_POSITIONS = ["top", "left", "center", "right", "bottom"] as const;
|
|
10
|
+
|
|
11
|
+
function selectDockedOutline(geometry: LiveDragGeometry) {
|
|
12
|
+
const { paneDrag, floating } = geometry;
|
|
13
|
+
return paneDrag?.mode === "docked" && floating?.paneId === paneDrag.paneId ? floating.rect : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const selectDockPreview = (geometry: LiveDragGeometry) => geometry.dockPreview;
|
|
17
|
+
const selectPaneDragActive = (geometry: LiveDragGeometry) => geometry.paneDrag !== null;
|
|
18
|
+
const selectOutlineWidth = (geometry: LiveDragGeometry) => selectDockedOutline(geometry)?.width ?? null;
|
|
19
|
+
const selectOutlineHeight = (geometry: LiveDragGeometry) => selectDockedOutline(geometry)?.height ?? null;
|
|
20
|
+
|
|
21
|
+
function isActiveCell(dockPreview: DragPreview | null, overlay: HoverOverlay, position: string): boolean {
|
|
22
|
+
return dockPreview?.kind === "dock"
|
|
23
|
+
&& dockPreview.target.kind === "leaf"
|
|
24
|
+
&& dockPreview.target.targetId === overlay.targetId
|
|
25
|
+
&& dockPreview.target.position === position;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ShellDragOverlays(props: {
|
|
29
|
+
dockLeafLayouts: DockLeafLayout[];
|
|
30
|
+
/** A pane from another window over this one's edge. */
|
|
31
|
+
externalDockPreview: DragPreview | null;
|
|
32
|
+
live: LiveDragStore;
|
|
33
|
+
}) {
|
|
34
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
35
|
+
return nativePaneChrome ? <DesktopDragOverlays {...props} /> : <TerminalDragOverlays {...props} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A docked pane on the move is drawn as an outline at its floating size, until it is over a drop target. */
|
|
39
|
+
function TerminalDockedDragOutline({ live }: { live: LiveDragStore }) {
|
|
40
|
+
const rect = useLiveDrag(live, selectDockedOutline);
|
|
41
|
+
if (!rect) return null;
|
|
42
|
+
return (
|
|
43
|
+
<Box
|
|
44
|
+
position="absolute"
|
|
45
|
+
left={rect.x}
|
|
46
|
+
top={rect.y}
|
|
47
|
+
width={rect.width}
|
|
48
|
+
height={rect.height}
|
|
49
|
+
border
|
|
50
|
+
borderStyle="single"
|
|
51
|
+
borderColor={colors.borderFocused}
|
|
52
|
+
backgroundColor={colors.panel}
|
|
53
|
+
zIndex={95}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function TerminalDragOverlays({
|
|
59
|
+
dockLeafLayouts,
|
|
60
|
+
externalDockPreview,
|
|
61
|
+
live,
|
|
12
62
|
}: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
|
|
17
|
-
effectiveDockPreview: DragPreview | null;
|
|
63
|
+
dockLeafLayouts: DockLeafLayout[];
|
|
64
|
+
externalDockPreview: DragPreview | null;
|
|
65
|
+
live: LiveDragStore;
|
|
18
66
|
}) {
|
|
67
|
+
const activeHoverOverlay = useLiveHoverOverlay(live, dockLeafLayouts);
|
|
68
|
+
const dockPreview = useLiveDrag(live, selectDockPreview);
|
|
69
|
+
const effectiveDockPreview = dockPreview ?? externalDockPreview;
|
|
19
70
|
return (
|
|
20
71
|
<>
|
|
21
72
|
{activeHoverOverlay && activeHoverOverlay.cells.map((cell) => {
|
|
22
|
-
const active = dockPreview
|
|
23
|
-
&& dockPreview.target.kind === "leaf"
|
|
24
|
-
&& dockPreview.target.targetId === activeHoverOverlay.targetId
|
|
25
|
-
&& dockPreview.target.position === cell.position;
|
|
73
|
+
const active = isActiveCell(dockPreview, activeHoverOverlay, cell.position);
|
|
26
74
|
return (
|
|
27
75
|
<Box
|
|
28
76
|
key={`cell:${activeHoverOverlay.targetId}:${cell.position}`}
|
|
@@ -40,24 +88,7 @@ export function ShellDragOverlays({
|
|
|
40
88
|
);
|
|
41
89
|
})}
|
|
42
90
|
|
|
43
|
-
{
|
|
44
|
-
&& activePaneDrag.mode === "docked"
|
|
45
|
-
&& dragFloatingRect?.paneId === activePaneDrag.paneId
|
|
46
|
-
&& !effectiveDockPreview
|
|
47
|
-
&& (
|
|
48
|
-
<Box
|
|
49
|
-
position="absolute"
|
|
50
|
-
left={dragFloatingRect.rect.x}
|
|
51
|
-
top={dragFloatingRect.rect.y}
|
|
52
|
-
width={dragFloatingRect.rect.width}
|
|
53
|
-
height={dragFloatingRect.rect.height}
|
|
54
|
-
border
|
|
55
|
-
borderStyle="single"
|
|
56
|
-
borderColor={colors.borderFocused}
|
|
57
|
-
backgroundColor={colors.panel}
|
|
58
|
-
zIndex={95}
|
|
59
|
-
/>
|
|
60
|
-
)}
|
|
91
|
+
{!effectiveDockPreview && <TerminalDockedDragOutline live={live} />}
|
|
61
92
|
|
|
62
93
|
{effectiveDockPreview && (
|
|
63
94
|
<Box
|
|
@@ -76,3 +107,116 @@ export function ShellDragOverlays({
|
|
|
76
107
|
</>
|
|
77
108
|
);
|
|
78
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Desktop: an outline laid out at the shell's corner and drawn at `rect` by a
|
|
113
|
+
* transform, hidden by opacity, so a new drop target restyles it in place.
|
|
114
|
+
*/
|
|
115
|
+
function OutlineBox({
|
|
116
|
+
rect,
|
|
117
|
+
zIndex,
|
|
118
|
+
borderColor = colors.borderFocused,
|
|
119
|
+
backgroundColor = colors.panel,
|
|
120
|
+
role,
|
|
121
|
+
boxRef,
|
|
122
|
+
follows = false,
|
|
123
|
+
}: {
|
|
124
|
+
rect: LayoutBounds | null;
|
|
125
|
+
zIndex: number;
|
|
126
|
+
borderColor?: string;
|
|
127
|
+
backgroundColor?: string;
|
|
128
|
+
role: string;
|
|
129
|
+
boxRef?: RefObject<BoxRenderable | null>;
|
|
130
|
+
/** Its position follows the pointer outside React (`useLiveBoxFrame`). */
|
|
131
|
+
follows?: boolean;
|
|
132
|
+
}) {
|
|
133
|
+
const style: CSSProperties = {
|
|
134
|
+
...(rect && !follows ? placeStyle(rect.x, rect.y) : undefined),
|
|
135
|
+
opacity: rect ? 1 : 0,
|
|
136
|
+
pointerEvents: "none",
|
|
137
|
+
};
|
|
138
|
+
return (
|
|
139
|
+
<Box
|
|
140
|
+
ref={boxRef}
|
|
141
|
+
position="absolute"
|
|
142
|
+
left={0}
|
|
143
|
+
top={0}
|
|
144
|
+
width={rect?.width ?? 0}
|
|
145
|
+
height={rect?.height ?? 0}
|
|
146
|
+
border
|
|
147
|
+
borderStyle="single"
|
|
148
|
+
borderColor={borderColor}
|
|
149
|
+
backgroundColor={backgroundColor}
|
|
150
|
+
zIndex={zIndex}
|
|
151
|
+
data-gloom-role={role}
|
|
152
|
+
style={style}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Desktop overlays of a pane move: the outline a docked pane drags around, the
|
|
159
|
+
* drop grid of the pane under the pointer and the dock or snap preview. They
|
|
160
|
+
* render when the drop target changes, never on a move: the outline follows
|
|
161
|
+
* the pointer by restyling itself.
|
|
162
|
+
*/
|
|
163
|
+
function DesktopDragOverlays({
|
|
164
|
+
dockLeafLayouts,
|
|
165
|
+
externalDockPreview,
|
|
166
|
+
live,
|
|
167
|
+
}: {
|
|
168
|
+
dockLeafLayouts: DockLeafLayout[];
|
|
169
|
+
externalDockPreview: DragPreview | null;
|
|
170
|
+
live: LiveDragStore;
|
|
171
|
+
}) {
|
|
172
|
+
const dragging = useLiveDrag(live, selectPaneDragActive);
|
|
173
|
+
if (!dragging && !externalDockPreview) return null;
|
|
174
|
+
return <DesktopDragOverlayLayers dockLeafLayouts={dockLeafLayouts} externalDockPreview={externalDockPreview} live={live} />;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function DesktopDragOverlayLayers({
|
|
178
|
+
dockLeafLayouts,
|
|
179
|
+
externalDockPreview,
|
|
180
|
+
live,
|
|
181
|
+
}: {
|
|
182
|
+
dockLeafLayouts: DockLeafLayout[];
|
|
183
|
+
externalDockPreview: DragPreview | null;
|
|
184
|
+
live: LiveDragStore;
|
|
185
|
+
}) {
|
|
186
|
+
const activeHoverOverlay = useLiveHoverOverlay(live, dockLeafLayouts);
|
|
187
|
+
const dockPreview = useLiveDrag(live, selectDockPreview);
|
|
188
|
+
const effectiveDockPreview = dockPreview ?? externalDockPreview;
|
|
189
|
+
const outlineWidth = useLiveDrag(live, selectOutlineWidth);
|
|
190
|
+
const outlineHeight = useLiveDrag(live, selectOutlineHeight);
|
|
191
|
+
const outlineRef = useRef<BoxRenderable | null>(null);
|
|
192
|
+
useLiveBoxFrame(live, outlineRef, followDockedOutline);
|
|
193
|
+
const outline = outlineWidth !== null && outlineHeight !== null && !effectiveDockPreview
|
|
194
|
+
? { x: 0, y: 0, width: outlineWidth, height: outlineHeight }
|
|
195
|
+
: null;
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
{CELL_POSITIONS.map((position) => {
|
|
199
|
+
const cell = activeHoverOverlay?.cells.find((entry) => entry.position === position) ?? null;
|
|
200
|
+
const active = !!activeHoverOverlay && isActiveCell(dockPreview, activeHoverOverlay, position);
|
|
201
|
+
return (
|
|
202
|
+
<OutlineBox
|
|
203
|
+
key={position}
|
|
204
|
+
role="drop-cell"
|
|
205
|
+
rect={cell?.rect ?? null}
|
|
206
|
+
borderColor={active ? colors.borderFocused : colors.border}
|
|
207
|
+
backgroundColor={active ? colors.header : colors.panel}
|
|
208
|
+
zIndex={position === "center" ? 98 : 97}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
<OutlineBox role="drag-outline" rect={outline} zIndex={95} boxRef={outlineRef} follows />
|
|
213
|
+
<OutlineBox role="drop-preview" rect={effectiveDockPreview?.rect ?? null} zIndex={96} />
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The docked pane's outline sits where the drag has it; laid out at the shell's corner. */
|
|
219
|
+
function followDockedOutline(geometry: LiveDragGeometry): LiveBoxFrame | null {
|
|
220
|
+
const rect = selectDockedOutline(geometry);
|
|
221
|
+
return rect ? { dx: rect.x, dy: rect.y } : null;
|
|
222
|
+
}
|