Stack branch labels when a row is tight; fan them out on hover or selection

This commit is contained in:
2026-10-14 22:01:35 +02:00
parent ad4d087aaf
commit 04cf5c5dae
5 changed files with 190 additions and 36 deletions
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { MESSAGE_KEEP, PEEK, shouldStack, stackWidth } from "./labels";
describe("shouldStack", () => {
it("never stacks a single label", () => {
expect(shouldStack([500], 900, 100)).toBe(false);
});
it("does not stack when labels and message fit", () => {
expect(shouldStack([100, 100], 200, 600)).toBe(false);
});
it("stacks when the labels would leave the message too little room", () => {
expect(shouldStack([200, 200], 900, 300 + MESSAGE_KEEP)).toBe(true);
});
it("lets a short message leave its room to the labels", () => {
expect(shouldStack([200, 200], 50, 470)).toBe(false);
});
});
describe("stackWidth", () => {
it("is the first label plus a peek for each other label", () => {
expect(stackWidth([80, 120, 60])).toBe(80 + 2 * PEEK);
expect(stackWidth([])).toBe(0);
});
});
+17
View File
@@ -0,0 +1,17 @@
/** How far each label behind the first one sticks out of a stack, in pixels. */
export const PEEK = 6;
export const GAP = 5;
/** The message keeps at least this much width before labels start stacking. */
export const MESSAGE_KEEP = 240;
/** Whether a row's labels must stack because they do not fit next to the message. */
export function shouldStack(labelWidths: number[], messageWidth: number, space: number): boolean {
if (labelWidths.length < 2) return false;
const full = labelWidths.reduce((a, b) => a + b, 0) + GAP * (labelWidths.length - 1);
// 8px is the gap between the message and the labels.
return full + Math.min(messageWidth, MESSAGE_KEEP) + 8 > space;
}
export function stackWidth(labelWidths: number[]): number {
return labelWidths.length === 0 ? 0 : labelWidths[0] + PEEK * (labelWidths.length - 1);
}
+15
View File
@@ -0,0 +1,15 @@
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
const cache = new Map<string, number>();
/** Width in pixels of `text` drawn in the CSS `font`. */
export function textWidth(text: string, font: string): number {
const key = font + "\u0000" + text;
let w = cache.get(key);
if (w === undefined) {
ctx.font = font;
w = ctx.measureText(text).width;
cache.set(key, w);
}
return w;
}