Files
feuerwerk/src/ui/widgets/Label.ts
T
rootandClaude Fable 5.1 1eb28314cf Refactoring: one file per node, consistent comments, shared helpers, split large modules
- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with
  shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry
  separated, writeTyped() replaces duplicated typed writes
- Comments: one style (JSDoc on non-obvious exports, // for decisions and
  traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars
- Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts,
  commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code
  removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe,
  Input.axis, setMode, titleText, World.worldPosition, unused ctor params)
- Editor: Inspector split into inspector/ sections, App.ts into Editor.ts +
  debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared
  "Add collider" action, unified type labels/swatches
- UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers,
  Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes
- Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib
  (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections
- Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 08:37:22 +02:00

80 lines
2.7 KiB
TypeScript

import { CanvasTextMetrics, Text } from 'pixi.js';
import { centreY } from '../math';
import { Theme, textStyle, type TextStyleToken } from '../Theme';
import { type Size, Widget } from '../Widget';
export interface LabelOptions {
style?: TextStyleToken;
align?: 'left' | 'center' | 'right';
/** Horizontal padding inside the label bounds. */
padding?: number;
}
/** Single-line text that truncates with an ellipsis when narrower than its content. */
export class Label extends Widget {
private readonly textNode: Text;
private fullText: string;
private readonly align: NonNullable<LabelOptions['align']>;
private readonly padding: number;
constructor(text = '', options: LabelOptions = {}) {
super();
this.eventMode = 'none';
this.fullText = text;
this.align = options.align ?? 'left';
this.padding = options.padding ?? 0;
this.textNode = new Text({ text, style: textStyle(options.style ?? 'body') });
this.addChild(this.textNode);
this.minHeight = Theme.rowHeight;
}
/** The full text, not the truncated version on screen. */
get text(): string {
return this.fullText;
}
setText(text: string): void {
if (text === this.fullText) return;
this.fullText = text;
this.invalidate();
}
/** Repaints only; the measured size may change, so the parent must relayout when that matters. */
setStyle(token: TextStyleToken): void {
this.textNode.style = textStyle(token);
this.invalidate();
}
override measure(): Size {
const metrics = CanvasTextMetrics.measureText(this.fullText, this.textNode.style);
return {
w: Math.max(this.minWidth, Math.ceil(metrics.width) + this.padding * 2),
h: Math.max(this.minHeight, Math.ceil(metrics.height)),
};
}
override redraw(): void {
const available = this.w - this.padding * 2;
this.textNode.text = truncate(this.fullText, this.textNode.style, available);
const tw = this.textNode.width;
let x = this.padding;
if (this.align === 'center') x = (this.w - tw) / 2;
else if (this.align === 'right') x = this.w - this.padding - tw;
this.textNode.position.set(Math.round(x), centreY(this.h, this.textNode.height));
}
}
/** Cuts `text` to fit `maxWidth` in the given style, appending "…" when shortened. */
export function truncate(text: string, style: Text['style'], maxWidth: number): string {
if (maxWidth <= 0) return '';
if (CanvasTextMetrics.measureText(text, style).width <= maxWidth) return text;
let lo = 0;
let hi = text.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (CanvasTextMetrics.measureText(text.slice(0, mid) + '…', style).width <= maxWidth) lo = mid;
else hi = mid - 1;
}
return lo === 0 ? '' : text.slice(0, lo) + '…';
}