Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
resolveBranchTriggerLabel,
resolveBranchToolbarPrBranch,
resolveBranchToolbarValue,
resolveContextStripLabelHiddenWidth,
resolveContextStripLabelsCompact,
resolveLockedWorkspaceLabel,
resolveLocalCheckoutBranchMismatch,
resolvePreviousWorktreeLabel,
Expand Down Expand Up @@ -834,3 +836,68 @@ describe("sanitizeNewRefName", () => {
expect(sanitizeNewRefName("foo--bar")).toBe("foo--bar");
});
});

describe("resolveContextStripLabelHiddenWidth", () => {
it("reserves the whole text while compact and nothing once expanded", () => {
expect(
resolveContextStripLabelHiddenWidth({ leafWidths: [120], maxWidth: 240, visibleWidth: 0 }),
).toBe(120);
expect(
resolveContextStripLabelHiddenWidth({ leafWidths: [120], maxWidth: 240, visibleWidth: 120 }),
).toBe(0);
});

it("counts a middle-truncated head and tail together, capped at the expanded width", () => {
expect(
resolveContextStripLabelHiddenWidth({
leafWidths: [100, 60],
maxWidth: 240,
visibleWidth: 0,
}),
).toBe(160);
expect(
resolveContextStripLabelHiddenWidth({
leafWidths: [200, 60],
maxWidth: 240,
visibleWidth: 0,
}),
).toBe(240);
expect(
resolveContextStripLabelHiddenWidth({
leafWidths: [200, 60],
maxWidth: 240,
visibleWidth: 240,
}),
).toBe(0);
});

it("settles the compact decision in one pass at every strip width", () => {
// Compact and expanded passes disagreeing by more than the hysteresis
// flip forever until React throws error #185 (#12891).
const controlsWidth = 300;
const label = { leafWidths: [200, 60], maxWidth: 240 };
const needed = (compact: boolean) => {
const visibleWidth = compact ? 0 : Math.min(200 + 60, label.maxWidth);
return (
controlsWidth +
visibleWidth +
resolveContextStripLabelHiddenWidth({ ...label, visibleWidth })
);
};
for (let availableWidth = 400; availableWidth <= 700; availableWidth += 1) {
for (const compact of [false, true]) {
const next = resolveContextStripLabelsCompact({
compact,
neededWidth: needed(compact),
availableWidth,
});
const settled = resolveContextStripLabelsCompact({
compact: next,
neededWidth: needed(next),
availableWidth,
});
expect(settled).toBe(next);
}
}
});
});
14 changes: 14 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ export function resolveContextStripLabelsCompact(input: {
: input.neededWidth > input.availableWidth;
}

/**
* Width of a strip label's text that its current box hides. Leaf spans are
* summed so a middle-truncated head and tail count together, and capped at
* the expanded max-width so compact and expanded passes measure the same need.
*/
export function resolveContextStripLabelHiddenWidth(input: {
leafWidths: readonly number[];
maxWidth: number;
visibleWidth: number;
}): number {
const naturalWidth = input.leafWidths.reduce((sum, width) => sum + width, 0);
return Math.max(0, Math.min(naturalWidth, input.maxWidth) - input.visibleWidth);
}

export function resolveEnvModeLabel(mode: EnvMode): string {
return mode === "worktree" ? "New worktree" : "Current checkout";
}
Expand Down
26 changes: 17 additions & 9 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useProject, useThreadShell, useThreadShellsForProjectRefs } from "../st
import {
type EnvMode,
type EnvironmentOption,
resolveContextStripLabelHiddenWidth,
resolveContextStripLabelsCompact,
resolveCurrentWorkspaceLabel,
resolveEnvModeLabel,
Expand Down Expand Up @@ -319,6 +320,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
const COMPOSER_CONTEXT_MOTION_DURATION_MS = 180;
const COMPOSER_CONTEXT_MOTION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
const COMPOSER_CONTEXT_LABEL_SELECTOR = "[data-composer-label]";
const COMPOSER_CONTEXT_LABEL_MOTION_SELECTOR = "[data-composer-label-motion]";

function useLabelsOverflow(element: HTMLDivElement | null): boolean {
const [overflows, setOverflows] = useState(false);
Expand Down Expand Up @@ -380,17 +382,23 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean {
needed += width;
}
needed += stripGap * Math.max(0, groups - 1);
for (const label of current.querySelectorAll<HTMLElement>("[data-composer-label]")) {
// The clipping can happen below the marker (SelectValue truncates
// internally), where the outer span's scrollWidth matches its clipped
// box. The text's real width is the largest scrollWidth in the subtree.
let textWidth = label.scrollWidth;
for (const label of current.querySelectorAll<HTMLElement>(COMPOSER_CONTEXT_LABEL_SELECTOR)) {
// Sum the leaf spans so a middle-truncated head and tail count together.
// The motion element keeps its max-width in both states; the marker's
// collapses to zero while compact.
const leafWidths: number[] = [];
for (const inner of label.querySelectorAll<HTMLElement>("*")) {
textWidth = Math.max(textWidth, inner.scrollWidth);
if (inner.children.length === 0) leafWidths.push(inner.scrollWidth);
}
// Subtract the visible width even during an animation. The content
// sum already includes it; only the hidden text needs reserving.
needed += Math.max(0, textWidth - label.getBoundingClientRect().width);
const motion = label.querySelector<HTMLElement>(COMPOSER_CONTEXT_LABEL_MOTION_SELECTOR);
const maxWidth = motion ? Number.parseFloat(getComputedStyle(motion).maxWidth) : Number.NaN;
needed += resolveContextStripLabelHiddenWidth({
leafWidths: leafWidths.length > 0 ? leafWidths : [label.scrollWidth],
maxWidth: Number.isFinite(maxWidth) ? maxWidth : Number.POSITIVE_INFINITY,
// Subtract the visible width even during an animation. The content
// sum already includes it; only the hidden text needs reserving.
visibleWidth: label.getBoundingClientRect().width,
});
}
const nextOverflows = resolveContextStripLabelsCompact({
compact,
Expand Down
Loading