Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/browser/mobileNavigationStack.ts
13388 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Disposable } from '../../base/common/lifecycle.js';
7
import { Emitter, Event } from '../../base/common/event.js';
8
import { mainWindow } from '../../base/browser/window.js';
9
10
export type MobileNavigationLayer = 'sidebar' | 'editor' | 'panel' | 'auxbar';
11
12
interface MobileNavigationEntry {
13
readonly layer: MobileNavigationLayer;
14
readonly id: number;
15
}
16
17
/**
18
* Manages a stack of open overlay layers (sidebar, editor modal, panel sheet,
19
* aux bar) and integrates with `history.pushState` / `popstate` so that the
20
* Android back button dismisses overlays in LIFO order.
21
*/
22
export class MobileNavigationStack extends Disposable {
23
24
private readonly _stack: MobileNavigationEntry[] = [];
25
private _nextId = 0;
26
27
private readonly _onDidPop = this._register(new Emitter<MobileNavigationLayer>());
28
readonly onDidPop: Event<MobileNavigationLayer> = this._onDidPop.event;
29
30
constructor() {
31
super();
32
33
this._register(Event.fromDOMEventEmitter<PopStateEvent>(mainWindow, 'popstate')(e => {
34
this._onPopState(e);
35
}));
36
}
37
38
push(layer: MobileNavigationLayer): void {
39
const id = this._nextId++;
40
this._stack.push({ layer, id });
41
mainWindow.history.pushState({ layer, id }, '');
42
}
43
44
pop(): MobileNavigationLayer | undefined {
45
const entry = this._stack.pop();
46
if (entry) {
47
this._onDidPop.fire(entry.layer);
48
}
49
return entry?.layer;
50
}
51
52
peek(): MobileNavigationLayer | undefined {
53
return this._stack.length > 0
54
? this._stack[this._stack.length - 1].layer
55
: undefined;
56
}
57
58
has(layer: MobileNavigationLayer): boolean {
59
return this._stack.some(e => e.layer === layer);
60
}
61
62
clear(): void {
63
this._stack.length = 0;
64
}
65
66
/**
67
* Removes the topmost entry matching `layer` from the stack (without
68
* firing {@link onDidPop}) and rewinds the browser history by one entry.
69
* Use this when a layer is closed by UI interaction (e.g., backdrop click)
70
* so the history and stack stay in sync without recursing back into
71
* close handlers.
72
*
73
* Concurrent silent pops are handled via a counter: each call increments
74
* {@link _pendingSilentPops} and the matching {@link _onPopState} decrements
75
* it, so rapid back-button taps or multiple overlay closes cannot leak
76
* suppression state across unrelated pops.
77
*/
78
popSilently(layer: MobileNavigationLayer): void {
79
for (let i = this._stack.length - 1; i >= 0; i--) {
80
if (this._stack[i].layer === layer) {
81
this._stack.splice(i, 1);
82
this._pendingSilentPops++;
83
mainWindow.history.back();
84
return;
85
}
86
}
87
}
88
89
private _pendingSilentPops = 0;
90
91
private _onPopState(e: PopStateEvent): void {
92
if (this._pendingSilentPops > 0) {
93
this._pendingSilentPops--;
94
return;
95
}
96
97
if (this._stack.length === 0) {
98
return;
99
}
100
101
const top = this._stack[this._stack.length - 1];
102
const state = e.state as { layer?: string; id?: number } | null;
103
104
// Only pop if the event's state id matches expectations —
105
// the popstate must correspond to a state *before* our top entry,
106
// meaning the top entry's push was just undone.
107
if (state && typeof state.id === 'number' && state.id >= top.id) {
108
return;
109
}
110
111
this.pop();
112
}
113
}
114
115