diff --git a/apps/desktop/src/components/layout/EditorToolbar.vue b/apps/desktop/src/components/layout/EditorToolbar.vue index 7ba9450aaa..6e73feda78 100644 --- a/apps/desktop/src/components/layout/EditorToolbar.vue +++ b/apps/desktop/src/components/layout/EditorToolbar.vue @@ -84,39 +84,56 @@ const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOpt const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions(); const toolbarRootRef = ref(null); +const toolbarActionsRef = ref(null); const toolbarTier = ref(0); // Available width when the current tier was condensed into; anchors the // step-down hysteresis so a static narrow layout cannot oscillate. const condensedAtWidth = ref(0); const expandedTierRequiredWidths: Partial> = {}; let toolbarResizeObserver: ResizeObserver | undefined; +let toolbarMeasureRaf = 0; function measureToolbarTier() { - const element = toolbarRootRef.value; - if (!element) { + const element = toolbarActionsRef.value; + const root = toolbarRootRef.value; + if (!element || !root) { return; } + const availableWidth = root.clientWidth; + // Use the stable full-row coordinate space, including the right controls. + // Tier-dependent helpers must not appear to grow the pane when they hide. + const contentWidth = element.scrollWidth + availableWidth - element.clientWidth; const next = resolveNextEditorToolbarTier({ tier: toolbarTier.value, - availableWidth: element.clientWidth, - contentWidth: element.scrollWidth, + availableWidth, + contentWidth, condensedAtWidth: condensedAtWidth.value, expandedTierRequiredWidths, }); if (next !== toolbarTier.value) { if (next > toolbarTier.value) { const currentTier = toolbarTier.value; - expandedTierRequiredWidths[currentTier] = Math.max(expandedTierRequiredWidths[currentTier] ?? 0, element.scrollWidth); - condensedAtWidth.value = element.clientWidth; + expandedTierRequiredWidths[currentTier] = Math.max(expandedTierRequiredWidths[currentTier] ?? 0, contentWidth); + condensedAtWidth.value = availableWidth; } toolbarTier.value = next; } } +function scheduleToolbarMeasurement() { + if (toolbarMeasureRaf) { + return; + } + toolbarMeasureRaf = requestAnimationFrame(() => { + toolbarMeasureRaf = 0; + measureToolbarTier(); + }); +} + // Hiding or restoring controls changes the row content without resizing the // toolbar box, so every tier change re-measures until the row settles. watch(toolbarTier, () => { - void nextTick(measureToolbarTier); + void nextTick(scheduleToolbarMeasurement); }); // The visible control set also changes with connection type and transaction @@ -124,7 +141,7 @@ watch(toolbarTier, () => { watch( () => [props.activeConnection?.id, props.activeConnection?.db_type, props.txnSessionId, props.activeTab.isExecuting, props.activeTab.isExplaining] as const, () => { - void nextTick(measureToolbarTier); + void nextTick(scheduleToolbarMeasurement); }, ); @@ -134,15 +151,19 @@ watch( toolbarResizeObserver?.disconnect(); toolbarResizeObserver = undefined; if (element && typeof ResizeObserver !== "undefined") { - toolbarResizeObserver = new ResizeObserver(measureToolbarTier); + toolbarResizeObserver = new ResizeObserver(scheduleToolbarMeasurement); toolbarResizeObserver.observe(element); } - void nextTick(measureToolbarTier); + void nextTick(scheduleToolbarMeasurement); }, { flush: "post" }, ); onUnmounted(() => { + if (toolbarMeasureRaf) { + cancelAnimationFrame(toolbarMeasureRaf); + toolbarMeasureRaf = 0; + } toolbarResizeObserver?.disconnect(); toolbarResizeObserver = undefined; }); @@ -153,7 +174,10 @@ const activeCatalogs = computed(() => { }); const activeCatalogNames = computed(() => activeCatalogs.value.map((catalog) => catalog.name)); const catalogSelectorAvailable = computed(() => connectionIsDorisFamilyCatalogCapable(props.activeConnection) && queryCatalogSelectorVisible(activeCatalogs.value)); -const showCatalogSelector = computed(() => catalogSelectorAvailable.value && toolbarTier.value < 3); +// Keep connection context controls mounted while the action group condenses. +// Removing them changes the action group's available width and can make the +// tier immediately expand again at the boundary, causing visible flicker. +const showCatalogSelector = computed(() => catalogSelectorAvailable.value); const activeCatalogValue = computed(() => selectedQueryCatalogName(activeCatalogs.value, props.activeTab.catalog)); const activeCatalogDatabaseKey = computed(() => (props.activeConnection && props.activeTab.catalog ? catalogDatabaseOptionsKey(props.activeConnection.id, props.activeTab.catalog) : "")); const activeDatabaseOptions = computed(() => { @@ -277,7 +301,7 @@ const schemaSelectorAvailable = computed(() => { const connection = props.activeConnection; return connection && isSchemaAware(connection.id) && (props.activeTab.database || isSingleDb.value || hasDefaultDatabaseOption.value); }); -const showSchemaSelector = computed(() => schemaSelectorAvailable.value && toolbarTier.value < 3); +const showSchemaSelector = computed(() => schemaSelectorAvailable.value); const activeSchemaOptions = computed(() => { const connection = props.activeConnection; @@ -389,8 +413,8 @@ async function changeCatalog(selectedCatalog: string) {