lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
209 lines • 7.58 kB
JavaScript
import { ButtonClickHelper } from '../helpers/ButtonClickHelper.js';
import { Widget } from './Widget.js';
import { ClickState } from '../helpers/ClickState.js';
import { paintCircle } from '../helpers/paintCircle.js';
import { PropagationModel } from '../events/WidgetEvent.js';
import { FocusEvent } from '../events/FocusEvent.js';
import { BlurEvent } from '../events/BlurEvent.js';
/**
* A radio button widget; used for selecting one of many options. Uses a shared
* {@link Box} instance and expects the creation of multiple RadioButton
* instances.
*
* @typeParam V - The type stored in the {@link RadioButton#"variable"}; when a radio button is clicked, the value inside the variable has this type.
*
* @category Widget
*/
export class RadioButton extends Widget {
/**
* @param variable - The shared variable that radio buttons will save the value to when selected.
* @param value - The value that will be used to set the {@link RadioButton#"variable"} when the radio button is clicked
*/
constructor(variable, value, properties) {
var _a;
// Radio buttons need a clear background, have no children and don't
// propagate events
super(properties);
/** Horizontal offset. */
this.offsetX = 0;
/** Vertical offset. */
this.offsetY = 0;
/** Actual length after resolving layout. */
this.actualLength = 0;
/** Was the radio button selected in the last paint? */
this._wasSelected = false;
this._clickable = (_a = properties === null || properties === void 0 ? void 0 : properties.clickable) !== null && _a !== void 0 ? _a : true;
this.tabFocusable = true;
this.variable = variable;
this.value = value;
this.clickHelper = new ButtonClickHelper(this);
this.callback = this.handleChange.bind(this);
this._wasSelected = this.selected;
}
handleChange() {
if (this.selected !== this._wasSelected) {
this.markWholeAsDirty();
}
}
onThemeUpdated(property = null) {
super.onThemeUpdated(property);
if (property === null || property === 'radioButtonLength') {
this._layoutDirty = true;
this.markWholeAsDirty();
}
else if (property === 'backgroundGlowFill' ||
property === 'backgroundFill' ||
property === 'accentFill' ||
property === 'primaryFill' ||
property === 'radioButtonInnerPadding') {
this.markWholeAsDirty();
}
}
/**
* Select this radio button. Sets the value in
* {@link RadioButton#"variable"} to be {@link RadioButton#value}
*/
select() {
this.variable.value = this.value;
}
/**
* Is the radio button selected? Equivalent to checking if the value in the
* {@link RadioButton#"variable"} is strictly equal to the
* {@link RadioButton#value}
*/
get selected() {
return this.variable.value === this.value;
}
handleEvent(event) {
if (event.propagation !== PropagationModel.Trickling) {
if (event.isa(FocusEvent)) {
if (this.clickHelper.onFocusGrabbed(event.focusType)) {
this.markWholeAsDirty();
}
return this;
}
else if (event.isa(BlurEvent)) {
if (this.clickHelper.onFocusDropped(event.focusType)) {
this.markWholeAsDirty();
}
return this;
}
else {
return super.handleEvent(event);
}
}
const x = this.idealX + this.offsetX;
const y = this.idealY + this.offsetY;
const [wasClick, capture] = this.clickHelper.handleEvent(event, this.root, this._clickable, [x, x + this.actualLength, y, y + this.actualLength]);
// Select radio button if button was clicked
if (wasClick) {
this.select();
}
// Always mark as dirty if the click state changed (so glow colour takes
// effect). Toggle value if clicked
if (this.clickHelper.clickStateChanged) {
this.markWholeAsDirty();
}
return capture ? this : null;
}
handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) {
// Resolve width and height
const minLength = Math.min(this.radioButtonLength, maxWidth, maxHeight);
this.idealWidth = minLength;
this.idealHeight = minLength;
if (this.idealWidth < minWidth) {
this.idealWidth = minWidth;
}
if (this.idealHeight < minHeight) {
this.idealHeight = minHeight;
}
}
finalizeBounds() {
super.finalizeBounds();
// Center checkbox
this.actualLength = Math.min(this.radioButtonLength, this.width, this.height);
this.offsetX = (this.width - this.actualLength) / 2;
this.offsetY = (this.height - this.actualLength) / 2;
}
handlePainting(_dirtyRects) {
this._wasSelected = this.selected;
// Should we use glow colours? (background glow and accent)
const useGlow = this.clickHelper.clickState === ClickState.Hover ||
this.clickHelper.clickState === ClickState.Hold;
// Draw unchecked part of radio button
const ctx = this.viewport.context;
if (useGlow) {
ctx.fillStyle = this.backgroundGlowFill;
}
else {
ctx.fillStyle = this.backgroundFill;
}
const halfLength = this.actualLength / 2;
const radioX = this.offsetX + this.x + halfLength;
const radioY = this.offsetY + this.y + halfLength;
paintCircle(ctx, radioX, radioY, halfLength);
// Draw checked part of checkbox
if (this.selected) {
if (useGlow) {
ctx.fillStyle = this.accentFill;
}
else {
ctx.fillStyle = this.primaryFill;
}
const innerLength = this.actualLength - this.radioButtonInnerPadding * 2;
// Fall back to filling entire radio button if there isn't enough
// space for padding
if (innerLength <= 0) {
paintCircle(ctx, radioX, radioY, halfLength);
}
else {
const halfInnerLength = innerLength / 2;
paintCircle(ctx, radioX, radioY, halfInnerLength);
}
}
}
handleAttachment() {
this.variable.watch(this.callback);
}
handleDetachment() {
this.variable.unwatch(this.callback);
}
activate() {
super.activate();
this.clickHelper.reset();
}
/**
* Is the radio button clickable? True by default. Used for disabling the
* radio button without hiding it.
*/
get clickable() {
return this._clickable;
}
set clickable(clickable) {
if (this._clickable === clickable) {
return;
}
this._clickable = clickable;
this.clickHelper.reset();
this.markWholeAsDirty();
}
handlePreLayoutUpdate() {
super.handlePreLayoutUpdate();
this.clickHelper.doneProcessing();
}
}
RadioButton.autoXML = {
name: 'radio-button',
inputConfig: [
{
mode: 'value',
name: 'variable',
validator: 'box'
},
{
mode: 'value',
name: 'value'
}
]
};
//# sourceMappingURL=RadioButton.js.map