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 } from '@core/platform';
import { RpcService } from '@core/rpc.service';
import { ShortcutsService } from '@core/shortcuts.service';
import { UpdaterService } from '@core/updater.service';
@Component({
selector: 'app-tab-bar',
imports: [NgIcon],
template: `
@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 shortcuts = inject(ShortcutsService);
readonly isMacChrome = IS_MAC_CHROME;
// 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 {
this.updater.advance();
}
/** 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);
}
}