143 lines
3.7 KiB
TypeScript
143 lines
3.7 KiB
TypeScript
export type WorkspaceTabLayoutMode = "full" | "compact" | "icon";
|
|
|
|
export type WorkspaceTabLayoutInput = {
|
|
viewportWidth: number;
|
|
tabLabelLengths: number[];
|
|
metrics: {
|
|
rowHorizontalInset: number;
|
|
actionsReservedWidth: number;
|
|
rowPaddingHorizontal: number;
|
|
tabGap: number;
|
|
maxTabWidth: number;
|
|
iconOnlyTabWidth: number;
|
|
tabBaseWidthWithClose: number;
|
|
minLabelChars: number;
|
|
charWidth: number;
|
|
};
|
|
};
|
|
|
|
export type WorkspaceTabLayoutResult = {
|
|
mode: WorkspaceTabLayoutMode;
|
|
tabWidths: number[];
|
|
shouldScroll: boolean;
|
|
showLabels: boolean;
|
|
showCloseButtons: boolean;
|
|
};
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
if (value < min) {
|
|
return min;
|
|
}
|
|
if (value > max) {
|
|
return max;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function sum(values: number[]): number {
|
|
let total = 0;
|
|
for (const value of values) {
|
|
total += value;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function computeTotalRowWidth(input: {
|
|
tabWidths: number[];
|
|
tabGap: number;
|
|
rowPaddingHorizontal: number;
|
|
}): number {
|
|
if (input.tabWidths.length === 0) {
|
|
return 0;
|
|
}
|
|
return (
|
|
input.rowPaddingHorizontal * 2 +
|
|
sum(input.tabWidths) +
|
|
Math.max(input.tabWidths.length - 1, 0) * input.tabGap
|
|
);
|
|
}
|
|
|
|
export function computeWorkspaceTabLayout(
|
|
input: WorkspaceTabLayoutInput
|
|
): WorkspaceTabLayoutResult {
|
|
const tabCount = input.tabLabelLengths.length;
|
|
if (tabCount === 0) {
|
|
return {
|
|
mode: "full",
|
|
tabWidths: [],
|
|
shouldScroll: false,
|
|
showLabels: true,
|
|
showCloseButtons: true,
|
|
};
|
|
}
|
|
|
|
const availableWidth = Math.max(
|
|
0,
|
|
input.viewportWidth - input.metrics.rowHorizontalInset * 2 - input.metrics.actionsReservedWidth
|
|
);
|
|
const minLabelWidth =
|
|
input.metrics.tabBaseWidthWithClose + input.metrics.charWidth * input.metrics.minLabelChars;
|
|
|
|
const preferredTabWidths = input.tabLabelLengths.map((rawLength) => {
|
|
const labelLength = Math.max(rawLength, 1);
|
|
const preferred = input.metrics.tabBaseWidthWithClose + labelLength * input.metrics.charWidth;
|
|
return clamp(preferred, minLabelWidth, input.metrics.maxTabWidth);
|
|
});
|
|
|
|
const preferredTotal = computeTotalRowWidth({
|
|
tabWidths: preferredTabWidths,
|
|
tabGap: input.metrics.tabGap,
|
|
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
|
});
|
|
|
|
if (preferredTotal <= availableWidth) {
|
|
return {
|
|
mode: "full",
|
|
tabWidths: preferredTabWidths,
|
|
shouldScroll: false,
|
|
showLabels: true,
|
|
showCloseButtons: true,
|
|
};
|
|
}
|
|
|
|
const compactMinTabWidths = preferredTabWidths.map(() => minLabelWidth);
|
|
const compactTotal = computeTotalRowWidth({
|
|
tabWidths: compactMinTabWidths,
|
|
tabGap: input.metrics.tabGap,
|
|
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
|
});
|
|
|
|
if (compactTotal <= availableWidth) {
|
|
const totalShrinkCapacity = sum(preferredTabWidths.map((width) => width - minLabelWidth));
|
|
const targetShrink = preferredTotal - availableWidth;
|
|
const shrinkRatio = totalShrinkCapacity > 0 ? targetShrink / totalShrinkCapacity : 1;
|
|
const tabWidths = preferredTabWidths.map((preferred) => {
|
|
const shrinkCapacity = preferred - minLabelWidth;
|
|
return preferred - shrinkCapacity * shrinkRatio;
|
|
});
|
|
|
|
return {
|
|
mode: "compact",
|
|
tabWidths,
|
|
shouldScroll: false,
|
|
showLabels: true,
|
|
showCloseButtons: true,
|
|
};
|
|
}
|
|
|
|
const iconTabWidths = preferredTabWidths.map(() => input.metrics.iconOnlyTabWidth);
|
|
const iconTotal = computeTotalRowWidth({
|
|
tabWidths: iconTabWidths,
|
|
tabGap: input.metrics.tabGap,
|
|
rowPaddingHorizontal: input.metrics.rowPaddingHorizontal,
|
|
});
|
|
|
|
return {
|
|
mode: "icon",
|
|
tabWidths: iconTabWidths,
|
|
shouldScroll: iconTotal > availableWidth,
|
|
showLabels: false,
|
|
showCloseButtons: false,
|
|
};
|
|
}
|