A node's description and every note in the inspector were one line cut with an ellipsis. Stop pattern says what it does with no pattern picked, but only past the cut, so nobody could read it. - Label takes a wrap option. It breaks its text at the width it gets and keeps the margin a single line has, above and below all lines. - Widget.heightFor(width) gives the height at a width. It is measure().h everywhere except where text wraps. - VStack and SectionStack lay a column out with it, at the width each row will get. - ScrollContainer measures again at the narrower width when its scrollbar shows, because the text then takes more lines. - addNote and the empty inspector's hint wrap. - The file list's own heightFor(count) is now heightOfRows, so the layout does not hand it a width as a row count. Tests: - Unit scroll-container: 7 green, 2 new (height for width, again beside the scrollbar) - Unit ui + inspector: 17 files, 336 green (before the 2 new ones) - e2e 02 (stein2, atx busy with suite 545): ALL PASSED, 29 checks - Screenshot: Stop pattern shows its description in six lines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { ScrollContainer, Spacer, Theme, VStack, Widget } from '../../src/ui';
|
|
|
|
/** Row tops after a layout; an unlaid-out stack leaves every child at 0. */
|
|
const tops = (stack: VStack) => stack.children.map((c) => c.y);
|
|
|
|
describe('a scroll container told its content changed', () => {
|
|
it('lays out rows that leave the height pinned to the viewport', () => {
|
|
const scroll = new ScrollContainer();
|
|
scroll.resize(200, 300);
|
|
const rows = new VStack();
|
|
// The list tab hands over the stack before filling it, so the empty content is measured first
|
|
// and pinned to the viewport height.
|
|
scroll.setContent(rows);
|
|
rows.add(new Spacer(20));
|
|
rows.add(new Spacer(20));
|
|
rows.add(new Spacer(20));
|
|
|
|
// 60 < 300, so the content keeps the height it already had: `resize` alone would see no
|
|
// change and skip the layout, leaving all three rows stacked at the top.
|
|
scroll.relayoutContent();
|
|
expect(tops(rows)).toEqual([0, 20, 40]);
|
|
});
|
|
|
|
it('still lays out rows that make the content taller than the viewport', () => {
|
|
const scroll = new ScrollContainer();
|
|
scroll.resize(200, 50);
|
|
const rows = new VStack();
|
|
scroll.setContent(rows);
|
|
rows.add(new Spacer(20));
|
|
rows.add(new Spacer(20));
|
|
rows.add(new Spacer(20));
|
|
|
|
scroll.relayoutContent();
|
|
expect(tops(rows)).toEqual([0, 20, 40]);
|
|
expect(scroll.maxScroll).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('a scroll container and its scrollbar', () => {
|
|
const filled = (viewportH: number, rowCount: number) => {
|
|
const scroll = new ScrollContainer();
|
|
scroll.resize(200, viewportH);
|
|
const rows = new VStack();
|
|
for (let i = 0; i < rowCount; i++) rows.add(new Spacer(20));
|
|
scroll.setContent(rows);
|
|
return { scroll, rows };
|
|
};
|
|
|
|
it('gives content that fits the full width', () => {
|
|
expect(filled(300, 3).rows.w).toBe(200);
|
|
});
|
|
|
|
it('keeps the gutter free for the bar while the content scrolls', () => {
|
|
expect(filled(50, 3).rows.w).toBe(200 - Theme.scrollbarWidth);
|
|
});
|
|
|
|
it('hands the gutter back once the content fits again', () => {
|
|
const { scroll, rows } = filled(50, 3);
|
|
scroll.resize(200, 300);
|
|
expect(rows.w).toBe(200);
|
|
});
|
|
});
|
|
|
|
/** Stands in for wrapped text: 20 px per line, a line per 100 px of text. */
|
|
class Wrapping extends Widget {
|
|
constructor(private readonly textWidth: number) {
|
|
super();
|
|
}
|
|
|
|
override heightFor(width: number): number {
|
|
return Math.ceil(this.textWidth / width) * 20;
|
|
}
|
|
}
|
|
|
|
describe('a scroll container with content that wraps', () => {
|
|
it('gives a row the height it needs at the width it gets', () => {
|
|
const scroll = new ScrollContainer();
|
|
scroll.resize(200, 300);
|
|
const rows = new VStack();
|
|
const text = rows.add(new Wrapping(500));
|
|
rows.add(new Spacer(20));
|
|
scroll.setContent(rows);
|
|
expect(text.h).toBe(60);
|
|
expect(tops(rows)).toEqual([0, 60]);
|
|
});
|
|
|
|
it('measures again once the scrollbar narrows the content', () => {
|
|
const scroll = new ScrollContainer();
|
|
scroll.resize(200, 100);
|
|
const rows = new VStack();
|
|
// 400 px of text is two lines at 200 px, but three beside the bar.
|
|
const text = rows.add(new Wrapping(400));
|
|
rows.add(new Spacer(70));
|
|
scroll.setContent(rows);
|
|
expect(rows.w).toBe(200 - Theme.scrollbarWidth);
|
|
expect(text.h).toBe(60);
|
|
expect(scroll.maxScroll).toBe(30);
|
|
});
|
|
});
|