Skip to content

Commit 9a520c5

Browse files
committed
layout: support modal width 'auto'
1 parent 05b486d commit 9a520c5

2 files changed

Lines changed: 90 additions & 19 deletions

File tree

packages/core/src/layout/__tests__/layout.edgecases.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,41 @@ describe("layout edge cases", () => {
176176
if (!laidOut.ok) return;
177177
assert.deepEqual(laidOut.value.rect, { x: 0, y: 0, w: 20, h: 6 });
178178
});
179+
180+
test("modal width:'auto' measures content and actions", () => {
181+
const modal: VNode = {
182+
kind: "modal",
183+
props: {
184+
id: "m-auto",
185+
width: "auto",
186+
content: { kind: "text", text: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", props: {} }, // 30 cols
187+
actions: Object.freeze([{ kind: "button", props: { id: "ok", label: "OK" } }]),
188+
},
189+
};
190+
const tree: VNode = { kind: "layers", props: {}, children: Object.freeze([modal]) };
191+
const laidOut = mustLayout(tree, 80, 25);
192+
const modalLayout = laidOut.children[0];
193+
assert.ok(modalLayout !== undefined, "expected modal layout node");
194+
if (!modalLayout) return;
195+
assert.equal(modalLayout.rect.w, 32);
196+
});
197+
198+
test("modal width:'auto' respects maxWidth", () => {
199+
const modal: VNode = {
200+
kind: "modal",
201+
props: {
202+
id: "m-auto-maxw",
203+
width: "auto",
204+
maxWidth: 20,
205+
content: { kind: "text", text: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", props: {} }, // 30 cols
206+
actions: Object.freeze([{ kind: "button", props: { id: "ok", label: "OK" } }]),
207+
},
208+
};
209+
const tree: VNode = { kind: "layers", props: {}, children: Object.freeze([modal]) };
210+
const laidOut = mustLayout(tree, 80, 25);
211+
const modalLayout = laidOut.children[0];
212+
assert.ok(modalLayout !== undefined, "expected modal layout node");
213+
if (!modalLayout) return;
214+
assert.equal(modalLayout.rect.w, 20);
215+
});
179216
});

packages/core/src/layout/kinds/overlays.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { clampNonNegative, clampWithin } from "../engine/bounds.js";
33
import { isVNode } from "../engine/guards.js";
44
import { ok } from "../engine/result.js";
55
import type { LayoutTree } from "../engine/types.js";
6+
import { measureTextCells } from "../textMeasure.js";
67
import type { Axis, Rect, Size } from "../types.js";
78
import type { LayoutResult } from "../validateProps.js";
89

@@ -203,37 +204,70 @@ export function layoutOverlays(
203204

204205
const title = typeof props.title === "string" ? props.title : undefined;
205206

207+
const content = isVNode(props.content) ? props.content : null;
208+
const actionsRaw = Array.isArray(props.actions) ? props.actions : [];
209+
const actions: VNode[] = [];
210+
for (const a of actionsRaw) {
211+
if (isVNode(a)) actions.push(a);
212+
}
213+
214+
const border = 1;
215+
const titleH = title ? 1 : 0;
216+
const actionsH = actions.length > 0 ? 1 : 0;
217+
206218
const maxWidth =
207219
typeof props.maxWidth === "number" && props.maxWidth > 0
208220
? Math.floor(props.maxWidth)
209221
: rectW;
210222

211-
let modalW =
212-
typeof props.width === "number" && props.width > 0
213-
? Math.floor(props.width)
214-
: Math.floor(rectW * 0.7);
215-
216-
modalW = clampNonNegative(Math.min(modalW, maxWidth));
217-
modalW = clampWithin(modalW, Math.min(10, rectW), rectW);
218-
if (rectW >= 4) modalW = Math.min(modalW, rectW - 2);
219-
220223
let modalH = Math.floor(rectH * 0.6);
221224
modalH = clampWithin(modalH, Math.min(5, rectH), rectH);
222225
if (rectH >= 4) modalH = Math.min(modalH, rectH - 2);
223226

224-
const mx = x + Math.floor((rectW - modalW) / 2);
225-
const my = y + Math.floor((rectH - modalH) / 2);
227+
const maxModalW = clampNonNegative(Math.min(rectW, maxWidth));
228+
const maxInnerW = clampNonNegative(maxModalW - border * 2);
229+
const maxInnerH = clampNonNegative(modalH - border * 2 - titleH - actionsH);
226230

227-
const content = isVNode(props.content) ? props.content : null;
228-
const actionsRaw = Array.isArray(props.actions) ? props.actions : [];
229-
const actions: VNode[] = [];
230-
for (const a of actionsRaw) {
231-
if (isVNode(a)) actions.push(a);
231+
let modalW: number;
232+
if (typeof props.width === "number" && props.width > 0) {
233+
modalW = Math.floor(props.width);
234+
} else if (props.width === "auto") {
235+
let contentW = 0;
236+
if (content) {
237+
const sizeRes = measureNode(content, maxInnerW, maxInnerH, "column");
238+
if (!sizeRes.ok) return sizeRes;
239+
contentW = clampNonNegative(Math.min(maxInnerW, sizeRes.value.w));
240+
}
241+
242+
let actionsW = 0;
243+
if (actions.length > 0) {
244+
const gap = 1;
245+
let total = 0;
246+
for (let i = 0; i < actions.length; i++) {
247+
const a = actions[i];
248+
if (!a) continue;
249+
const sizeRes = measureNode(a, maxInnerW, 1, "row");
250+
if (!sizeRes.ok) return sizeRes;
251+
const aw = clampNonNegative(Math.min(maxInnerW, sizeRes.value.w));
252+
total += aw;
253+
if (i < actions.length - 1) total += gap;
254+
}
255+
actionsW = total;
256+
}
257+
258+
const innerW = Math.max(contentW, actionsW);
259+
const titleW = title ? measureTextCells(title) + 4 : 0;
260+
modalW = Math.max(innerW + border * 2, titleW);
261+
} else {
262+
modalW = Math.floor(rectW * 0.7);
232263
}
233264

234-
const border = 1;
235-
const titleH = title ? 1 : 0;
236-
const actionsH = actions.length > 0 ? 1 : 0;
265+
modalW = clampNonNegative(Math.min(modalW, maxWidth));
266+
modalW = clampWithin(modalW, Math.min(10, rectW), rectW);
267+
if (rectW >= 4) modalW = Math.min(modalW, rectW - 2);
268+
269+
const mx = x + Math.floor((rectW - modalW) / 2);
270+
const my = y + Math.floor((rectH - modalH) / 2);
237271

238272
const innerX = mx + border;
239273
const innerY = my + border + titleH;

0 commit comments

Comments
 (0)