@gjsify/adwaita-web 0.32.0 → 0.34.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/dist/adwaita-web.css +118 -55
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-combo-row.d.ts +10 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -1
- package/lib/types/elements/adw-navigation-split-view.d.ts +42 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +56 -0
- package/lib/types/elements/adw-spinner.d.ts +23 -0
- package/lib/types/elements/adw-view-stack.d.ts +12 -0
- package/lib/types/elements/adw-view-switcher-bar.d.ts +7 -5
- package/lib/types/elements/adw-view-switcher.d.ts +55 -3
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +6 -6
- package/scss/_combo_row.scss +19 -0
- package/scss/_navigation_split_view.scss +52 -9
- package/scss/_navigation_view.scss +28 -4
- package/scss/_overlay_split_view.scss +122 -33
- package/scss/_panes.scss +57 -0
- package/scss/_sidebar.scss +8 -1
- package/scss/_spinner.scss +35 -21
- package/scss/_tab_view.scss +12 -13
- package/scss/_theme.scss +9 -0
- package/scss/_variables.scss +18 -1
- package/scss/_wrap_box.scss +9 -0
- package/scss/adwaita-skin.scss +1 -0
- package/src/adw-about-dialog.spec.ts +36 -0
- package/src/adw-avatar.spec.ts +37 -0
- package/src/adw-button-content.spec.ts +24 -0
- package/src/adw-row-state.spec.ts +49 -0
- package/src/adw-sidebar.spec.ts +32 -0
- package/src/adw-wrap-box.spec.ts +148 -1
- package/src/chrome.spec.ts +111 -12
- package/src/elements/adw-action-row.ts +11 -1
- package/src/elements/adw-combo-row.ts +17 -0
- package/src/elements/adw-inline-view-switcher.ts +91 -8
- package/src/elements/adw-navigation-split-view.ts +189 -14
- package/src/elements/adw-overlay-split-view.ts +395 -20
- package/src/elements/adw-spinner.ts +165 -21
- package/src/elements/adw-tab-view.ts +20 -2
- package/src/elements/adw-view-stack.ts +21 -0
- package/src/elements/adw-view-switcher-bar.ts +13 -5
- package/src/elements/adw-view-switcher.ts +103 -11
- package/src/elements/adw-wrap-box.ts +290 -135
- package/src/split-views.spec.ts +677 -2
- package/src/styles.generated.ts +1 -1
- package/src/view-switcher.spec.ts +219 -0
|
@@ -12,12 +12,64 @@ import {
|
|
|
12
12
|
DEFAULT_MIN_SIDEBAR_WIDTH,
|
|
13
13
|
DEFAULT_SIDEBAR_WIDTH_FRACTION,
|
|
14
14
|
OverlaySplitViewState,
|
|
15
|
+
isSidebarAtVisualStart,
|
|
16
|
+
layoutOverlaySplitView,
|
|
17
|
+
resolveOverlaySidebarWidth,
|
|
15
18
|
resolveSidebarBounds,
|
|
19
|
+
resolveSwipeArea,
|
|
20
|
+
resolveSwipeSnapPoints,
|
|
21
|
+
resolveSwipeStart,
|
|
22
|
+
swipeCancelProgress,
|
|
16
23
|
type AdwPackType,
|
|
24
|
+
type AdwTextDirection,
|
|
25
|
+
type SplitViewAnimator,
|
|
17
26
|
} from '@gjsify/adwaita-core';
|
|
18
27
|
|
|
19
28
|
import { bindBreakpointSetter } from '../breakpoints.js';
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The reveal spring, as a CSS-timed animator.
|
|
32
|
+
*
|
|
33
|
+
* libadwaita animates with a spring `(1, 0.5, 500)`
|
|
34
|
+
* (adw-overlay-split-view.c:1163-1165). A browser cannot be handed spring
|
|
35
|
+
* parameters, so this drives the progress from `requestAnimationFrame` on a
|
|
36
|
+
* critically-damped approximation and lets the element write the resulting
|
|
37
|
+
* geometry — which is the same seam the NativeScript port fills with
|
|
38
|
+
* `View.animate()`, and the same one a test fills with an instant fake.
|
|
39
|
+
*/
|
|
40
|
+
const CSS_SPLIT_VIEW_ANIMATOR: SplitViewAnimator = {
|
|
41
|
+
animate(request) {
|
|
42
|
+
let handle = 0;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
// 250ms is the visible settle time of libadwaita's spring at these
|
|
45
|
+
// parameters; the CURVE is an approximation and says so, the ENDPOINTS
|
|
46
|
+
// are exact because `onDone` writes `to`.
|
|
47
|
+
const duration = 250;
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
const step = (now: number) => {
|
|
50
|
+
if (cancelled) return;
|
|
51
|
+
const t = Math.min((now - start) / duration, 1);
|
|
52
|
+
// ease-out: fast first, settling — the visible signature of a spring
|
|
53
|
+
// with no overshoot, which is what `clamp` asks for anyway.
|
|
54
|
+
const eased = 1 - (1 - t) * (1 - t);
|
|
55
|
+
request.onValue(request.from + (request.to - request.from) * eased);
|
|
56
|
+
if (t < 1) {
|
|
57
|
+
handle = requestAnimationFrame(step);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
request.onValue(request.to);
|
|
61
|
+
request.onDone();
|
|
62
|
+
};
|
|
63
|
+
handle = requestAnimationFrame(step);
|
|
64
|
+
return {
|
|
65
|
+
cancel() {
|
|
66
|
+
cancelled = true;
|
|
67
|
+
if (handle) cancelAnimationFrame(handle);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
21
73
|
/**
|
|
22
74
|
* Read a boolean attribute the way a GTK property with a TRUE default has to be
|
|
23
75
|
* read: absence means the default, not false.
|
|
@@ -48,6 +100,13 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
48
100
|
private _state = new OverlaySplitViewState();
|
|
49
101
|
/** Re-entrancy guard for {@link _reflectShowSidebar}. */
|
|
50
102
|
private _reflecting = false;
|
|
103
|
+
/** The view's own width in CSS px — the size the sidebar fraction is OF. */
|
|
104
|
+
private _measuredWidth = 0;
|
|
105
|
+
private _resizeObserver: ResizeObserver | undefined;
|
|
106
|
+
/** The pointer currently panning the sidebar, if any. */
|
|
107
|
+
private _swipePointer: number | null = null;
|
|
108
|
+
/** Where the pan started, and the progress it started from. */
|
|
109
|
+
private _swipeOrigin = { x: 0, progress: 0 };
|
|
51
110
|
|
|
52
111
|
/**
|
|
53
112
|
* Every attribute {@link _readAttribute} handles must be listed here — an
|
|
@@ -65,6 +124,8 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
65
124
|
'min-sidebar-width',
|
|
66
125
|
'max-sidebar-width',
|
|
67
126
|
'sidebar-width-fraction',
|
|
127
|
+
'enable-show-gesture',
|
|
128
|
+
'enable-hide-gesture',
|
|
68
129
|
'breakpoint',
|
|
69
130
|
];
|
|
70
131
|
}
|
|
@@ -107,6 +168,17 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
107
168
|
}
|
|
108
169
|
|
|
109
170
|
connectedCallback() {
|
|
171
|
+
this._buildOnce();
|
|
172
|
+
// EVERY connect, not only the first — see `_bindToDocument`.
|
|
173
|
+
this._bindToDocument();
|
|
174
|
+
this._reflectShowSidebar();
|
|
175
|
+
this._syncClasses();
|
|
176
|
+
this._syncSidebarWidth();
|
|
177
|
+
this._syncBreakpoint();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The DOM surgery and the state, which happen once per element lifetime. */
|
|
181
|
+
private _buildOnce() {
|
|
110
182
|
if (this._initialized) return;
|
|
111
183
|
this._initialized = true;
|
|
112
184
|
|
|
@@ -150,18 +222,80 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
150
222
|
showSidebar: readTriStateAttr(this, 'show-sidebar', true),
|
|
151
223
|
pinSidebar: this.hasAttribute('pin-sidebar'),
|
|
152
224
|
sidebarPosition: this.getAttribute('sidebar-position') === 'end' ? 'end' : ('start' as AdwPackType),
|
|
225
|
+
enableShowGesture: readTriStateAttr(this, 'enable-show-gesture', true),
|
|
226
|
+
enableHideGesture: readTriStateAttr(this, 'enable-hide-gesture', true),
|
|
227
|
+
animator: CSS_SPLIT_VIEW_ANIMATOR,
|
|
153
228
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
229
|
+
// Every progress step repaints — the reveal is a CONTINUUM now, not two
|
|
230
|
+
// end states, which is what the five OVERLAY_SWIPE_* tables describe.
|
|
231
|
+
this._state.subscribe(() => {
|
|
232
|
+
this._reflectShowSidebar();
|
|
233
|
+
this._syncClasses();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// `escape_shortcut_cb` (adw-overlay-split-view.c:705-716) — absent from
|
|
237
|
+
// BOTH ports, which made `OverlaySplitViewState.escape()` dead code.
|
|
238
|
+
// Bound on the element, so it only fires while focus is inside the view
|
|
239
|
+
// and still propagates when the state declines to consume it.
|
|
240
|
+
//
|
|
241
|
+
// Bound ONCE, here, and never removed: a listener on the element itself
|
|
242
|
+
// survives detaching and is collected with the element, so there is
|
|
243
|
+
// nothing to leak — while re-adding it per connect would stack a second
|
|
244
|
+
// handler on every move. Only the two bindings that reach OUTSIDE the
|
|
245
|
+
// element (the observer below and the breakpoint's media query) need a
|
|
246
|
+
// teardown, and they are re-established on every connect.
|
|
247
|
+
this.addEventListener('keydown', this._onKeyDown);
|
|
248
|
+
this._bindSwipe();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Everything that must be re-established every time the element enters a
|
|
253
|
+
* document — NOT once per lifetime.
|
|
254
|
+
*
|
|
255
|
+
* `connectedCallback` returns early on the second connect (the DOM is
|
|
256
|
+
* already built), so anything torn down in `disconnectedCallback` used to be
|
|
257
|
+
* gone for good: moving the view between parents — which is what a slideshow
|
|
258
|
+
* or a client-side route change does — killed the ResizeObserver and left
|
|
259
|
+
* `_measuredWidth` frozen at whatever it last saw, or at 0 if the view had
|
|
260
|
+
* never been visible. That is the other half of the docs-site regression.
|
|
261
|
+
*/
|
|
262
|
+
private _bindToDocument() {
|
|
263
|
+
// The view's own box is the size source, the same one the breakpoints
|
|
264
|
+
// use: the sidebar width is a FRACTION of it and has to be recomputed
|
|
265
|
+
// when it changes, which a one-shot read at connect could not do.
|
|
266
|
+
this._resizeObserver?.disconnect();
|
|
267
|
+
this._resizeObserver = new ResizeObserver((entries) => {
|
|
268
|
+
const entry = entries[entries.length - 1];
|
|
269
|
+
if (!entry) return;
|
|
270
|
+
const box = entry.borderBoxSize?.[0];
|
|
271
|
+
this._measuredWidth = box ? box.inlineSize : entry.contentRect.width;
|
|
272
|
+
this._syncSidebarWidth();
|
|
273
|
+
this._syncClasses();
|
|
274
|
+
});
|
|
275
|
+
this._resizeObserver.observe(this);
|
|
276
|
+
// Seed synchronously so the FIRST paint after a connect is already
|
|
277
|
+
// placed. The observer's initial callback is a frame away, and on a
|
|
278
|
+
// re-connect there may be no size change at all for it to report.
|
|
279
|
+
// Zero is the honest answer while the view is in a `display: none`
|
|
280
|
+
// subtree, and `_syncGeometry` knows what to do with it.
|
|
281
|
+
this._measuredWidth = this.offsetWidth;
|
|
158
282
|
}
|
|
159
283
|
|
|
160
284
|
disconnectedCallback() {
|
|
161
285
|
this._disposeBreakpoint?.();
|
|
162
286
|
this._disposeBreakpoint = undefined;
|
|
287
|
+
this._resizeObserver?.disconnect();
|
|
288
|
+
this._resizeObserver = undefined;
|
|
163
289
|
}
|
|
164
290
|
|
|
291
|
+
/** `escape_shortcut_cb` — only a COLLAPSED view with a revealed sidebar consumes it. */
|
|
292
|
+
private readonly _onKeyDown = (event: KeyboardEvent) => {
|
|
293
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
294
|
+
// The state decides: an uncollapsed view lets Escape through so an
|
|
295
|
+
// enclosing dialog still closes.
|
|
296
|
+
if (this._state.escape()) event.preventDefault();
|
|
297
|
+
};
|
|
298
|
+
|
|
165
299
|
attributeChangedCallback(_name: string, _old: string | null, _val: string | null) {
|
|
166
300
|
if (!this._initialized) return;
|
|
167
301
|
if (_name === 'breakpoint') {
|
|
@@ -169,10 +303,20 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
169
303
|
return;
|
|
170
304
|
}
|
|
171
305
|
this._readAttribute(_name);
|
|
172
|
-
|
|
173
|
-
|
|
306
|
+
// `collapsed` is in the list because `get_sidebar_width` IGNORES the
|
|
307
|
+
// fraction while collapsed and clamps the VIEW width instead
|
|
308
|
+
// (adw-overlay-split-view.c:462-463) — a collapsed sidebar is a
|
|
309
|
+
// different number, not the same one drawn differently. Leaving it out
|
|
310
|
+
// is the frame-late bug the NativeScript port paid for.
|
|
311
|
+
if (
|
|
312
|
+
_name === 'min-sidebar-width' ||
|
|
313
|
+
_name === 'max-sidebar-width' ||
|
|
314
|
+
_name === 'sidebar-width-fraction' ||
|
|
315
|
+
_name === 'collapsed'
|
|
316
|
+
) {
|
|
174
317
|
this._syncSidebarWidth();
|
|
175
318
|
}
|
|
319
|
+
this._syncClasses();
|
|
176
320
|
}
|
|
177
321
|
|
|
178
322
|
/**
|
|
@@ -197,6 +341,14 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
197
341
|
if (name === 'show-sidebar') {
|
|
198
342
|
state.setShowSidebar(readTriStateAttr(this, 'show-sidebar', true));
|
|
199
343
|
}
|
|
344
|
+
// Both default TRUE (adw-overlay-split-view.c:1178-1210), so they read
|
|
345
|
+
// through the tri-state helper for the same reason `show-sidebar` does.
|
|
346
|
+
if (name === 'enable-show-gesture') {
|
|
347
|
+
state.setEnableShowGesture(readTriStateAttr(this, 'enable-show-gesture', true));
|
|
348
|
+
}
|
|
349
|
+
if (name === 'enable-hide-gesture') {
|
|
350
|
+
state.setEnableHideGesture(readTriStateAttr(this, 'enable-hide-gesture', true));
|
|
351
|
+
}
|
|
200
352
|
// Last: it can change `show-sidebar`, so it must not be overwritten after.
|
|
201
353
|
if (name === 'collapsed') state.setCollapsed(this.hasAttribute('collapsed'));
|
|
202
354
|
this._reflectShowSidebar();
|
|
@@ -248,12 +400,64 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
248
400
|
);
|
|
249
401
|
}
|
|
250
402
|
|
|
403
|
+
/**
|
|
404
|
+
* The reading direction `start` / `end` are resolved against.
|
|
405
|
+
*
|
|
406
|
+
* `get_start_or_end` (adw-overlay-split-view.c:227-234) returns
|
|
407
|
+
* `GTK_PACK_END` under RTL, so a `start` sidebar is drawn on the RIGHT.
|
|
408
|
+
* Neither renderer mirrored any of it; the browser has the direction for
|
|
409
|
+
* free and only had to read it.
|
|
410
|
+
*/
|
|
411
|
+
private get _direction(): AdwTextDirection {
|
|
412
|
+
return getComputedStyle(this).direction === 'rtl' ? 'rtl' : 'ltr';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The sizing properties as the core takes them. */
|
|
416
|
+
private get _widthSpec() {
|
|
417
|
+
return {
|
|
418
|
+
minSidebarWidth: this.minSidebarWidth,
|
|
419
|
+
maxSidebarWidth: this.maxSidebarWidth,
|
|
420
|
+
sidebarWidthFraction: this.sidebarWidthFraction,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The sidebar's allocated width in px — `get_sidebar_width` (:441-466). */
|
|
425
|
+
private get _sidebarWidth(): number {
|
|
426
|
+
return resolveOverlaySidebarWidth({
|
|
427
|
+
...this._widthSpec,
|
|
428
|
+
totalWidth: this._measuredWidth,
|
|
429
|
+
collapsed: this._state.collapsed,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
251
433
|
private _syncClasses() {
|
|
252
434
|
const state = this._state;
|
|
253
435
|
this.classList.toggle('collapsed', state.collapsed);
|
|
254
436
|
this.classList.toggle('show-sidebar', state.showSidebar);
|
|
437
|
+
const atStart = isSidebarAtVisualStart(state.sidebarPosition, this._direction);
|
|
255
438
|
this.classList.toggle('sidebar-start', state.sidebarPosition === 'start');
|
|
256
439
|
this.classList.toggle('sidebar-end', state.sidebarPosition === 'end');
|
|
440
|
+
// The VISUAL side, which under RTL is the opposite of the logical one.
|
|
441
|
+
// The stylesheet needs it for the divider edge, which `border-inline-*`
|
|
442
|
+
// alone cannot get right: the divider follows where the pane is DRAWN.
|
|
443
|
+
this.classList.toggle('sidebar-at-visual-start', atStart);
|
|
444
|
+
|
|
445
|
+
// THE PANE STYLE CLASSES, which libadwaita documents as part of this
|
|
446
|
+
// widget's CSS node tree (adw-overlay-split-view.c:138-152) and switches
|
|
447
|
+
// in `update_collapsed` (:718-741): docked, the panes are `.sidebar-pane`
|
|
448
|
+
// and `.content-pane`; collapsed, the sidebar becomes an `.overlay-pane`
|
|
449
|
+
// floating above a content pane that carries no class at all.
|
|
450
|
+
//
|
|
451
|
+
// They are not decoration. `.overlay-pane` is what makes a collapsed
|
|
452
|
+
// sidebar drop the sidebar background for the window one, and they are
|
|
453
|
+
// the documented hook an app themes its own panes through — so leaving
|
|
454
|
+
// them off does not merely look wrong, it makes correct app CSS a no-op.
|
|
455
|
+
// The docs site is the proof: its split-view examples carried a
|
|
456
|
+
// hand-written `class="sidebar"` that styled nothing, written by someone
|
|
457
|
+
// reaching for exactly this and finding nothing there.
|
|
458
|
+
this._sidebarEl?.classList.toggle('sidebar-pane', !state.collapsed);
|
|
459
|
+
this._sidebarEl?.classList.toggle('overlay-pane', state.collapsed);
|
|
460
|
+
this._contentEl?.classList.toggle('content-pane', !state.collapsed);
|
|
257
461
|
|
|
258
462
|
// Derived, not re-decided here: the shield only exists while collapsed
|
|
259
463
|
// AND revealed, and each pane is reachable by keyboard only when it is
|
|
@@ -262,16 +466,90 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
262
466
|
this._sidebarEl?.setAttribute('aria-hidden', String(!state.sidebarFocusable));
|
|
263
467
|
this._contentEl?.setAttribute('aria-hidden', String(!state.contentFocusable));
|
|
264
468
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
469
|
+
this._syncGeometry();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Place the sidebar for the current progress — `allocate_uncollapsed`
|
|
474
|
+
* (:572-610) and `allocate_collapsed` (:653-703), through the core.
|
|
475
|
+
*
|
|
476
|
+
* The slide used to be `marginRight: -offsetWidth` REGARDLESS of which edge
|
|
477
|
+
* the sidebar is on, so an `end` sidebar slid the wrong way; and it was only
|
|
478
|
+
* two states, so a swipe had nothing to drive. `layoutOverlaySplitView`
|
|
479
|
+
* returns the pane rect for any progress including the overshoot, which is
|
|
480
|
+
* what makes a continuous gesture expressible at all.
|
|
481
|
+
*/
|
|
482
|
+
private _syncGeometry() {
|
|
483
|
+
const sidebar = this._sidebarEl;
|
|
484
|
+
if (!sidebar) return;
|
|
485
|
+
// NOTHING MEASURED YET — HAND THE PLACEMENT BACK TO THE STYLESHEET.
|
|
486
|
+
//
|
|
487
|
+
// This used to `return` and leave the inline styles alone, which is the
|
|
488
|
+
// one thing it must not do. The stylesheet no longer carries a
|
|
489
|
+
// `transform: translateX(±100%)` for the hidden end state (that is the
|
|
490
|
+
// point of driving the offset per frame), so an absolutely-positioned
|
|
491
|
+
// pane with no `left` resolves to `left: 0` at full opacity: a HIDDEN
|
|
492
|
+
// sidebar painted over the content at the wrong edge. Shipped that way
|
|
493
|
+
// in v0.32.0's `feat(adwaita)` commit and visible on the docs site,
|
|
494
|
+
// whose slideshow builds each slide hidden and moves it in — so the
|
|
495
|
+
// first measurement never arrived and the fallback was all there was.
|
|
496
|
+
//
|
|
497
|
+
// Clearing rather than skipping matters just as much: a STALE inline
|
|
498
|
+
// `left` from an earlier measurement outranks the resting rule (an
|
|
499
|
+
// absolutely-positioned box with `left` + `width` ignores `right`), so
|
|
500
|
+
// leaving the old value behind would defeat the stylesheet exactly when
|
|
501
|
+
// it is needed. `_resting.scss` carries the four end states.
|
|
502
|
+
if (this._measuredWidth <= 0) {
|
|
503
|
+
for (const prop of [
|
|
504
|
+
'left',
|
|
505
|
+
'marginLeft',
|
|
506
|
+
'marginRight',
|
|
507
|
+
'width',
|
|
508
|
+
'minWidth',
|
|
509
|
+
'maxWidth',
|
|
510
|
+
'opacity',
|
|
511
|
+
'pointerEvents',
|
|
512
|
+
] as const) {
|
|
513
|
+
sidebar.style[prop] = '';
|
|
273
514
|
}
|
|
515
|
+
if (this._backdropEl) this._backdropEl.style.opacity = '';
|
|
516
|
+
return;
|
|
274
517
|
}
|
|
518
|
+
const state = this._state;
|
|
519
|
+
const measured = this._sidebarWidth;
|
|
520
|
+
const layout = layoutOverlaySplitView({
|
|
521
|
+
totalWidth: this._measuredWidth,
|
|
522
|
+
sidebarWidth: measured,
|
|
523
|
+
showProgress: state.showProgress,
|
|
524
|
+
collapsed: state.collapsed,
|
|
525
|
+
sidebarPosition: state.sidebarPosition,
|
|
526
|
+
direction: this._direction,
|
|
527
|
+
});
|
|
528
|
+
const atStart = isSidebarAtVisualStart(state.sidebarPosition, this._direction);
|
|
529
|
+
|
|
530
|
+
sidebar.style.width = `${layout.sidebar.width}px`;
|
|
531
|
+
if (state.collapsed) {
|
|
532
|
+
// Floating above the content: absolutely placed at the rect the core
|
|
533
|
+
// returned, so an overshoot widens the pane instead of detaching it.
|
|
534
|
+
sidebar.style.marginLeft = '';
|
|
535
|
+
sidebar.style.marginRight = '';
|
|
536
|
+
sidebar.style.insetInlineStart = '';
|
|
537
|
+
sidebar.style.left = `${layout.sidebar.x}px`;
|
|
538
|
+
} else {
|
|
539
|
+
// Docked: the pane keeps its place in the flex row and the hidden
|
|
540
|
+
// part is taken off the edge it is ON — the left one at visual
|
|
541
|
+
// start, the right one at visual end.
|
|
542
|
+
sidebar.style.left = '';
|
|
543
|
+
const hidden = measured - Math.trunc(measured * Math.min(state.showProgress, 1));
|
|
544
|
+
sidebar.style.marginLeft = atStart ? `${-hidden}px` : '';
|
|
545
|
+
sidebar.style.marginRight = atStart ? '' : `${-hidden}px`;
|
|
546
|
+
}
|
|
547
|
+
sidebar.style.opacity = state.showProgress <= 0 ? '0' : '1';
|
|
548
|
+
// `sidebarPainted` is the snapshot gate (:757): below zero progress there
|
|
549
|
+
// is nothing on screen, and a pane that is not painted must not take
|
|
550
|
+
// pointer events either.
|
|
551
|
+
sidebar.style.pointerEvents = state.sidebarPainted ? '' : 'none';
|
|
552
|
+
if (this._backdropEl) this._backdropEl.style.opacity = String(1 - layout.shadowProgress);
|
|
275
553
|
}
|
|
276
554
|
|
|
277
555
|
private _syncSidebarWidth() {
|
|
@@ -280,14 +558,111 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
280
558
|
// libadwaita never lets `max` fall below `min`, and CSS resolves that
|
|
281
559
|
// conflict the other way round (min-width wins over max-width), so an
|
|
282
560
|
// inverted pair would render differently here than in GTK.
|
|
283
|
-
const bounds = resolveSidebarBounds(
|
|
284
|
-
{ minSidebarWidth: this.minSidebarWidth, maxSidebarWidth: this.maxSidebarWidth },
|
|
285
|
-
0,
|
|
286
|
-
{ ceil: true },
|
|
287
|
-
);
|
|
561
|
+
const bounds = resolveSidebarBounds(this._widthSpec, 0, { ceil: true });
|
|
288
562
|
this._sidebarEl.style.minWidth = `${bounds.min}px`;
|
|
289
563
|
this._sidebarEl.style.maxWidth = `${bounds.max}px`;
|
|
290
|
-
|
|
564
|
+
// NOT a CSS percentage. `(int)` truncates toward zero, and a percentage
|
|
565
|
+
// is fractional — core's own header forbids it in as many words, and
|
|
566
|
+
// this element carried one for its whole life.
|
|
567
|
+
this._syncGeometry();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// --- swipe (Adw.Swipeable) ---
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The pan gesture the five `OVERLAY_SWIPE_*` tables describe.
|
|
574
|
+
*
|
|
575
|
+
* There was no gesture code in either renderer — grep for `pan` in both
|
|
576
|
+
* split views returned zero — so `showProgress` was only ever 0 or 1 and the
|
|
577
|
+
* tables shipped unexercised. Pointer events give the browser the whole
|
|
578
|
+
* gesture in three handlers; every DECISION in them is core's.
|
|
579
|
+
*/
|
|
580
|
+
private _bindSwipe() {
|
|
581
|
+
this.addEventListener('pointerdown', (event) => {
|
|
582
|
+
if (this._swipePointer !== null || event.button !== 0) return;
|
|
583
|
+
const state = this._state;
|
|
584
|
+
const rect = this.getBoundingClientRect();
|
|
585
|
+
const area = resolveSwipeArea({
|
|
586
|
+
isDrag: true,
|
|
587
|
+
sidebarWidth: this._sidebarWidth,
|
|
588
|
+
showProgress: state.showProgress,
|
|
589
|
+
totalWidth: rect.width,
|
|
590
|
+
totalHeight: rect.height,
|
|
591
|
+
sidebarPosition: state.sidebarPosition,
|
|
592
|
+
direction: this._direction,
|
|
593
|
+
});
|
|
594
|
+
const x = event.clientX - rect.left;
|
|
595
|
+
const y = event.clientY - rect.top;
|
|
596
|
+
if (x < area.x || x > area.x + area.width || y < area.y || y > area.y + area.height) return;
|
|
597
|
+
this._swipePointer = event.pointerId;
|
|
598
|
+
this._swipeOrigin = { x: event.clientX, progress: state.showProgress };
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
this.addEventListener('pointermove', (event) => {
|
|
602
|
+
if (event.pointerId !== this._swipePointer) return;
|
|
603
|
+
const state = this._state;
|
|
604
|
+
const width = this._sidebarWidth || 1;
|
|
605
|
+
const atStart = isSidebarAtVisualStart(state.sidebarPosition, this._direction);
|
|
606
|
+
// Dragging AWAY from the sidebar's edge opens it, so the sign of the
|
|
607
|
+
// travel depends on which edge that is — the same `atStart` predicate
|
|
608
|
+
// the layout uses.
|
|
609
|
+
const travel = ((event.clientX - this._swipeOrigin.x) / width) * (atStart ? 1 : -1);
|
|
610
|
+
if (!state.swipeActive) {
|
|
611
|
+
if (
|
|
612
|
+
!resolveSwipeStart({
|
|
613
|
+
showProgress: state.showProgress,
|
|
614
|
+
collapsed: state.collapsed,
|
|
615
|
+
direction: travel >= 0 ? 'forward' : 'back',
|
|
616
|
+
enableShowGesture: state.enableShowGesture,
|
|
617
|
+
enableHideGesture: state.enableHideGesture,
|
|
618
|
+
})
|
|
619
|
+
) {
|
|
620
|
+
this._swipePointer = null;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
state.beginSwipe();
|
|
624
|
+
// Capture keeps the move/up events coming once the pointer
|
|
625
|
+
// leaves the element — an optimisation, not a precondition. It
|
|
626
|
+
// THROWS for a pointer id the browser has no active pointer for,
|
|
627
|
+
// which Firefox does strictly and Chromium tolerates: a synthetic
|
|
628
|
+
// event, or one whose pointer was already released, aborted the
|
|
629
|
+
// whole gesture before the progress was ever written. The swipe
|
|
630
|
+
// still works without it, so the failure is swallowed on purpose.
|
|
631
|
+
try {
|
|
632
|
+
this.setPointerCapture(event.pointerId);
|
|
633
|
+
} catch {
|
|
634
|
+
/* no active pointer for this id — the gesture does not need capture */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
state.setShowProgress(this._swipeOrigin.progress + travel);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const release = (event: PointerEvent, cancelled: boolean) => {
|
|
641
|
+
if (event.pointerId !== this._swipePointer) return;
|
|
642
|
+
this._swipePointer = null;
|
|
643
|
+
const state = this._state;
|
|
644
|
+
if (!state.swipeActive) return;
|
|
645
|
+
// Same asymmetry on the way out: `hasPointerCapture` is false when the
|
|
646
|
+
// capture above was refused, so the release is already conditional.
|
|
647
|
+
if (this.hasPointerCapture(event.pointerId)) this.releasePointerCapture(event.pointerId);
|
|
648
|
+
// A cancelled gesture snaps back to where it came from
|
|
649
|
+
// (`get_cancel_progress`, :1253-1258); a released one settles on the
|
|
650
|
+
// NEAREST allowed snap point.
|
|
651
|
+
const points = resolveSwipeSnapPoints({
|
|
652
|
+
showProgress: state.showProgress,
|
|
653
|
+
enableShowGesture: state.enableShowGesture,
|
|
654
|
+
enableHideGesture: state.enableHideGesture,
|
|
655
|
+
swipeActive: true,
|
|
656
|
+
});
|
|
657
|
+
const target = cancelled
|
|
658
|
+
? swipeCancelProgress(state.showProgress)
|
|
659
|
+
: points.reduce((best, point) =>
|
|
660
|
+
Math.abs(point - state.showProgress) < Math.abs(best - state.showProgress) ? point : best,
|
|
661
|
+
);
|
|
662
|
+
state.endSwipe(target);
|
|
663
|
+
};
|
|
664
|
+
this.addEventListener('pointerup', (event) => release(event, false));
|
|
665
|
+
this.addEventListener('pointercancel', (event) => release(event, true));
|
|
291
666
|
}
|
|
292
667
|
}
|
|
293
668
|
|