import { type FederatedPointerEvent, Graphics } from 'pixi.js'; import { clamp, centreY } from '../math'; import { Theme } from '../Theme'; import { Widget } from '../Widget'; const TRACK_HEIGHT = 4; const KNOB_RADIUS = 6; /** A value picked by dragging along a track. Reports every step of the drag, not just the end. */ export class Slider extends Widget { private readonly bar = new Graphics(); private dragging = false; onChange?: (value: number) => void; constructor( private value = 0, private min = 0, private max = 1, width = 160, ) { super(); this.focusable = true; this.cursor = Theme.hoverCursor; this.minWidth = width; this.minHeight = Theme.controlHeight; this.addChild(this.bar); this.trackHover(); this.on('pointerdown', (e: FederatedPointerEvent) => { this.dragging = true; this.pick(e); e.stopPropagation(); }); this.on('globalpointermove', (e: FederatedPointerEvent) => { if (this.dragging) this.pick(e); }); this.onPointerRelease(() => { this.dragging = false; this.invalidate(); }); } /** Does not fire `onChange`, unlike a drag. */ setValue(value: number): void { const next = clamp(value, this.min, this.max); if (next === this.value) return; this.value = next; this.invalidate(); } setRange(min: number, max: number): void { this.min = min; this.max = max > min ? max : min + 1; this.setValue(this.value); this.invalidate(); } get currentValue(): number { return this.value; } private pick(e: FederatedPointerEvent): void { const span = Math.max(1, this.w - KNOB_RADIUS * 2); const at = clamp((e.global.x - this.getGlobalPosition().x - KNOB_RADIUS) / span, 0, 1); const next = this.min + at * (this.max - this.min); if (next === this.value) return; this.value = next; this.invalidate(); this.onChange?.(next); } override redraw(): void { const c = Theme.colors; const y = centreY(this.h, TRACK_HEIGHT); const span = Math.max(1, this.w - KNOB_RADIUS * 2); const at = KNOB_RADIUS + ((this.value - this.min) / (this.max - this.min)) * span; this.bar.clear(); this.bar.rect(KNOB_RADIUS, y, span, TRACK_HEIGHT).fill(c.control); this.bar.rect(KNOB_RADIUS, y, at - KNOB_RADIUS, TRACK_HEIGHT).fill(c.accent); this.bar .circle(at, this.h / 2, KNOB_RADIUS) .fill(this.hovered || this.dragging ? c.controlHover : c.control) .stroke({ width: 1, color: this.focused ? c.accent : c.border }); } }