mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(tui): add configurable fullscreen scrollbars
This commit is contained in:
parent
12a4b24292
commit
6129a353b1
13 changed files with 153 additions and 39 deletions
|
|
@ -7,7 +7,7 @@
|
|||
- Added chainable `pi.registerMarkdownTransformer()` hooks for display-only transformation of user and assistant Markdown.
|
||||
- Added an experimental fullscreen UI mode, selectable through `--ui-mode fullscreen` or `/settings` ([#7304](https://github.com/earendil-works/pi/issues/7304)).
|
||||
- Added a sticky editor, status, widget, and footer dock to fullscreen mode while keeping the transcript independently scrollable.
|
||||
- Added a transient draggable transcript scrollbar to fullscreen mode.
|
||||
- Added a draggable transcript scrollbar to fullscreen mode with configurable `auto`, `always`, and `hidden` modes through `/settings`; `always` reserves the rightmost column.
|
||||
- Added page scrolling and marked-message navigation shortcuts to fullscreen mode.
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|||
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
||||
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
||||
| `uiMode` | string | `"regular"` | Interactive UI mode: `"regular"` or experimental `"fullscreen"`. Changes from `/settings` apply after restart; `--ui-mode` overrides this setting for one run |
|
||||
| `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"` shows it temporarily while scrolling, `"always"` reserves the rightmost column and keeps it visible, and `"hidden"` hides it. Has no effect in regular UI mode |
|
||||
|
||||
For VS Code, include `--wait` so pi resumes after the editor exits:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { Transport } from "@earendil-works/pi-ai";
|
||||
import type { ScrollViewScrollbar } from "@earendil-works/pi-tui";
|
||||
import { randomUUID } from "crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
|
|
@ -129,6 +130,7 @@ export interface Settings {
|
|||
httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it
|
||||
websocketConnectTimeoutMs?: number; // WebSocket connect/open handshake timeout in milliseconds; 0 disables it
|
||||
uiMode?: UiMode; // default: "regular"
|
||||
fullscreenScrollbar?: ScrollViewScrollbar; // default: "auto"; no effect in regular UI mode
|
||||
}
|
||||
|
||||
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
||||
|
|
@ -1133,6 +1135,17 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getFullscreenScrollbar(): ScrollViewScrollbar {
|
||||
const mode = this.settings.fullscreenScrollbar;
|
||||
return mode === "always" || mode === "hidden" ? mode : "auto";
|
||||
}
|
||||
|
||||
setFullscreenScrollbar(mode: ScrollViewScrollbar): void {
|
||||
this.globalSettings.fullscreenScrollbar = mode;
|
||||
this.markModified("fullscreenScrollbar");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getImageAutoResize(): boolean {
|
||||
return this.settings.images?.autoResize ?? true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
type Component,
|
||||
Container,
|
||||
getCapabilities,
|
||||
type ScrollViewScrollbar,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
type SelectListLayoutOptions,
|
||||
|
|
@ -80,6 +81,7 @@ export interface SettingsConfig {
|
|||
clearOnShrink: boolean;
|
||||
showTerminalProgress: boolean;
|
||||
uiMode: UiMode;
|
||||
fullscreenScrollbar: ScrollViewScrollbar;
|
||||
warnings: WarningSettings;
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +114,7 @@ export interface SettingsCallbacks {
|
|||
onClearOnShrinkChange: (enabled: boolean) => void;
|
||||
onShowTerminalProgressChange: (enabled: boolean) => void;
|
||||
onUiModeChange: (mode: UiMode) => void;
|
||||
onFullscreenScrollbarChange: (mode: ScrollViewScrollbar) => void;
|
||||
onWarningsChange: (warnings: WarningSettings) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
|
@ -619,6 +622,13 @@ export class SettingsSelectorComponent extends Container {
|
|||
currentValue: config.uiMode,
|
||||
values: ["regular", "fullscreen"],
|
||||
},
|
||||
{
|
||||
id: "fullscreen-scrollbar",
|
||||
label: "Fullscreen scrollbar",
|
||||
description: "Scrollbar behavior in fullscreen mode; has no effect in regular mode",
|
||||
currentValue: config.fullscreenScrollbar,
|
||||
values: ["auto", "always", "hidden"],
|
||||
},
|
||||
{
|
||||
id: "theme",
|
||||
label: "Theme",
|
||||
|
|
@ -831,6 +841,9 @@ export class SettingsSelectorComponent extends Container {
|
|||
case "ui-mode":
|
||||
callbacks.onUiModeChange(newValue as UiMode);
|
||||
break;
|
||||
case "fullscreen-scrollbar":
|
||||
callbacks.onFullscreenScrollbarChange(newValue as ScrollViewScrollbar);
|
||||
break;
|
||||
case "theme":
|
||||
callbacks.onThemeChange(newValue);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ export class InteractiveMode {
|
|||
private loadedResourcesContainer: Container;
|
||||
private chatContainer: Container;
|
||||
private documentContainer: Container;
|
||||
private transcriptScrollView: TuiLayouts.ScrollView | undefined;
|
||||
private pendingMessagesContainer: Container;
|
||||
private statusContainer: Container;
|
||||
private defaultEditor: CustomEditor;
|
||||
|
|
@ -745,11 +746,11 @@ export class InteractiveMode {
|
|||
// Populate stable regions before selecting the renderer-specific composition.
|
||||
this.renderWidgets(); // Initialize with default spacer
|
||||
if (TuiLayouts.isViewportTUI(this.ui)) {
|
||||
const transcript = new TuiLayouts.ScrollView(this.documentContainer, {
|
||||
this.transcriptScrollView = new TuiLayouts.ScrollView(this.documentContainer, {
|
||||
follow: "end",
|
||||
primary: true,
|
||||
overscroll: "chain",
|
||||
scrollbar: "auto",
|
||||
scrollbar: this.settingsManager.getFullscreenScrollbar(),
|
||||
scrollbarStyle: (text) => theme.bg("selectedBg", text),
|
||||
});
|
||||
const dock = new TuiLayouts.VStack([
|
||||
|
|
@ -762,7 +763,7 @@ export class InteractiveMode {
|
|||
]);
|
||||
this.ui.setLayoutRoot(
|
||||
new TuiLayouts.VStack([
|
||||
{ component: transcript, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
||||
{ component: this.transcriptScrollView, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
||||
{ component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
||||
]),
|
||||
);
|
||||
|
|
@ -1769,8 +1770,13 @@ export class InteractiveMode {
|
|||
this.showStartupNoticesIfNeeded();
|
||||
}
|
||||
|
||||
private applyFullscreenScrollbarSetting(): void {
|
||||
this.transcriptScrollView?.setScrollbar(this.settingsManager.getFullscreenScrollbar());
|
||||
}
|
||||
|
||||
private applyRuntimeSettings(): void {
|
||||
configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
|
||||
this.applyFullscreenScrollbarSetting();
|
||||
this.footer.setSession(this.session);
|
||||
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
||||
|
|
@ -4237,6 +4243,7 @@ export class InteractiveMode {
|
|||
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
||||
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
||||
uiMode: this.settingsManager.getUiMode(),
|
||||
fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
|
||||
warnings: this.settingsManager.getWarnings(),
|
||||
},
|
||||
{
|
||||
|
|
@ -4381,6 +4388,10 @@ export class InteractiveMode {
|
|||
this.settingsManager.setUiMode(mode);
|
||||
this.showStatus(`UI mode: ${mode} (restart required)`);
|
||||
},
|
||||
onFullscreenScrollbarChange: (mode) => {
|
||||
this.settingsManager.setFullscreenScrollbar(mode);
|
||||
this.applyFullscreenScrollbarSetting();
|
||||
},
|
||||
onWarningsChange: (warnings) => {
|
||||
this.settingsManager.setWarnings(warnings);
|
||||
},
|
||||
|
|
@ -5452,7 +5463,6 @@ export class InteractiveMode {
|
|||
try {
|
||||
await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart });
|
||||
restoreChatBeforeSessionStart();
|
||||
configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
|
||||
this.keybindings.reload();
|
||||
const activeHeader = this.customHeader ?? this.builtInHeader;
|
||||
if (isExpandable(activeHeader)) {
|
||||
|
|
@ -5460,20 +5470,7 @@ export class InteractiveMode {
|
|||
}
|
||||
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
||||
await this.themeController.applyFromSettings();
|
||||
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
||||
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
||||
this.defaultEditor.setPaddingX(editorPaddingX);
|
||||
this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
|
||||
if (this.editor !== this.defaultEditor) {
|
||||
this.editor.setPaddingX?.(editorPaddingX);
|
||||
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
||||
}
|
||||
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
||||
const clearOnShrink = this.settingsManager.getClearOnShrink();
|
||||
this.ui.setClearOnShrink(clearOnShrink);
|
||||
if (!clearOnShrink && !this.activeStatusIndicator) {
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
this.applyRuntimeSettings();
|
||||
this.setupAutocompleteProvider();
|
||||
const runner = this.session.extensionRunner;
|
||||
this.setupExtensionShortcuts(runner);
|
||||
|
|
|
|||
|
|
@ -420,6 +420,18 @@ describe("SettingsManager", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("validates and persists the fullscreen scrollbar mode", async () => {
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
expect(manager.getFullscreenScrollbar()).toBe("auto");
|
||||
|
||||
manager.setFullscreenScrollbar("hidden");
|
||||
await manager.flush();
|
||||
expect(JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8")).fullscreenScrollbar).toBe("hidden");
|
||||
|
||||
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ fullscreenScrollbar: "sometimes" }));
|
||||
expect(SettingsManager.create(projectDir, agentDir).getFullscreenScrollbar()).toBe("auto");
|
||||
});
|
||||
|
||||
describe("outputPad", () => {
|
||||
it("should default to 1 and persist binary values", async () => {
|
||||
const manager = SettingsManager.create(projectDir, agentDir);
|
||||
|
|
|
|||
37
packages/coding-agent/test/settings-selector.test.ts
Normal file
37
packages/coding-agent/test/settings-selector.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
||||
import {
|
||||
type SettingsCallbacks,
|
||||
type SettingsConfig,
|
||||
SettingsSelectorComponent,
|
||||
} from "../src/modes/interactive/components/settings-selector.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
||||
describe("SettingsSelectorComponent", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
it("cycles through fullscreen scrollbar modes", () => {
|
||||
const onChange = vi.fn();
|
||||
const selector = new SettingsSelectorComponent(
|
||||
{
|
||||
fullscreenScrollbar: "auto",
|
||||
warnings: {},
|
||||
availableThinkingLevels: [],
|
||||
availableThemes: [],
|
||||
} as unknown as SettingsConfig,
|
||||
{ onFullscreenScrollbarChange: onChange } as unknown as SettingsCallbacks,
|
||||
);
|
||||
const settingsList = selector.getSettingsList();
|
||||
|
||||
for (const character of "Fullscreen scrollbar") settingsList.handleInput(character);
|
||||
settingsList.handleInput("\r");
|
||||
settingsList.handleInput("\r");
|
||||
settingsList.handleInput("\r");
|
||||
|
||||
expect(onChange.mock.calls.flat()).toEqual(["always", "hidden", "auto"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
- Added interface-compatible main-screen and alternate-screen TUI renderers with application-owned scrolling ([#7304](https://github.com/earendil-works/pi/issues/7304)).
|
||||
- Added alternate-screen `VStack`, `HStack`, and nested `ScrollView` layouts with constrained sizing, sticky regions, and pointer-targeted scrolling.
|
||||
- Added edge auto-scrolling for alternate-screen drag selection across off-screen scroll-view content.
|
||||
- Added transient proportional scrollbars with mouse dragging and Home/End document navigation to alternate-screen `ScrollView`s.
|
||||
- Added proportional scrollbars with mouse dragging, Home/End document navigation, transient `auto` mode, and an `always` mode that reserves the rightmost column; scrollbar modes can be changed at runtime.
|
||||
- Added page scrolling and OSC 133 semantic prompt navigation to the alternate-screen viewport.
|
||||
|
||||
## [0.83.0] - 2026-07-29
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { LAYOUT_NODE, type ScrollLayoutNode } from "../layout-node.ts";
|
||||
import { type Component, Container } from "../tui.ts";
|
||||
|
||||
export type ScrollViewScrollbar = "hidden" | "auto" | "always";
|
||||
|
||||
export interface ScrollViewOptions {
|
||||
axis?: "vertical";
|
||||
follow?: "none" | "end";
|
||||
primary?: boolean;
|
||||
overscroll?: "chain" | "contain";
|
||||
scrollbar?: "hidden" | "auto" | "always";
|
||||
scrollbar?: ScrollViewScrollbar;
|
||||
scrollbarStyle?: (text: string) => string;
|
||||
scrollbarHideDelayMs?: number;
|
||||
}
|
||||
|
|
@ -16,8 +18,8 @@ export class ScrollView extends Container {
|
|||
private readonly followEnd: boolean;
|
||||
readonly primary: boolean;
|
||||
readonly overscroll: "chain" | "contain";
|
||||
readonly scrollbar: "hidden" | "auto" | "always";
|
||||
readonly scrollbarStyle: (text: string) => string;
|
||||
private currentScrollbar: ScrollViewScrollbar;
|
||||
private readonly scrollbarHideDelayMs: number;
|
||||
private currentScrollTop = 0;
|
||||
private contentHeight = 0;
|
||||
|
|
@ -39,7 +41,7 @@ export class ScrollView extends Container {
|
|||
this.followingEnd = this.followEnd;
|
||||
this.primary = options.primary ?? false;
|
||||
this.overscroll = options.overscroll ?? "chain";
|
||||
this.scrollbar = options.scrollbar ?? "hidden";
|
||||
this.currentScrollbar = options.scrollbar ?? "hidden";
|
||||
this.scrollbarStyle = options.scrollbarStyle ?? ((text) => `\x1b[100m${text}\x1b[49m`);
|
||||
this.scrollbarHideDelayMs = Math.max(0, Math.floor(options.scrollbarHideDelayMs ?? 1000));
|
||||
}
|
||||
|
|
@ -56,13 +58,29 @@ export class ScrollView extends Container {
|
|||
return this.currentViewportHeight;
|
||||
}
|
||||
|
||||
get scrollbar(): ScrollViewScrollbar {
|
||||
return this.currentScrollbar;
|
||||
}
|
||||
|
||||
get isScrollbarVisible(): boolean {
|
||||
if (this.scrollbar === "always") return this.currentViewportHeight > 0;
|
||||
return (
|
||||
this.contentHeight > this.currentViewportHeight &&
|
||||
(this.scrollbar === "always" || this.transientScrollbarVisible)
|
||||
this.scrollbar === "auto" && this.contentHeight > this.currentViewportHeight && this.transientScrollbarVisible
|
||||
);
|
||||
}
|
||||
|
||||
setScrollbar(scrollbar: ScrollViewScrollbar): void {
|
||||
if (scrollbar === this.currentScrollbar) return;
|
||||
this.currentScrollbar = scrollbar;
|
||||
if (scrollbar !== "auto") this.hideTransientScrollbar();
|
||||
else if (this.scrollbarActive) this.markScrollbarActivity();
|
||||
this.requestRenderCallback?.();
|
||||
}
|
||||
|
||||
getContentWidth(width: number): number {
|
||||
return this.scrollbar === "always" && width > 1 ? width - 1 : width;
|
||||
}
|
||||
|
||||
private markScrollbarActivity(): void {
|
||||
if (this.scrollbar !== "auto" || this.contentHeight <= this.currentViewportHeight) return;
|
||||
this.transientScrollbarVisible = true;
|
||||
|
|
@ -166,7 +184,9 @@ export class ScrollView extends Container {
|
|||
}
|
||||
|
||||
override render(width: number): string[] {
|
||||
return this.child.render(width);
|
||||
const contentWidth = this.getContentWidth(width);
|
||||
const lines = this.child.render(contentWidth);
|
||||
return contentWidth === width ? lines : lines.map((line) => `${line} `);
|
||||
}
|
||||
|
||||
[LAYOUT_NODE](): ScrollLayoutNode {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export { Image, type ImageOptions, type ImageTheme } from "./components/image.ts
|
|||
export { Input } from "./components/input.ts";
|
||||
export { Loader, type LoaderIndicatorOptions } from "./components/loader.ts";
|
||||
export { type DefaultTextStyle, Markdown, type MarkdownOptions, type MarkdownTheme } from "./components/markdown.ts";
|
||||
export { ScrollView, type ScrollViewOptions } from "./components/scroll-view.ts";
|
||||
export { ScrollView, type ScrollViewOptions, type ScrollViewScrollbar } from "./components/scroll-view.ts";
|
||||
export {
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface ScrollLayoutState {
|
|||
readonly primary: boolean;
|
||||
readonly overscroll: "chain" | "contain";
|
||||
readonly viewportHeight: number;
|
||||
getContentWidth(width: number): number;
|
||||
updateLayout(contentHeight: number, viewportHeight: number, requestRender: () => void): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,16 @@ function layoutComponent(
|
|||
|
||||
if (node.type === "scroll") {
|
||||
const previousScrollTop = node.state.scrollTop;
|
||||
const childBox = layoutComponent(context, node.component, x, y - previousScrollTop, safeWidth, undefined, clip);
|
||||
const contentWidth = node.state.getContentWidth(safeWidth);
|
||||
const childBox = layoutComponent(
|
||||
context,
|
||||
node.component,
|
||||
x,
|
||||
y - previousScrollTop,
|
||||
contentWidth,
|
||||
undefined,
|
||||
clip,
|
||||
);
|
||||
const contentHeight = childBox.rect.height;
|
||||
const viewportHeight = height === undefined ? contentHeight : Math.max(0, Math.floor(height));
|
||||
node.state.updateLayout(contentHeight, viewportHeight, context.requestRender);
|
||||
|
|
@ -144,7 +153,7 @@ function layoutComponent(
|
|||
clip: childClip,
|
||||
children: [childBox],
|
||||
scrollView,
|
||||
scrollContentLines: renderCached(context, node.component, safeWidth),
|
||||
scrollContentLines: renderCached(context, node.component, contentWidth),
|
||||
layer: 0,
|
||||
};
|
||||
childBox.parent = box;
|
||||
|
|
@ -261,9 +270,9 @@ export function getScrollbarGeometry(box: LayoutBox): ScrollbarGeometry | undefi
|
|||
minThumbHeight,
|
||||
Math.min(trackHeight, Math.round((trackHeight * trackHeight) / contentHeight)),
|
||||
);
|
||||
const maxScrollTop = contentHeight - trackHeight;
|
||||
const maxScrollTop = Math.max(0, contentHeight - trackHeight);
|
||||
const maxThumbTop = trackHeight - thumbHeight;
|
||||
const thumbOffset = Math.round((box.scrollView.scrollTop / maxScrollTop) * maxThumbTop);
|
||||
const thumbOffset = maxScrollTop === 0 ? 0 : Math.round((box.scrollView.scrollTop / maxScrollTop) * maxThumbTop);
|
||||
const column = box.rect.x + box.rect.width - 1;
|
||||
if (column < box.clip.x || column >= box.clip.x + box.clip.width) return undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -159,16 +159,14 @@ describe("viewport layout", () => {
|
|||
);
|
||||
|
||||
const alwaysFitting = new ScrollView(fittingContent, { scrollbar: "always", scrollbarStyle });
|
||||
assert.ok(
|
||||
renderLayoutFrame(alwaysFitting, 6, 4, () => {}).lines.every((line) => !line.includes(scrollbarBackground)),
|
||||
);
|
||||
const alwaysFittingFrame = renderLayoutFrame(alwaysFitting, 6, 4, () => {});
|
||||
assert.strictEqual(alwaysFittingFrame.root.children[0]?.rect.width, 5);
|
||||
assert.ok(alwaysFittingFrame.lines.every((line) => line.includes(scrollbarBackground)));
|
||||
|
||||
const alwaysOverflowing = new ScrollView(content, { scrollbar: "always", scrollbarStyle });
|
||||
assert.strictEqual(
|
||||
renderLayoutFrame(alwaysOverflowing, 6, 4, () => {}).lines.filter((line) => line.includes(scrollbarBackground))
|
||||
.length,
|
||||
2,
|
||||
);
|
||||
const alwaysOverflowingFrame = renderLayoutFrame(alwaysOverflowing, 6, 4, () => {});
|
||||
assert.strictEqual(alwaysOverflowingFrame.root.children[0]?.rect.width, 5);
|
||||
assert.strictEqual(alwaysOverflowingFrame.lines.filter((line) => line.includes(scrollbarBackground)).length, 2);
|
||||
|
||||
const thumbHeightFor = (contentHeight: number) => {
|
||||
const sized = new ScrollView(new Text(Array.from({ length: contentHeight }, () => "x").join("\n"), 0, 0), {
|
||||
|
|
@ -186,6 +184,19 @@ describe("viewport layout", () => {
|
|||
assert.strictEqual(thumbHeightFor(400), 2);
|
||||
});
|
||||
|
||||
it("updates reserved scrollbar layout at runtime", () => {
|
||||
const scrollView = new ScrollView(new Text("123456", 0, 0), { scrollbar: "always" });
|
||||
const render = () => renderLayoutFrame(new HStack([scrollView], { align: "start" }), 6, 2, () => {});
|
||||
const always = render();
|
||||
assert.deepStrictEqual(visibleLines(always.lines), ["12345", "6"]);
|
||||
assert.strictEqual(always.root.children[0]?.rect.width, 6);
|
||||
assert.strictEqual(always.root.children[0]?.children[0]?.rect.width, 5);
|
||||
|
||||
scrollView.setScrollbar("hidden");
|
||||
assert.strictEqual(render().root.children[0]?.children[0]?.rect.width, 6);
|
||||
assert.strictEqual(scrollView.isScrollbarVisible, false);
|
||||
});
|
||||
|
||||
it("measures nested scroll content from constrained child geometry", () => {
|
||||
const inner = new ScrollView(new Text("1\n2\n3\n4\n5\n6", 0, 0));
|
||||
const outer = new ScrollView(new VStack([{ component: inner, basis: 2 }, new Text("tail", 0, 0)]));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue