diff --git a/.changeset/tidy-rows-stack.md b/.changeset/tidy-rows-stack.md
new file mode 100644
index 00000000..ede08cbf
--- /dev/null
+++ b/.changeset/tidy-rows-stack.md
@@ -0,0 +1,5 @@
+---
+"hunkdiff": patch
+---
+
+Keep overcommitted STML rows within their requested width without dropping inline content.
diff --git a/src/ui/lib/stml/layout.test.ts b/src/ui/lib/stml/layout.test.ts
index f632f2df..66d38969 100644
--- a/src/ui/lib/stml/layout.test.ts
+++ b/src/ui/lib/stml/layout.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
+import { measureTextWidth } from "../text";
import { layoutStml, layoutStmlCached, type StmlLine } from "./layout";
function lineText(line: StmlLine): string {
@@ -102,10 +103,24 @@ describe("layoutStml", () => {
expect(top.length).toBe(20);
});
- test("degrades a too-narrow row to stacked blocks with a note", () => {
- const { lines, errors } = layoutStml("" + "x".repeat(6) + "
", 9);
+ test("stacks overcommitted fixed-width columns within the row width", () => {
+ const width = 20;
+ const { lines, errors } = layoutStml(
+ 'ab
',
+ width,
+ );
+
+ expect(errors.some((error) => error.includes("too narrow"))).toBe(true);
+ expect(lines.every((line) => measureTextWidth(lineText(line)) <= width)).toBe(true);
+ });
+
+ test("preserves loose text when a too-narrow row stacks its blocks", () => {
+ const markup = "prefix" + "x".repeat(6) + "
";
+ const { lines, errors } = layoutStml(markup, 9);
+
expect(errors.some((error) => error.includes("too narrow"))).toBe(true);
- expect(lines.length).toBeGreaterThan(6);
+ expect(frameText(lines)[0]).toBe("prefix");
+ expect(lines.length).toBeGreaterThan(7);
});
test("renders ordered and unordered lists with hanging indents", () => {
diff --git a/src/ui/lib/stml/layout.ts b/src/ui/lib/stml/layout.ts
index b9bfc248..a627e973 100644
--- a/src/ui/lib/stml/layout.ts
+++ b/src/ui/lib/stml/layout.ts
@@ -552,6 +552,7 @@ function mergeColumns(columns: StmlLine[][], widths: number[], gap: number): Stm
return merged;
}
+/** Lay out one row horizontally when its columns fit, otherwise stack every child. */
function layoutRow(
el: StmlElement,
width: number,
@@ -571,25 +572,28 @@ function layoutRow(
if (looseInline.some((node) => node.type !== "text" || node.value.trim() !== "")) {
errors.add(" mixes bare text with block children; text laid out above the row");
}
+ const inlinePrefix =
+ looseInline.length > 0 ? layoutBlockNodes(looseInline, width, style, errors) : [];
const gap = Math.max(0, numAttr(el.attrs.gap) ?? 1);
const totalGap = gap * (children.length - 1);
const available = width - totalGap;
- // Fixed-width columns claim their space first; the rest share what remains.
+ // Fixed-width columns claim their space first; flex columns need at least one cell each.
const fixed = children.map((child) => widthAttr(child.attrs.width, available));
const fixedTotal = fixed.reduce((total, w) => total + (w ?? 0), 0);
const flexCount = fixed.filter((w) => w === undefined).length;
- const flexSpace = Math.max(flexCount, available - fixedTotal);
- const flexWidth = flexCount > 0 ? Math.floor(flexSpace / flexCount) : 0;
- let flexRemainder = flexCount > 0 ? flexSpace - flexWidth * flexCount : 0;
-
- if (available < children.length) {
- // Too narrow to sit side by side — degrade to stacked blocks.
+ if (fixedTotal + flexCount > available) {
errors.add(" too narrow for its columns; stacking vertically");
- return children.flatMap((child) => layoutBlock(child, width, style, errors));
+ return [
+ ...inlinePrefix,
+ ...children.flatMap((child) => layoutBlock(child, width, style, errors)),
+ ];
}
+ const flexSpace = available - fixedTotal;
+ const flexWidth = flexCount > 0 ? Math.floor(flexSpace / flexCount) : 0;
+ let flexRemainder = flexCount > 0 ? flexSpace - flexWidth * flexCount : 0;
const widths = fixed.map((w) => {
if (w !== undefined) {
return Math.max(1, Math.min(w, available));
@@ -598,9 +602,6 @@ function layoutRow(
flexRemainder -= extra;
return Math.max(1, flexWidth + extra);
});
-
- const inlinePrefix =
- looseInline.length > 0 ? layoutBlockNodes(looseInline, width, style, errors) : [];
const columns = children.map((child, index) => layoutBlock(child, widths[index]!, style, errors));
return [...inlinePrefix, ...mergeColumns(columns, widths, gap)];
}