UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

460 lines 18 kB
import { View } from '../View.js'; import { Point, Rect, Size, interpolate } from '../geometry.js'; import { isMouseDragging, isMouseExit, isMouseClicked, isMousePressStart, isMousePressExit, isMousePressEnd, } from '../events/index.js'; const MIN = 5; export class Slider extends View { // styles #direction = 'horizontal'; #border = false; #buttons = false; // position of slider #range = [0, 0]; #value = 0; #step = 1; // focus #hasFocus = false; // mouse information #contentSize = Size.zero; #isPressingDecrease = false; #isPressingIncrease = false; #buttonTracking = 'off'; #isHoverSlider = false; #isHoverDecrease = false; #isHoverIncrease = false; #onChange; constructor(props) { super(props); this.#update(props); } update(props) { this.#update(props); super.update(props); } get border() { return this.#border; } set border(value) { if (value === this.#border) return; this.#border = value; this.invalidateSize(); } #update({ direction, border, buttons, range, value, step, onChange }) { this.#direction = direction ?? 'horizontal'; this.#border = border ?? false; this.#buttons = buttons ?? false; this.#range = range ?? [0, 1]; this.#step = step ? Math.max(step, 1) : 1; this.#onChange = onChange; this.#value = value ?? this.#range[0]; } get value() { return this.#value; } set value(value) { this.#value = value; if (value !== this.#value) { this.#value = value; this.invalidateRender(); } } naturalSize(_available) { // try to have enough room for every value const min = Math.max(MIN, Math.ceil((this.#range[1] - this.#range[0]) / this.#step)); if (this.#direction === 'horizontal') { const minWidth = min + 2 * (this.#buttons ? 3 : this.#border ? 1 : 0); if (this.#border) { //╭─┬── //│◃│█╶ //╰─┴── // ╭── // │█╶ // ╰── return new Size(minWidth, 3); } else { // [◃] // █╶─ return new Size(minWidth, 1); } } else { const minHeight = min + 2 * (this.#buttons && this.#border ? 3 : this.#buttons || this.#border ? 1 : 0); if (this.#border) { // ╭─╮ // │▵│ // ├─┤ ╭─╮ // │█│ │█│ // │╷│ │╷│ return new Size(3, minHeight); } else { // ▵ // █ █ // ╷ ╷ return new Size(1, minHeight); } } } legendItems() { return [ { key: this.#direction === 'horizontal' ? ['left', 'right'] : ['up', 'down'], label: 'Adjust', }, { key: 'home', label: 'Min' }, { key: 'end', label: 'Max' }, ]; } receiveKey(event) { const prev = this.#value; switch (event.name) { case 'right': case 'down': this.#value = Math.min(this.#range[1], this.#value + this.#step); break; case 'left': case 'up': this.#value = Math.max(this.#range[0], this.#value - this.#step); break; case 'home': this.#value = this.#range[0]; break; case 'end': this.#value = this.#range[1]; break; } if (this.#value !== prev) { this.#onChange?.(this.#value); } } receiveMouse(event) { if (this.#contentSize === undefined) { return; } const prev = this.#value; let pos, // the beginning of the slider area minSlider = 0, // the smaller dimension, ie the height of the horizontal slider // the bigger dimension, ie the width of the horizontal slider bigSize, // the end of the slider area maxSlider; if (this.#direction === 'horizontal') { pos = event.position.x; bigSize = this.#contentSize.width; } else { pos = event.position.y; bigSize = this.#contentSize.height; } maxSlider = bigSize - 1; if (this.#buttons) { if (this.#direction === 'horizontal') { //╭─┬ //│◃│ or [◃] //╰─┴ minSlider += 3; maxSlider -= 3; } else if (this.#border) { // ╭─╮ // │▵│ // ├─┤ minSlider += 3; maxSlider -= 3; } else { // ▵ minSlider += 1; maxSlider -= 1; } } else if (this.#border) { //╭ //│ or ╭─╮ //╰ minSlider += 1; maxSlider -= 1; } const isHoverDecrease = pos >= 0 && pos < minSlider; const isHoverIncrease = pos > maxSlider && pos < bigSize; const isMouseDown = event.name === 'mouse.button.down' || this.#buttonTracking === 'pressing'; const isDragging = (!isMouseDown && isMouseDragging(event)) || this.#buttonTracking === 'dragging'; let shouldUpdate = false; if (isDragging) { this.#isHoverSlider = true; this.#isHoverDecrease = false; this.#isHoverIncrease = false; } else if (isMouseExit(event)) { this.#isHoverSlider = false; this.#isHoverDecrease = false; this.#isHoverIncrease = false; } else { this.#isHoverSlider = pos >= minSlider && pos <= maxSlider; this.#isHoverDecrease = isHoverDecrease; this.#isHoverIncrease = isHoverIncrease; } if (isMouseDown && pos < minSlider) { if (isMousePressStart(event)) { this.#isPressingDecrease = true; this.#buttonTracking = 'pressing'; } else if (isMousePressExit(event)) { this.#isPressingDecrease = false; } if (isMouseClicked(event) && pos < minSlider) { this.#value = prev > this.#range[1] ? this.#range[1] : prev - this.#step; this.#buttonTracking = 'off'; shouldUpdate = true; } } else if (isMouseDown && pos > maxSlider) { if (isMousePressStart(event)) { this.#isPressingIncrease = true; this.#buttonTracking = 'pressing'; } else if (isMousePressExit(event)) { this.#isPressingIncrease = false; } if (isMouseClicked(event) && pos > maxSlider) { this.#value = prev > this.#range[1] ? this.#range[1] : prev + this.#step; this.#buttonTracking = 'off'; shouldUpdate = true; } } else if (isMousePressEnd(event)) { this.#buttonTracking = 'off'; } else if (isMouseDown || isDragging) { this.#buttonTracking = 'dragging'; this.#value = interpolate(pos, [minSlider, maxSlider], this.#range, true); shouldUpdate = true; if (~~this.#step === this.#step) { this.#value = this.#range[0] + Math.round((this.#value - this.#range[0]) / this.#step) * this.#step; } } if (shouldUpdate) { this.#value = Math.min(this.#range[1], Math.max(this.#range[0], this.#value)); if (this.#value !== prev) { this.#onChange?.(this.#value); } } } #borderChars(hasFocus) { return hasFocus ? BORDER_FOCUS : BORDER_DEFAULT; } #arrowChars() { return { up: this.#isHoverDecrease ? ARROWS_HOVER.up : ARROWS_DEFAULT.up, down: this.#isHoverIncrease ? ARROWS_HOVER.down : ARROWS_DEFAULT.down, left: this.#isHoverDecrease ? ARROWS_HOVER.left : ARROWS_DEFAULT.left, right: this.#isHoverIncrease ? ARROWS_HOVER.right : ARROWS_DEFAULT.right, }; } #renderHorizontal(viewport, sliderStyle, decreaseButtonStyle, increaseButtonStyle) { const hasFocus = this.#hasFocus; const hasBorder = this.#border && viewport.contentSize.height >= 3; const height = hasBorder ? 3 : 1; const marginX = this.#buttons ? 3 : hasBorder ? 1 : 0; const outerRect = new Rect([0, 0], [viewport.contentSize.width, height]); const innerRect = new Rect([marginX, 0], [viewport.contentSize.width - 2 * marginX, height]); viewport.registerMouse(['mouse.move', 'mouse.button.left'], outerRect); const border = this.#borderChars(hasFocus); if (this.#buttons) { const arrows = this.#arrowChars(); if (hasBorder) { viewport.write(`${border.topLeft}${border.horiz}${border.horizSepTop}`, Point.zero, decreaseButtonStyle); viewport.write(`${border.vert}${arrows.left}${border.vert}`, Point.zero.offset(0, 1), decreaseButtonStyle); viewport.write(`${border.bottomLeft}${border.horiz}${border.horizSepBottom}`, Point.zero.offset(0, 2), decreaseButtonStyle); const rx = viewport.contentSize.width - 3; viewport.write(`${border.horizSepTop}${border.horiz}${border.topRight}`, Point.zero.offset(rx, 0), increaseButtonStyle); viewport.write(`${border.vert}${arrows.right}${border.vert}`, Point.zero.offset(rx, 1), increaseButtonStyle); viewport.write(`${border.horizSepBottom}${border.horiz}${border.bottomRight}`, Point.zero.offset(rx, 2), increaseButtonStyle); } else { const [bl, br] = hasFocus ? BRACKETS_FOCUS : BRACKETS_DEFAULT; viewport.write(`${bl}${arrows.left}${br}`, Point.zero, decreaseButtonStyle); viewport.write(`${bl}${arrows.right}${br}`, Point.zero.offset(viewport.contentSize.width - 3, 0), increaseButtonStyle); } } else if (hasBorder) { viewport.write(border.topLeft, Point.zero.offset(0, 0), sliderStyle); viewport.write(border.vert, Point.zero.offset(0, 1), sliderStyle); viewport.write(border.bottomLeft, Point.zero.offset(0, 2), sliderStyle); const rx = viewport.contentSize.width - 1; viewport.write(border.topRight, Point.zero.offset(rx, 0), sliderStyle); viewport.write(border.vert, Point.zero.offset(rx, 1), sliderStyle); viewport.write(border.bottomRight, Point.zero.offset(rx, 2), sliderStyle); } if (hasBorder) { // Draw the top and bottom border rails for (let x = innerRect.minX(); x < innerRect.maxX(); x++) { viewport.write(border.horiz, Point.zero.offset(x, 0), sliderStyle); viewport.write(border.horiz, Point.zero.offset(x, 2), sliderStyle); } } const min = innerRect.minX(), max = innerRect.maxX(); const position = Math.round(interpolate(this.#value, this.#range, [min, max - 1], true)); innerRect.forEachPoint(pt => { let char; if (height === 1 || pt.y === 1) { if (pt.x === position) { char = BAR.fill; } else if (pt.x === position + 1) { char = BAR.right; } else if (pt.x === position - 1) { char = BAR.left; } else { char = BAR.horiz; } } else { // top/bottom border rows already drawn above return; } viewport.write(char, pt, sliderStyle); }); } #renderVertical(viewport, sliderStyle, decreaseButtonStyle, increaseButtonStyle) { const hasFocus = this.#hasFocus; const hasBorder = this.#border && viewport.contentSize.width >= 3; const width = hasBorder ? 3 : 1; const marginY = this.#buttons && hasBorder ? 3 : this.#buttons || hasBorder ? 1 : 0; const outerRect = new Rect([0, 0], [width, viewport.contentSize.height]); const innerRect = new Rect([0, marginY], [width, viewport.contentSize.height - 2 * marginY]); viewport.registerMouse(['mouse.move', 'mouse.button.left'], outerRect); const border = this.#borderChars(hasFocus); if (this.#buttons) { const arrows = this.#arrowChars(); if (hasBorder) { viewport.write(`${border.topLeft}${border.horiz}${border.topRight}`, Point.zero, decreaseButtonStyle); viewport.write(`${border.vert}${arrows.up}${border.vert}`, Point.zero.offset(0, 1), decreaseButtonStyle); viewport.write(`${border.vertSepLeft}${border.horiz}${border.vertSepRight}`, Point.zero.offset(0, 2), decreaseButtonStyle); const by = viewport.contentSize.height - 1; viewport.write(`${border.bottomLeft}${border.horiz}${border.bottomRight}`, Point.zero.offset(0, by), increaseButtonStyle); viewport.write(`${border.vert}${arrows.down}${border.vert}`, Point.zero.offset(0, by - 1), increaseButtonStyle); viewport.write(`${border.vertSepLeft}${border.horiz}${border.vertSepRight}`, Point.zero.offset(0, by - 2), increaseButtonStyle); } else { viewport.write(arrows.up, Point.zero, decreaseButtonStyle); viewport.write(arrows.down, Point.zero.offset(0, viewport.contentSize.height - 1), increaseButtonStyle); } } else if (hasBorder) { viewport.write(`${border.topLeft}${border.horiz}${border.topRight}`, Point.zero, sliderStyle); viewport.write(`${border.bottomLeft}${border.horiz}${border.bottomRight}`, Point.zero.offset(0, viewport.contentSize.height - 1), sliderStyle); } if (hasBorder) { // Draw the left and right border rails for (let y = innerRect.minY(); y < innerRect.maxY(); y++) { viewport.write(border.vert, Point.zero.offset(0, y), sliderStyle); viewport.write(border.vert, Point.zero.offset(2, y), sliderStyle); } } const min = innerRect.minY(), max = innerRect.maxY(); const position = Math.round(interpolate(this.#value, this.#range, [min, max - 1], true)); innerRect.forEachPoint(pt => { let char; if (width === 1 || pt.x === 1) { if (pt.y === position) { char = BAR.fill; } else if (pt.y === position + 1) { char = BAR.vertBelow; } else if (pt.y === position - 1) { char = BAR.vertAbove; } else { char = BAR.vert; } } else { // left/right border rails already drawn above return; } viewport.write(char, pt, sliderStyle); }); } render(viewport) { if (viewport.isEmpty) { return; } const hasFocus = viewport.registerFocus({ isDefault: false }); this.#hasFocus = hasFocus; this.#contentSize = viewport.contentSize; const sliderStyle = this.purpose.ui({ isHover: this.#isHoverSlider || hasFocus, }); const decreaseButtonStyle = this.purpose.ui({ isPressed: this.#isPressingDecrease, isHover: this.#isHoverDecrease, }); const increaseButtonStyle = this.purpose.ui({ isPressed: this.#isPressingIncrease, isHover: this.#isHoverIncrease, }); if (this.#direction === 'horizontal') { this.#renderHorizontal(viewport, sliderStyle, decreaseButtonStyle, increaseButtonStyle); } else { this.#renderVertical(viewport, sliderStyle, decreaseButtonStyle, increaseButtonStyle); } } } const BRACKETS_DEFAULT = ['[', ']']; const BRACKETS_FOCUS = ['⟦', '⟧']; // true => hover, false => default const ARROWS_DEFAULT = { up: '▵', down: '▿', left: '◃', right: '▹' }; const ARROWS_HOVER = { up: '▴', down: '▾', left: '◂', right: '▸' }; const BAR = { left: '╴', right: '╶', horiz: '─', fill: '█', vert: '│', vertAbove: '╵', vertBelow: '╷', }; // true => focus, false => default const BORDER_DEFAULT = { topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯', horiz: '─', vert: '│', vertSepLeft: '├', vertSepRight: '┤', horizSepTop: '┬', horizSepBottom: '┴', }; const BORDER_FOCUS = { topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', horiz: '═', vert: '║', vertSepLeft: '╠', vertSepRight: '╣', horizSepTop: '╦', horizSepBottom: '╩', }; //# sourceMappingURL=Slider.js.map