@cplieger/web-terminal-ui 8.0.1 → 8.1.1
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/README.md +53 -27
- package/css/00-tokens.css +1 -1
- package/css/01-scope.css +1 -1
- package/css/10-primitives.css +1 -1
- package/css/21-scroll-to-bottom.css +3 -3
- package/css/30-tabs.css +82 -29
- package/css/31-switcher.css +5 -0
- package/css/32-split.css +67 -15
- package/css/MANIFEST.touch +1 -1
- package/css/page.css +1 -1
- package/package.json +12 -12
- package/src/features/tabs/device-snap.ts +56 -0
- package/src/features/tabs/index.ts +212 -99
- package/src/features/tabs/strip.ts +8 -3
- package/src/kernel/pane.ts +34 -14
- package/src/kernel/shell.ts +91 -16
- package/src/kernel/split-handle.ts +37 -13
- package/src/kernel/types.ts +8 -1
- package/src/viewport.ts +166 -73
package/src/kernel/pane.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type TerminalEngine,
|
|
7
7
|
} from "@cplieger/web-terminal-engine";
|
|
8
8
|
import { createComposition } from "../composition.js";
|
|
9
|
-
import { createViewport } from "../viewport.js";
|
|
9
|
+
import { createViewport, type KeyboardInsets } from "../viewport.js";
|
|
10
10
|
import { INPUT_PLACEHOLDER, resetToPlaceholder } from "../input-placeholder.js";
|
|
11
11
|
import { createBus } from "./bus.js";
|
|
12
12
|
import { TAP_MAX_MS, TAP_MOVEMENT_PX, isLinkTarget } from "./gesture.js";
|
|
@@ -143,6 +143,9 @@ export interface PaneServices {
|
|
|
143
143
|
/** The narrow classification for this pane (the shell root's width, this
|
|
144
144
|
* pane's height). */
|
|
145
145
|
narrowProbe(): boolean;
|
|
146
|
+
/** The document's keyboard geometry, published on the shell root, which this
|
|
147
|
+
* pane's root inherits rather than publishes. */
|
|
148
|
+
readonly keyboardInsets: KeyboardInsets;
|
|
146
149
|
/** This pane's base title (the served `<title>`, or its program's OSC 0/2). */
|
|
147
150
|
titleBase(text: string): void;
|
|
148
151
|
readonly loading: PaneLoading;
|
|
@@ -209,6 +212,9 @@ export interface PaneKernel extends PaneHandle {
|
|
|
209
212
|
* from the moment the split closes rather than from the class the display
|
|
210
213
|
* gets when the closing transition ends. */
|
|
211
214
|
setHidden(value: boolean): void;
|
|
215
|
+
/** The shell is about to slide this pane's box: nothing it measures from now
|
|
216
|
+
* until its viewport settles is final, and the settle sends the size. */
|
|
217
|
+
holdGeometry(): void;
|
|
212
218
|
/** Whether this pane's input is a stop in the document's Tab order. On while
|
|
213
219
|
* the split is open and the pane shows a tab, so Tab runs left input, divider,
|
|
214
220
|
* right input, chrome; off otherwise, where typing or a click enters the one
|
|
@@ -218,6 +224,13 @@ export interface PaneKernel extends PaneHandle {
|
|
|
218
224
|
destroy(): void;
|
|
219
225
|
}
|
|
220
226
|
|
|
227
|
+
/** Whether `win` has a fine pointer, and with it a hardware keyboard. Read as
|
|
228
|
+
* `any-pointer: fine` rather than from pointerType because iPadOS reports a
|
|
229
|
+
* COARSE primary pointer even with a trackpad attached. */
|
|
230
|
+
export function hasFinePointer(win: Window): boolean {
|
|
231
|
+
return typeof win.matchMedia === "function" && win.matchMedia("(any-pointer: fine)").matches;
|
|
232
|
+
}
|
|
233
|
+
|
|
221
234
|
/** The consumer's theme as custom properties on `root`, so the whole subtree
|
|
222
235
|
* inherits them over the token defaults of css/00-tokens.css. */
|
|
223
236
|
export function applyTheme(root: HTMLElement, theme: CreateTerminalOptions["theme"]): void {
|
|
@@ -456,7 +469,9 @@ function buildPaneInto(
|
|
|
456
469
|
termWrap.classList.toggle("wt-mouse-app", engine.modes.getMouseMode() !== 0);
|
|
457
470
|
}
|
|
458
471
|
function measurableSize(): { cols: number; rows: number } | null {
|
|
459
|
-
|
|
472
|
+
// A pane hidden by a closed split is `display: none`: the cell width it would
|
|
473
|
+
// measure is 0, and the first render after a reveal would lay out on it.
|
|
474
|
+
if (!fontsLoaded || hidden || viewport.isInTransition()) {
|
|
460
475
|
return null;
|
|
461
476
|
}
|
|
462
477
|
engine.renderer.updateFontMetrics();
|
|
@@ -764,10 +779,7 @@ function buildPaneInto(
|
|
|
764
779
|
|
|
765
780
|
// On touch the output is the native text-selection surface, so this does the
|
|
766
781
|
// MINIMUM: it opens the keyboard on a clean tap and otherwise gets out of the
|
|
767
|
-
// browser's way.
|
|
768
|
-
// reports a COARSE primary pointer even with a trackpad attached.
|
|
769
|
-
const hasFinePointer = (): boolean =>
|
|
770
|
-
typeof win.matchMedia === "function" && win.matchMedia("(any-pointer: fine)").matches;
|
|
782
|
+
// browser's way.
|
|
771
783
|
let lastPointerType = "mouse";
|
|
772
784
|
let pointerDownX = 0;
|
|
773
785
|
let pointerDownY = 0;
|
|
@@ -827,7 +839,7 @@ function buildPaneInto(
|
|
|
827
839
|
const sel = doc.getSelection();
|
|
828
840
|
if (sel && !sel.isCollapsed) {
|
|
829
841
|
sel.removeAllRanges();
|
|
830
|
-
if (hasFinePointer()) {
|
|
842
|
+
if (hasFinePointer(win)) {
|
|
831
843
|
focusTerminal();
|
|
832
844
|
}
|
|
833
845
|
return;
|
|
@@ -843,7 +855,7 @@ function buildPaneInto(
|
|
|
843
855
|
// Cancel the synthetic mousedown after a touch tap so iOS keeps the
|
|
844
856
|
// keyboard up, except with a fine pointer, where suppressing it defeated
|
|
845
857
|
// the native focus.
|
|
846
|
-
if (!hasFinePointer()) {
|
|
858
|
+
if (!hasFinePointer(win)) {
|
|
847
859
|
e.preventDefault();
|
|
848
860
|
}
|
|
849
861
|
return;
|
|
@@ -872,7 +884,7 @@ function buildPaneInto(
|
|
|
872
884
|
win.open(link.href, "_blank", "noopener,noreferrer");
|
|
873
885
|
return;
|
|
874
886
|
}
|
|
875
|
-
if (lastPointerType === "touch" && !hasFinePointer()) {
|
|
887
|
+
if (lastPointerType === "touch" && !hasFinePointer(win)) {
|
|
876
888
|
return;
|
|
877
889
|
}
|
|
878
890
|
const sel = doc.getSelection();
|
|
@@ -956,9 +968,9 @@ function buildPaneInto(
|
|
|
956
968
|
|
|
957
969
|
const viewport = createViewport({
|
|
958
970
|
termWrap,
|
|
959
|
-
root,
|
|
971
|
+
box: root,
|
|
972
|
+
keyboard: services.keyboardInsets,
|
|
960
973
|
scroll: engine.scroll,
|
|
961
|
-
suppressKeyboardInset: hasFinePointer,
|
|
962
974
|
onSettled() {
|
|
963
975
|
// The settle is the one resize a keyboard slide or rotation should cost;
|
|
964
976
|
// sendResize deduplicates.
|
|
@@ -1157,10 +1169,13 @@ function buildPaneInto(
|
|
|
1157
1169
|
return;
|
|
1158
1170
|
}
|
|
1159
1171
|
detachSession();
|
|
1172
|
+
if (activeSession !== null) {
|
|
1173
|
+
services.noteEpoch(activeSession.id, engine.connection.serverEpochOf(activeSession.id));
|
|
1174
|
+
}
|
|
1160
1175
|
activeSession = session;
|
|
1161
|
-
// A session another pane
|
|
1162
|
-
// first resumeAck has nothing to compare against and a
|
|
1163
|
-
// the two attaches goes undetected.
|
|
1176
|
+
// A session that left another pane, emptied or replaced, arrives with the epoch
|
|
1177
|
+
// that pane knew, or the first resumeAck has nothing to compare against and a
|
|
1178
|
+
// server restart between the two attaches goes undetected.
|
|
1164
1179
|
const epoch = services.knownEpoch(session.id);
|
|
1165
1180
|
if (epoch !== 0) {
|
|
1166
1181
|
engine.connection.adoptPersistedEpoch(session.id, epoch);
|
|
@@ -1398,6 +1413,11 @@ function buildPaneInto(
|
|
|
1398
1413
|
setHidden(value) {
|
|
1399
1414
|
hidden = value;
|
|
1400
1415
|
},
|
|
1416
|
+
holdGeometry() {
|
|
1417
|
+
if (!destroyed) {
|
|
1418
|
+
viewport.beginTransition();
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1401
1421
|
setTabStop(value) {
|
|
1402
1422
|
input.tabIndex = value ? 0 : -1;
|
|
1403
1423
|
},
|
package/src/kernel/shell.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
applyTheme,
|
|
8
8
|
buildPane,
|
|
9
|
+
hasFinePointer,
|
|
9
10
|
type PaneKernel,
|
|
10
11
|
type PaneServices,
|
|
11
12
|
type StoreRegistry,
|
|
@@ -28,6 +29,7 @@ import { createRegions, type Regions } from "./regions.js";
|
|
|
28
29
|
import { createAnnouncer, createTablist, type Announcer, type PaneTablist } from "./a11y.js";
|
|
29
30
|
import { createSplitHandle, type SplitHandle } from "./split-handle.js";
|
|
30
31
|
import { windowOf } from "./realm.js";
|
|
32
|
+
import { createKeyboardInsets } from "../viewport.js";
|
|
31
33
|
import type {
|
|
32
34
|
AttentionOptions,
|
|
33
35
|
AttentionReporter,
|
|
@@ -132,16 +134,19 @@ type PaneFailure =
|
|
|
132
134
|
| { readonly phase: "kernel-init"; readonly cause: unknown }
|
|
133
135
|
| { readonly phase: "feature-setup"; readonly feature: string; readonly cause: unknown };
|
|
134
136
|
|
|
135
|
-
/**
|
|
136
|
-
* display finishes when the transition ends, on the
|
|
137
|
-
* `settle()`. */
|
|
138
|
-
interface
|
|
139
|
-
/** The ratio the columns slide to: 1 when the left pane survives, 0 when the right does. */
|
|
140
|
-
readonly toward: 0 | 1;
|
|
137
|
+
/** An open or a close whose columns are still sliding: the model is in its new
|
|
138
|
+
* state already and the display finishes when the transition ends, on the
|
|
139
|
+
* fallback timer, or on `settle()`. */
|
|
140
|
+
interface Slide {
|
|
141
141
|
/** Finish the display now. */
|
|
142
|
-
settle()
|
|
142
|
+
readonly settle: () => void;
|
|
143
143
|
/** Drop the pending finish without running it. */
|
|
144
|
-
abort()
|
|
144
|
+
readonly abort: () => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface ClosingTransition extends Slide {
|
|
148
|
+
/** The ratio the columns slide to: 1 when the left pane survives, 0 when the right does. */
|
|
149
|
+
readonly toward: 0 | 1;
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
/** A shell-scoped feature's registration on every built pane and on every pane
|
|
@@ -246,6 +251,7 @@ function createShellInto(
|
|
|
246
251
|
"wt-narrow",
|
|
247
252
|
"wt-split-open",
|
|
248
253
|
"wt-split-collapsed",
|
|
254
|
+
"wt-split-opening",
|
|
249
255
|
"wt-split-closing",
|
|
250
256
|
);
|
|
251
257
|
shellRoot.style.removeProperty("--wt-split-ratio");
|
|
@@ -263,6 +269,17 @@ function createShellInto(
|
|
|
263
269
|
});
|
|
264
270
|
}
|
|
265
271
|
const paneOpts: CreateTerminalOptions = splitEnabled ? { ...opts, layout: "container" } : opts;
|
|
272
|
+
// The visual viewport is the document's, so its geometry is read once and
|
|
273
|
+
// published on the outermost root, which the shell's chrome and every pane
|
|
274
|
+
// inherit; published on a pane root it would never reach the chrome beside
|
|
275
|
+
// the panes.
|
|
276
|
+
const keyboardInsets = createKeyboardInsets({
|
|
277
|
+
root: shellRoot,
|
|
278
|
+
suppressKeyboardInset: () => hasFinePointer(win),
|
|
279
|
+
});
|
|
280
|
+
held.push(() => {
|
|
281
|
+
keyboardInsets.teardown();
|
|
282
|
+
});
|
|
266
283
|
|
|
267
284
|
const loadingStatus = attachLoadingStatus(opts.loading, {
|
|
268
285
|
...DEFAULT_LOADING_MESSAGES,
|
|
@@ -308,9 +325,11 @@ function createShellInto(
|
|
|
308
325
|
const selectionListeners = new Set<(side: PaneSide) => void>();
|
|
309
326
|
const panesListeners = new Set<() => void>();
|
|
310
327
|
let closing: ClosingTransition | null = null;
|
|
328
|
+
let opening: Slide | null = null;
|
|
311
329
|
held.push(() => {
|
|
312
330
|
closing?.abort();
|
|
313
331
|
closing = null;
|
|
332
|
+
opening?.abort();
|
|
314
333
|
});
|
|
315
334
|
const selectedPane = (): PaneKernel => {
|
|
316
335
|
const p = paneAt(selectedSide) ?? slots[0]?.kernel ?? undefined;
|
|
@@ -420,20 +439,18 @@ function createShellInto(
|
|
|
420
439
|
function applyRatioVar(): void {
|
|
421
440
|
shellRoot.style.setProperty("--wt-split-ratio", String(closing?.toward ?? effectiveRatio()));
|
|
422
441
|
}
|
|
423
|
-
/** How long a close slides, from the same token the stylesheet's
|
|
424
|
-
* reads; 0 without `wt-animate`, and the change is then instant. */
|
|
425
|
-
function
|
|
442
|
+
/** How long an open or a close slides, from the same token the stylesheet's
|
|
443
|
+
* transition reads; 0 without `wt-animate`, and the change is then instant. */
|
|
444
|
+
function slideDurationMs(): number {
|
|
426
445
|
if (!shellRoot.classList.contains("wt-animate")) {
|
|
427
446
|
return 0;
|
|
428
447
|
}
|
|
429
448
|
return cssTimeMs(win.getComputedStyle(shellRoot).getPropertyValue("--dur-standard"));
|
|
430
449
|
}
|
|
431
|
-
function
|
|
432
|
-
shellRoot.classList.add("wt-split-closing");
|
|
450
|
+
function slideFor(durationMs: number, tail: () => void): Slide {
|
|
433
451
|
const stop = (): void => {
|
|
434
452
|
win.clearTimeout(timer);
|
|
435
453
|
shellRoot.removeEventListener("transitionend", onEnd);
|
|
436
|
-
closing = null;
|
|
437
454
|
};
|
|
438
455
|
const settle = (): void => {
|
|
439
456
|
stop();
|
|
@@ -447,7 +464,52 @@ function createShellInto(
|
|
|
447
464
|
shellRoot.addEventListener("transitionend", onEnd);
|
|
448
465
|
// An engine that applies a track-list change at once fires no transitionend.
|
|
449
466
|
const timer = win.setTimeout(settle, durationMs);
|
|
450
|
-
|
|
467
|
+
return { settle, abort: stop };
|
|
468
|
+
}
|
|
469
|
+
function beginClosing(toward: 0 | 1, durationMs: number, tail: () => void): void {
|
|
470
|
+
shellRoot.classList.add("wt-split-closing");
|
|
471
|
+
const slide = slideFor(durationMs, () => {
|
|
472
|
+
closing = null;
|
|
473
|
+
tail();
|
|
474
|
+
});
|
|
475
|
+
closing = {
|
|
476
|
+
toward,
|
|
477
|
+
settle: slide.settle,
|
|
478
|
+
abort: () => {
|
|
479
|
+
slide.abort();
|
|
480
|
+
closing = null;
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/** The divider comes in from the row's right edge to the share the model
|
|
485
|
+
* already holds, and the sizes were announced at that share: the slide's
|
|
486
|
+
* start shows the new pane 0 px wide, so the panes hold their geometry until
|
|
487
|
+
* it is over, or a socket opening meanwhile would size its PTY to that. */
|
|
488
|
+
function beginOpening(durationMs: number): void {
|
|
489
|
+
for (const p of builtPanes()) {
|
|
490
|
+
p.holdGeometry();
|
|
491
|
+
}
|
|
492
|
+
shellRoot.style.setProperty("--wt-split-ratio", "1");
|
|
493
|
+
// The read commits the start as a style of its own; without it the slide
|
|
494
|
+
// would start from the closed view's single column.
|
|
495
|
+
shellRoot.getBoundingClientRect();
|
|
496
|
+
shellRoot.classList.add("wt-split-opening");
|
|
497
|
+
const slide = slideFor(durationMs, () => {
|
|
498
|
+
opening = null;
|
|
499
|
+
shellRoot.classList.remove("wt-split-opening");
|
|
500
|
+
for (const p of builtPanes()) {
|
|
501
|
+
p.announceSize();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
opening = {
|
|
505
|
+
settle: slide.settle,
|
|
506
|
+
abort: () => {
|
|
507
|
+
slide.abort();
|
|
508
|
+
opening = null;
|
|
509
|
+
shellRoot.classList.remove("wt-split-opening");
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
applyRatioVar();
|
|
451
513
|
}
|
|
452
514
|
function fireSplitChange(): void {
|
|
453
515
|
if (changeListeners.size === 0) {
|
|
@@ -745,6 +807,9 @@ function createShellInto(
|
|
|
745
807
|
knownEpoch.delete(sessionId);
|
|
746
808
|
keeper?.forget(sessionId);
|
|
747
809
|
},
|
|
810
|
+
chromeResized() {
|
|
811
|
+
keyboardInsets.refresh();
|
|
812
|
+
},
|
|
748
813
|
attention,
|
|
749
814
|
subscribeStatus: (path, callbacks) => statusShare.subscribe(path, callbacks),
|
|
750
815
|
notifications,
|
|
@@ -775,6 +840,7 @@ function createShellInto(
|
|
|
775
840
|
managed,
|
|
776
841
|
scrollbackLines,
|
|
777
842
|
narrowProbe: () => isNarrow(shellRoot.clientWidth, slot.root.clientHeight),
|
|
843
|
+
keyboardInsets,
|
|
778
844
|
titleBase(text) {
|
|
779
845
|
slot.baseTitle = text;
|
|
780
846
|
paintTitle();
|
|
@@ -1053,6 +1119,10 @@ function createShellInto(
|
|
|
1053
1119
|
for (const p of builtPanes()) {
|
|
1054
1120
|
p.announceSize();
|
|
1055
1121
|
}
|
|
1122
|
+
const slideMs = collapsed ? 0 : slideDurationMs();
|
|
1123
|
+
if (slideMs > 0) {
|
|
1124
|
+
beginOpening(slideMs);
|
|
1125
|
+
}
|
|
1056
1126
|
announcer?.announce("Split open");
|
|
1057
1127
|
firePanesChange();
|
|
1058
1128
|
fireSplitChange();
|
|
@@ -1065,7 +1135,10 @@ function createShellInto(
|
|
|
1065
1135
|
keepFocusOn(survivor, (el) => leaving.root.contains(el) || isHandle(el));
|
|
1066
1136
|
leaving.kernel?.clearActiveSession();
|
|
1067
1137
|
leaving.kernel?.setHidden(true);
|
|
1068
|
-
|
|
1138
|
+
// The closing class carries the same transition, so a close during the
|
|
1139
|
+
// open's slide turns it around from where the columns are.
|
|
1140
|
+
opening?.abort();
|
|
1141
|
+
const durationMs = leaving.failed || collapsed ? 0 : slideDurationMs();
|
|
1069
1142
|
const tail = (): void => {
|
|
1070
1143
|
if (leaving.failed) {
|
|
1071
1144
|
teardownSlot(leaving);
|
|
@@ -1131,6 +1204,8 @@ function createShellInto(
|
|
|
1131
1204
|
if (!isRatio(ratio) || !splitOpen || collapsed || destroyed) {
|
|
1132
1205
|
return false;
|
|
1133
1206
|
}
|
|
1207
|
+
// A drag or a key moves the divider at once, not through the open's slide.
|
|
1208
|
+
opening?.settle();
|
|
1134
1209
|
if (commit) {
|
|
1135
1210
|
committedRatio = ratio;
|
|
1136
1211
|
previewRatio = null;
|
|
@@ -9,6 +9,9 @@ const DRAG_START_PX = 8;
|
|
|
9
9
|
const RESIZE_INTERVAL_MS = 100;
|
|
10
10
|
/** One arrow press moves the divider this far: two fallback cells of 8 px. */
|
|
11
11
|
const KEY_STEP_PX = 16;
|
|
12
|
+
/** Pointer travel past a pane's minimum width that holds the pane AT the
|
|
13
|
+
* minimum, so a drag can set it; only past this does a release close. */
|
|
14
|
+
const GRACE_PX = 120;
|
|
12
15
|
|
|
13
16
|
const NUDGE_KEYS: ReadonlySet<string> = new Set(["ArrowLeft", "ArrowRight", "Home", "End"]);
|
|
14
17
|
|
|
@@ -31,12 +34,20 @@ interface Drag {
|
|
|
31
34
|
readonly span: number;
|
|
32
35
|
/** Whether the pointer has travelled DRAG_START_PX yet. */
|
|
33
36
|
moving: boolean;
|
|
34
|
-
/** The pane
|
|
37
|
+
/** The pane past the grace zone right now: dimmed, and closed on release. */
|
|
35
38
|
squeezed: PaneSide | null;
|
|
36
|
-
/** Whether
|
|
39
|
+
/** Whether the divider is held at a pane's minimum right now. */
|
|
40
|
+
held: boolean;
|
|
41
|
+
/** Whether any move of this drag went past the grace zone. */
|
|
37
42
|
dipped: boolean;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
interface Placement {
|
|
46
|
+
readonly ratio: number;
|
|
47
|
+
readonly squeezed: PaneSide | null;
|
|
48
|
+
readonly held: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
function clamp01(value: number): number {
|
|
41
52
|
return Math.min(Math.max(value, 0), 1);
|
|
42
53
|
}
|
|
@@ -107,12 +118,22 @@ export function createSplitHandle(shell: SplitHandleShell): SplitHandle {
|
|
|
107
118
|
announceNow();
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
function
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
function placement(leftPx: number, span: number): Placement {
|
|
122
|
+
const leftPast = MIN_PANE_PX - leftPx;
|
|
123
|
+
const rightPast = MIN_PANE_PX - (span - leftPx);
|
|
124
|
+
if (leftPast > GRACE_PX) {
|
|
125
|
+
return { ratio: clamp01(leftPx / span), squeezed: "left", held: false };
|
|
126
|
+
}
|
|
127
|
+
if (rightPast > GRACE_PX) {
|
|
128
|
+
return { ratio: clamp01(leftPx / span), squeezed: "right", held: false };
|
|
129
|
+
}
|
|
130
|
+
if (leftPast > 0) {
|
|
131
|
+
return { ratio: MIN_PANE_PX / span, squeezed: null, held: true };
|
|
132
|
+
}
|
|
133
|
+
if (rightPast > 0) {
|
|
134
|
+
return { ratio: (span - MIN_PANE_PX) / span, squeezed: null, held: true };
|
|
114
135
|
}
|
|
115
|
-
return
|
|
136
|
+
return { ratio: leftPx / span, squeezed: null, held: false };
|
|
116
137
|
}
|
|
117
138
|
function paintSqueezed(side: PaneSide | null): void {
|
|
118
139
|
for (const s of ["left", "right"] as const) {
|
|
@@ -156,6 +177,7 @@ export function createSplitHandle(shell: SplitHandleShell): SplitHandle {
|
|
|
156
177
|
span,
|
|
157
178
|
moving: false,
|
|
158
179
|
squeezed: null,
|
|
180
|
+
held: false,
|
|
159
181
|
dipped: false,
|
|
160
182
|
};
|
|
161
183
|
el.classList.add("wt-handle-held");
|
|
@@ -177,9 +199,10 @@ export function createSplitHandle(shell: SplitHandleShell): SplitHandle {
|
|
|
177
199
|
}
|
|
178
200
|
d.moving = true;
|
|
179
201
|
}
|
|
180
|
-
const ratio =
|
|
202
|
+
const { ratio, squeezed, held } = placement(d.startLeftPx + dx, d.span);
|
|
181
203
|
split.setRatio(ratio, false);
|
|
182
|
-
d.squeezed =
|
|
204
|
+
d.squeezed = squeezed;
|
|
205
|
+
d.held = held;
|
|
183
206
|
if (d.squeezed !== null) {
|
|
184
207
|
d.dipped = true;
|
|
185
208
|
}
|
|
@@ -200,11 +223,12 @@ export function createSplitHandle(shell: SplitHandleShell): SplitHandle {
|
|
|
200
223
|
if (d.squeezed !== null && split.closeSide(d.squeezed)) {
|
|
201
224
|
return;
|
|
202
225
|
}
|
|
203
|
-
// A drag that
|
|
204
|
-
// above
|
|
205
|
-
// committing a share at or past the threshold.
|
|
226
|
+
// A drag that went past the grace zone was a close gesture, so releasing
|
|
227
|
+
// above the minimum, or a close the split refuses, cancels the drag rather
|
|
228
|
+
// than committing a share at or past the threshold. A release held at the
|
|
229
|
+
// minimum sets it.
|
|
206
230
|
const state = split.state();
|
|
207
|
-
split.setRatio(d.dipped ? state.committedRatio : state.ratio, true);
|
|
231
|
+
split.setRatio(d.dipped && !d.held ? state.committedRatio : state.ratio, true);
|
|
208
232
|
announceNow();
|
|
209
233
|
}
|
|
210
234
|
let nudgePending = false;
|
package/src/kernel/types.ts
CHANGED
|
@@ -199,7 +199,9 @@ export interface SplitController {
|
|
|
199
199
|
isOpen(): boolean;
|
|
200
200
|
/** Enabled, closed, and the pane row is wide enough for two panes. */
|
|
201
201
|
canOpen(): boolean;
|
|
202
|
-
/** True when the split is now open at a 50/50 share; false when nothing changed.
|
|
202
|
+
/** True when the split is now open at a 50/50 share; false when nothing changed.
|
|
203
|
+
* The state is open at once; with `wt-animate` on the shell root the divider
|
|
204
|
+
* slides in from the right edge over `--dur-standard`. */
|
|
203
205
|
open(): boolean;
|
|
204
206
|
/** The selected pane fills the view; false when nothing changed. The state is
|
|
205
207
|
* closed at once; with `wt-animate` on the shell root the grid columns slide
|
|
@@ -303,6 +305,11 @@ export interface ShellContext {
|
|
|
303
305
|
* built pane's connection other than `side` (whose own `clearActiveSession()`
|
|
304
306
|
* already did it), and the scrollback keeper's forget. */
|
|
305
307
|
dropSessionExcept(sessionId: string, side: PaneSide): void;
|
|
308
|
+
/** Call after changing what the panes' term wraps clear at the bottom (the
|
|
309
|
+
* `--wt-tabbar-h` or `--wt-reserve-bottom` set on `root`): every pane re-pins
|
|
310
|
+
* its term wrap and sends its size once it settles. Nothing observes a term
|
|
311
|
+
* wrap, so this call is how such a change reaches the panes. */
|
|
312
|
+
chromeResized(): void;
|
|
306
313
|
/** Bind the page's attention surfaces (the document-title prefix, the app
|
|
307
314
|
* badge, the icon links) and get the reporter that drives them. The terminal
|
|
308
315
|
* is the only writer of `document.title`: a program's OSC 0/2 title replaces
|