Scrolling content is narrowed by the scrollbar width while the scrollbar is shown, so it no longer sits under the bar
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m55s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-18 07:29:58 +02:00
1 parent 91cdaf95cc
commit a65132bb5a
3 files changed
+35 -5

No files matched your search

+1 -1
View File
@@ -109,7 +109,7 @@ export class Menu extends Popup {
const size = this.list.measure();
const room = this.roomFor(this.scroll);
const scrolls = size.h > room;
this.scroll.minWidth = size.w + (scrolls ? Theme.scrollbarWidth + 4 : 0);
this.scroll.minWidth = size.w + (scrolls ? Theme.scrollbarWidth : 0);
this.scroll.minHeight = Math.min(size.h, room);
this.scroll.scrollTo(0);
this.refreshLayout();
+8 -3
View File
@@ -3,7 +3,11 @@ import { clamp } from '../math';
import { Theme } from '../Theme';
import { Widget } from '../Widget';
/** Vertical scroll of one content widget, which gets the container's width and its own height. */
/**
* Vertical scroll of one content widget, which gets the container's width and its own height.
* While the content scrolls it is narrowed by `Theme.scrollbarWidth`, so the bar sits beside it
* rather than on top of its right edge.
*/
export class ScrollContainer extends Widget {
private readonly maskShape = new Graphics();
private readonly scrollbar = new Graphics();
@@ -82,10 +86,11 @@ export class ScrollContainer extends Widget {
if (!this.content) return;
const size = this.content.measure();
const height = Math.max(size.h, this.h);
const width = Math.max(0, this.w - (size.h > this.h ? Theme.scrollbarWidth : 0));
// Content shorter than the viewport is pinned to the viewport height either way, so `resize`
// would see no change and skip the layout -- leaving rows added since stacked at the origin.
const unchanged = this.content.w === this.w && this.content.h === height;
this.content.resize(this.w, height);
const unchanged = this.content.w === width && this.content.h === height;
this.content.resize(width, height);
if (unchanged) this.content.layout();
this.scrollY = clamp(this.scrollY, 0, this.maxScroll);
this.applyScroll();
+26 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ScrollContainer, Spacer, VStack } from '../../src/ui';
import { ScrollContainer, Spacer, Theme, VStack } 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);
@@ -36,3 +36,28 @@ describe('a scroll container told its content changed', () => {
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);
});
});