diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index c497a38d55..70c4b7a561 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -14332,8 +14332,9 @@ function openGridSnapshot() { {{ t("grid.copyDdl") }} - + + {{ t("settings.wordWrap") }} diff --git a/apps/desktop/src/components/layout/EditorToolbar.vue b/apps/desktop/src/components/layout/EditorToolbar.vue index 00628883e0..84350a7988 100644 --- a/apps/desktop/src/components/layout/EditorToolbar.vue +++ b/apps/desktop/src/components/layout/EditorToolbar.vue @@ -87,6 +87,7 @@ 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; function measureToolbarTier() { @@ -99,9 +100,12 @@ function measureToolbarTier() { availableWidth: element.clientWidth, contentWidth: element.scrollWidth, 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; } toolbarTier.value = next; diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 7a6121de2e..dadfe54902 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -3592,7 +3592,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { - + @@ -3604,8 +3604,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { {{ t("grid.copyDdl") }} - + + {{ t("settings.wordWrap") }} diff --git a/apps/desktop/src/lib/__tests__/tabs/editorToolbarLayout.spec.ts b/apps/desktop/src/lib/__tests__/tabs/editorToolbarLayout.spec.ts index cc73617ef6..5e558e0173 100644 --- a/apps/desktop/src/lib/__tests__/tabs/editorToolbarLayout.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/editorToolbarLayout.spec.ts @@ -22,6 +22,40 @@ describe("editor toolbar measured condensation", () => { expect(resolveNextEditorToolbarTier({ tier: 2, availableWidth: grown, contentWidth: grown - EDITOR_TOOLBAR_STEP_DOWN_SLACK_PX - 10, condensedAtWidth: 400 })).toBe(1); }); + it("restores the fullest measured tier that fits after the pane expands", () => { + expect( + resolveNextEditorToolbarTier({ + tier: 3, + availableWidth: 920, + contentWidth: 920, + condensedAtWidth: 420, + expandedTierRequiredWidths: { 0: 840, 1: 700, 2: 560 }, + }), + ).toBe(0); + + expect( + resolveNextEditorToolbarTier({ + tier: 3, + availableWidth: 760, + contentWidth: 760, + condensedAtWidth: 420, + expandedTierRequiredWidths: { 0: 840, 1: 700, 2: 560 }, + }), + ).toBe(1); + }); + + it("does not use the condensed flex row width to restore a tier that cannot fit", () => { + expect( + resolveNextEditorToolbarTier({ + tier: 2, + availableWidth: 720, + contentWidth: 720, + condensedAtWidth: 420, + expandedTierRequiredWidths: { 0: 840, 1: 700 }, + }), + ).toBe(2); + }); + it("never oscillates: slack alone without pane growth keeps the condensed tier", () => { // The row fits with plenty of slack, but the pane barely grew since the // toolbar condensed at this width — stepping down would overflow again. diff --git a/apps/desktop/src/lib/tabs/editorToolbarLayout.ts b/apps/desktop/src/lib/tabs/editorToolbarLayout.ts index 1c398a9c6a..7b45e3f058 100644 --- a/apps/desktop/src/lib/tabs/editorToolbarLayout.ts +++ b/apps/desktop/src/lib/tabs/editorToolbarLayout.ts @@ -5,9 +5,9 @@ * changes per database type, transaction state, and feature flags, so any * static guess would over- or under-condense. Instead the toolbar measures its * own row: while the content overflows the available width it steps up one - * tier (moving controls into the overflow menu); it only steps back down when - * there is real slack AND the pane has grown meaningfully since the last - * condensation, so a static narrow layout can never oscillate. + * tier (moving controls into the overflow menu). Each expanded tier's measured + * requirement is retained, so the toolbar can restore the fullest tier that + * fits without relying on scrollWidth from the already-condensed flex row. * * - Tier 0: full layout. * - Tier 1: secondary edit/entry actions (compress, keyword case, semantic @@ -36,16 +36,18 @@ export interface EditorToolbarTierInput { contentWidth: number; /** Toolbar width when the current tier was condensed into; anchors step-down hysteresis. */ condensedAtWidth?: number; + /** Required widths captured immediately before each tier was condensed. */ + expandedTierRequiredWidths?: Partial>; } /** * Resolves the next toolbar tier from real measurements. Monotonic per - * measurement — repeated calls after a tier change converge instead of - * oscillating, because stepping down additionally requires the pane to have - * grown by EDITOR_TOOLBAR_STEP_DOWN_MIN_GROWTH_PX since the last step up. + * measurement. Known expanded-tier widths allow a widened pane to restore the + * best fitting tier in one render; the growth-based branch remains as a safe + * fallback for callers without historical measurements. */ export function resolveNextEditorToolbarTier(input: EditorToolbarTierInput): EditorToolbarTier { - const { tier, availableWidth, contentWidth, condensedAtWidth = 0 } = input; + const { tier, availableWidth, contentWidth, condensedAtWidth = 0, expandedTierRequiredWidths } = input; if (!Number.isFinite(availableWidth) || !Number.isFinite(contentWidth) || availableWidth <= 0 || contentWidth <= 0) { // Unmeasured (first paint, hosts without ResizeObserver) keeps the current tier. return tier; @@ -53,6 +55,14 @@ export function resolveNextEditorToolbarTier(input: EditorToolbarTierInput): Edi if (contentWidth > availableWidth && tier < EDITOR_TOOLBAR_MAX_TIER) { return (tier + 1) as EditorToolbarTier; } + if (tier > 0 && expandedTierRequiredWidths) { + for (let candidate = 0; candidate < tier; candidate += 1) { + const requiredWidth = expandedTierRequiredWidths[candidate as EditorToolbarTier]; + if (requiredWidth && availableWidth >= requiredWidth + EDITOR_TOOLBAR_STEP_DOWN_SLACK_PX) { + return candidate as EditorToolbarTier; + } + } + } if (tier > 0 && availableWidth - contentWidth >= EDITOR_TOOLBAR_STEP_DOWN_SLACK_PX && availableWidth - condensedAtWidth >= EDITOR_TOOLBAR_STEP_DOWN_MIN_GROWTH_PX) { return (tier - 1) as EditorToolbarTier; }