Path: blob/main/src/vs/sessions/browser/mobileNavigationStack.ts
13388 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../base/common/lifecycle.js';6import { Emitter, Event } from '../../base/common/event.js';7import { mainWindow } from '../../base/browser/window.js';89export type MobileNavigationLayer = 'sidebar' | 'editor' | 'panel' | 'auxbar';1011interface MobileNavigationEntry {12readonly layer: MobileNavigationLayer;13readonly id: number;14}1516/**17* Manages a stack of open overlay layers (sidebar, editor modal, panel sheet,18* aux bar) and integrates with `history.pushState` / `popstate` so that the19* Android back button dismisses overlays in LIFO order.20*/21export class MobileNavigationStack extends Disposable {2223private readonly _stack: MobileNavigationEntry[] = [];24private _nextId = 0;2526private readonly _onDidPop = this._register(new Emitter<MobileNavigationLayer>());27readonly onDidPop: Event<MobileNavigationLayer> = this._onDidPop.event;2829constructor() {30super();3132this._register(Event.fromDOMEventEmitter<PopStateEvent>(mainWindow, 'popstate')(e => {33this._onPopState(e);34}));35}3637push(layer: MobileNavigationLayer): void {38const id = this._nextId++;39this._stack.push({ layer, id });40mainWindow.history.pushState({ layer, id }, '');41}4243pop(): MobileNavigationLayer | undefined {44const entry = this._stack.pop();45if (entry) {46this._onDidPop.fire(entry.layer);47}48return entry?.layer;49}5051peek(): MobileNavigationLayer | undefined {52return this._stack.length > 053? this._stack[this._stack.length - 1].layer54: undefined;55}5657has(layer: MobileNavigationLayer): boolean {58return this._stack.some(e => e.layer === layer);59}6061clear(): void {62this._stack.length = 0;63}6465/**66* Removes the topmost entry matching `layer` from the stack (without67* firing {@link onDidPop}) and rewinds the browser history by one entry.68* Use this when a layer is closed by UI interaction (e.g., backdrop click)69* so the history and stack stay in sync without recursing back into70* close handlers.71*72* Concurrent silent pops are handled via a counter: each call increments73* {@link _pendingSilentPops} and the matching {@link _onPopState} decrements74* it, so rapid back-button taps or multiple overlay closes cannot leak75* suppression state across unrelated pops.76*/77popSilently(layer: MobileNavigationLayer): void {78for (let i = this._stack.length - 1; i >= 0; i--) {79if (this._stack[i].layer === layer) {80this._stack.splice(i, 1);81this._pendingSilentPops++;82mainWindow.history.back();83return;84}85}86}8788private _pendingSilentPops = 0;8990private _onPopState(e: PopStateEvent): void {91if (this._pendingSilentPops > 0) {92this._pendingSilentPops--;93return;94}9596if (this._stack.length === 0) {97return;98}99100const top = this._stack[this._stack.length - 1];101const state = e.state as { layer?: string; id?: number } | null;102103// Only pop if the event's state id matches expectations —104// the popstate must correspond to a state *before* our top entry,105// meaning the top entry's push was just undone.106if (state && typeof state.id === 'number' && state.id >= top.id) {107return;108}109110this.pop();111}112}113114115