import { Component, ElementRef, effect, inject, signal, viewChild } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; import { ContextMenuService } from '@core/context-menu.service'; import { I18nService } from '@core/i18n.service'; import { PanelsService, type Workspace } from '@core/panels.service'; import { IS_MAC_CHROME, MOD_LABEL } from '@core/platform'; import { RpcService } from '@core/rpc.service'; import { UpdaterService } from '@core/updater.service'; @Component({ selector: 'app-tab-bar', imports: [NgIcon], template: `
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
@if (panels.workspaces().length > 1) { } @else { } @if (ws.id === panels.activeWorkspaceId()) {
}
}
@if (updater.state() !== 'idle' && updater.state() !== 'error') { }
`, }) export class TabBar { readonly panels = inject(PanelsService); readonly i18n = inject(I18nService); readonly updater = inject(UpdaterService); private readonly contextMenu = inject(ContextMenuService); private readonly rpc = inject(RpcService); readonly isMacChrome = IS_MAC_CHROME; readonly modLabel = MOD_LABEL; // drag & drop reordering readonly draggedId = signal(null); readonly dropTarget = signal<{ index: number; before: boolean } | null>(null); private readonly tabScroll = viewChild.required>('tabScroll'); constructor() { // keep the active tab visible when the strip overflows effect(() => { const id = this.panels.activeWorkspaceId(); this.tabScroll() .nativeElement.querySelector(`[data-tab-id="${id}"]`) ?.scrollIntoView({ inline: 'nearest', block: 'nearest' }); }); } close(event: MouseEvent, id: number): void { event.stopPropagation(); this.panels.closeWorkspace(id); } /** * Middle-click closes the tab, as in browsers. `auxclick` also fires for the * right button, hence the button check; the last tab is refused by * closeWorkspace itself. */ onAuxClick(event: MouseEvent, id: number): void { if (event.button !== 1) return; event.preventDefault(); this.close(event, id); } /** A middle press over the scrollable strip would start WebView2's autoscroll. */ onTabMouseDown(event: MouseEvent): void { if (event.button === 1) event.preventDefault(); } /** * Right-click a tab. The menu targets THIS tab, not the active one — its * actions all carry the id — so right-clicking a background tab does not * yank focus to it first, the way Explorer and every browser behave. */ onTabContextMenu(event: MouseEvent, workspace: Workspace, index: number): void { event.preventDefault(); this.contextMenu.open( event, this.contextMenu.tabItems({ id: workspace.id, index, tabCount: this.panels.workspaces().length, isRemote: this.panels.workspaceIsRemote(workspace), }), ); } openManager(): void { void this.rpc.request.openConnectionManager({}); } openSettings(): void { void this.rpc.request.openSettings({}); } /** Update pill: first click downloads, second click (when ready) applies + relaunches. */ onUpdateClick(): void { const state = this.updater.state(); if (state === 'available') void this.updater.download(); else if (state === 'ready') this.updater.apply(); } /** Insertion line at the drop position (inset shadow: no layout shift). */ dropIndicatorShadow(index: number): string | null { const target = this.dropTarget(); if (!target || target.index !== index || this.draggedId() === null) return null; return target.before ? 'inset 2px 0 0 var(--primary)' : 'inset -2px 0 0 var(--primary)'; } onDragStart(event: DragEvent, id: number): void { this.draggedId.set(id); // WebKit refuses to start a drag without data event.dataTransfer?.setData('text/plain', String(id)); if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; } onDragOver(event: DragEvent, index: number): void { if (this.draggedId() === null) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; const el = event.currentTarget as HTMLElement; const before = event.clientX < el.getBoundingClientRect().left + el.offsetWidth / 2; this.dropTarget.set({ index, before }); } onDrop(event: DragEvent, index: number): void { event.preventDefault(); const id = this.draggedId(); const target = this.dropTarget(); this.onDragEnd(); if (id === null || !target) return; const list = this.panels.workspaces(); const fromIndex = list.findIndex((w) => w.id === id); if (fromIndex < 0) return; let toIndex = target.before ? index : index + 1; if (fromIndex < toIndex) toIndex--; // account for removal before insert if (toIndex !== fromIndex) this.panels.moveWorkspace(id, toIndex); } onDragEnd(): void { this.draggedId.set(null); this.dropTarget.set(null); } }