Skip to content

Commit 5d11b5f

Browse files
committed
fix new tab animation in terminal
1 parent b6e1e4d commit 5d11b5f

1 file changed

Lines changed: 67 additions & 20 deletions

File tree

src/components/TerminalPanel.vue

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ function scrollTabsBy(delta: number) {
4040
tabsListRef.value?.scrollBy({ left: delta, behavior: 'smooth' })
4141
}
4242
43-
async function scrollActiveTabIntoView() {
43+
function scrollTabsToEnd() {
44+
const el = tabsListRef.value
45+
if (!el) return
46+
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' })
47+
}
48+
49+
async function scrollActiveTabIntoView(alignToEnd = false) {
4450
await nextTick()
4551
const el = tabsListRef.value?.querySelector<HTMLElement>('.tab-active')
46-
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
52+
el?.scrollIntoView({ block: 'nearest', inline: alignToEnd ? 'end' : 'nearest' })
4753
}
4854
4955
const enc = new TextEncoder()
@@ -91,10 +97,12 @@ const tabs = shallowRef<TabState[]>([])
9197
const activeTabId = ref(-1)
9298
let nextTabId = 0
9399
const termElMap = new Map<number, HTMLDivElement>()
100+
const tabWidth = ref(160)
94101
95102
const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) ?? null)
96103
97104
let keybarResizeObserver: ResizeObserver | null = null
105+
let tabsListResizeObserver: ResizeObserver | null = null
98106
let panelResizeObserver: ResizeObserver | null = null
99107
let previousBodyOverflow = ''
100108
let previousHtmlOverflow = ''
@@ -146,6 +154,23 @@ function registerTermEl(id: number, el: unknown) {
146154
}
147155
}
148156
157+
function updateTabsLayout() {
158+
const el = tabsListRef.value
159+
if (!el) return
160+
161+
tabsScrollLeft.value = el.scrollLeft
162+
tabsScrollMax.value = Math.max(0, el.scrollWidth - el.clientWidth)
163+
164+
const availableWidth = el.clientWidth
165+
const count = tabs.value.length
166+
167+
if (count > 1 && availableWidth > 0) {
168+
tabWidth.value = Math.max(80, Math.min(160, Math.floor(availableWidth / count)))
169+
} else {
170+
tabWidth.value = 160
171+
}
172+
}
173+
149174
// The server only echoes a shell ID for the 2nd+ shell; the 1st shell's ID is the session ID.
150175
function effectiveShellId(tab: TabState): string | null {
151176
if (tab.shellId) return tab.shellId
@@ -393,7 +418,8 @@ async function addTab() {
393418
activeTabId.value = tab.id
394419
await nextTick()
395420
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
396-
void scrollActiveTabIntoView()
421+
updateTabsLayout()
422+
scrollTabsToEnd()
397423
await initTab(tab)
398424
}
399425
@@ -443,11 +469,21 @@ const observePanelSize = () => {
443469
panelResizeObserver?.disconnect()
444470
panelResizeObserver = new ResizeObserver(() => {
445471
if (activeTab.value) handleResizeTab(activeTab.value)
446-
updateTabsScroll()
472+
updateTabsLayout()
447473
})
448474
panelResizeObserver.observe(panelRef.value)
449475
}
450476
477+
const observeTabsListSize = () => {
478+
if (!tabsListRef.value || typeof ResizeObserver === 'undefined') return
479+
480+
tabsListResizeObserver?.disconnect()
481+
tabsListResizeObserver = new ResizeObserver(() => {
482+
updateTabsLayout()
483+
})
484+
tabsListResizeObserver.observe(tabsListRef.value)
485+
}
486+
451487
const observeKeybarHeight = () => {
452488
keybarHeight.value = keybarRef.value?.offsetHeight ?? 0
453489
@@ -568,11 +604,13 @@ onMounted(async () => {
568604
window.visualViewport?.addEventListener('scroll', updateKeybarPosition)
569605
await nextTick()
570606
observeKeybarHeight()
607+
observeTabsListSize()
571608
observePanelSize()
609+
updateTabsLayout()
572610
updateKeybarPosition()
573611
574612
await addTab()
575-
requestAnimationFrame(updateTabsScroll)
613+
requestAnimationFrame(updateTabsLayout)
576614
})
577615
578616
onUnmounted(() => {
@@ -582,6 +620,8 @@ onUnmounted(() => {
582620
window.visualViewport?.removeEventListener('scroll', updateKeybarPosition)
583621
keybarResizeObserver?.disconnect()
584622
keybarResizeObserver = null
623+
tabsListResizeObserver?.disconnect()
624+
tabsListResizeObserver = null
585625
panelResizeObserver?.disconnect()
586626
panelResizeObserver = null
587627
clearTerminalTouchScroll()
@@ -597,7 +637,7 @@ watch([terminalInsetBottom, panelViewportHeight], () => {
597637
598638
watch(() => tabs.value.length, async () => {
599639
await nextTick()
600-
requestAnimationFrame(updateTabsScroll)
640+
requestAnimationFrame(updateTabsLayout)
601641
})
602642
603643
// Window focus doesn't move DOM focus, so refocus xterm ourselves.
@@ -616,24 +656,29 @@ watch(() => props.focused, (focused) => {
616656
:disabled="tabsScrollLeft <= 0"
617657
@click="scrollTabsBy(-160)"
618658
><i class="bi bi-chevron-left"></i></button>
619-
<div ref="tabsListRef" class="tabs-list" @scroll="updateTabsScroll">
659+
<div
660+
ref="tabsListRef"
661+
class="tabs-list"
662+
:style="{ '--terminal-tab-width': `${tabWidth}px` }"
663+
@scroll="updateTabsScroll"
664+
>
620665
<button
621666
v-for="tab in tabs"
622667
:key="tab.id"
623668
class="tab-item"
624669
:class="{ 'tab-active': tab.id === activeTabId }"
625670
@mousedown.prevent="switchTab(tab.id, $event)"
626-
>
627-
<span class="tab-label">{{ tab.label }}</span>
628-
<span
629-
class="tab-close"
630-
role="button"
631-
:title="`Close ${tab.label}`"
632-
@click.stop="closeTab(tab.id)"
633-
>&times;</span>
634-
</button>
671+
>
672+
<span class="tab-label">{{ tab.label }}</span>
673+
<span
674+
class="tab-close"
675+
role="button"
676+
:title="`Close ${tab.label}`"
677+
@click.stop="closeTab(tab.id)"
678+
>&times;</span>
679+
</button>
680+
<button class="tab-add" title="New terminal" @click="addTab">+</button>
635681
</div>
636-
<button class="tab-add" title="New terminal" @click="addTab">+</button>
637682
<button
638683
v-if="tabsScrollMax > 0"
639684
class="tab-scroll-btn"
@@ -820,14 +865,14 @@ watch(() => props.focused, (focused) => {
820865
align-items: stretch;
821866
overflow: hidden;
822867
margin: -0.25rem 0 -0.25rem -0.6rem;
823-
flex: 1 1 0;
868+
flex: 1 1 auto;
824869
min-width: 0;
825870
}
826871
827872
.tabs-list {
828873
display: flex;
829874
align-items: stretch;
830-
flex: 0 1 auto;
875+
flex: 1 1 auto;
831876
min-width: 0;
832877
overflow-x: auto;
833878
scrollbar-width: none;
@@ -840,7 +885,9 @@ watch(() => props.focused, (focused) => {
840885
/* GNOME/Yaru style: inactive tabs blend into the bar, the active tab gets
841886
a lighter "pressed in" panel with an accent underline. */
842887
.tab-item {
843-
flex: 0 1 160px;
888+
flex: 0 0 var(--terminal-tab-width, 160px);
889+
width: var(--terminal-tab-width, 160px);
890+
max-width: var(--terminal-tab-width, 160px);
844891
min-width: 80px;
845892
display: flex;
846893
align-items: center;

0 commit comments

Comments
 (0)