Add features to NumberInput widget; refactor widgets and tool options (#311)

Closes #310

* Add features to NumberInput widget; refactor widgets and tool options

* Fix swap/reset working colors using @click instead of :action
This commit is contained in:
Keavon Chambers 2021-07-27 18:28:21 -07:00
parent 5c7fe243bf
commit 12fc330952
14 changed files with 330 additions and 172 deletions

View File

@ -34,10 +34,10 @@
<Separator :type="SeparatorType.Unrelated" />
<RadioInput v-model:index="viewModeIndex">
<IconButton :icon="'ViewModeNormal'" :size="24" title="View Mode: Normal" />
<IconButton :icon="'ViewModeOutline'" :size="24" title="View Mode: Outline" />
<IconButton :icon="'ViewModePixels'" :size="24" title="View Mode: Pixels" />
<RadioInput @update:index="viewModeChanged" v-model:index="viewModeIndex">
<IconButton :action="() => _" :icon="'ViewModeNormal'" :size="24" title="View Mode: Normal" />
<IconButton :action="() => _" :icon="'ViewModeOutline'" :size="24" title="View Mode: Outline" />
<IconButton :action="() => _" :icon="'ViewModePixels'" :size="24" title="View Mode: Pixels" />
</RadioInput>
<PopoverButton>
<h3>View Mode</h3>
@ -46,17 +46,27 @@
<Separator :type="SeparatorType.Section" />
<NumberInput :callback="setRotation" :initialValue="0" :step="15" :unit="`°`" :updateOnCallback="false" ref="rotation" />
<NumberInput @update:value="setRotation" v-model:value="documentRotation" :step="15" :unit="`°`" ref="rotation" />
<Separator :type="SeparatorType.Section" />
<IconButton :icon="'ZoomIn'" :size="24" title="Zoom In" @click="this.$refs.zoom.onIncrement(1)" />
<IconButton :icon="'ZoomOut'" :size="24" title="Zoom Out" @click="this.$refs.zoom.onIncrement(-1)" />
<IconButton :icon="'ZoomReset'" :size="24" title="Zoom to 100%" @click="this.$refs.zoom.updateValue(100)" />
<IconButton :action="() => this.$refs.zoom.onIncrement(IncrementDirection.Increase)" :icon="'ZoomIn'" :size="24" title="Zoom In" />
<IconButton :action="() => this.$refs.zoom.onIncrement(IncrementDirection.Decrease)" :icon="'ZoomOut'" :size="24" title="Zoom Out" />
<IconButton :action="() => this.$refs.zoom.updateValue(100)" :icon="'ZoomReset'" :size="24" title="Zoom to 100%" />
<Separator :type="SeparatorType.Related" />
<NumberInput :callback="setZoom" :initialValue="100" :min="0.001" :increaseMultiplier="1.25" :decreaseMultiplier="0.8" :unit="`%`" :updateOnCallback="false" ref="zoom" />
<NumberInput
v-model:value="documentZoom"
@update:value="setZoom"
:min="0.000001"
:max="1000000"
:step="1.25"
:stepIsMultiplier="true"
:unit="`%`"
:displayDecimalPlaces="4"
ref="zoom"
/>
</div>
</LayoutRow>
<LayoutRow :class="'shelf-and-viewport'">
@ -97,8 +107,8 @@
<div class="working-colors">
<SwatchPairInput />
<div class="swap-and-reset">
<IconButton @click="swapWorkingColors" :icon="'Swap'" title="Swap (Shift+X)" :size="16" />
<IconButton @click="resetWorkingColors" :icon="'ResetColors'" title="Reset (Ctrl+Shift+X)" :size="16" />
<IconButton :action="swapWorkingColors" :icon="'Swap'" title="Swap (Shift+X)" :size="16" />
<IconButton :action="resetWorkingColors" :icon="'ResetColors'" title="Reset (Ctrl+Shift+X)" :size="16" />
</div>
</div>
</LayoutCol>
@ -202,20 +212,23 @@
<script lang="ts">
import { defineComponent } from "vue";
import { makeModifiersBitfield } from "@/utilities/input";
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import ShelfItemInput from "@/components/widgets/inputs/ShelfItemInput.vue";
import Separator, { SeparatorDirection, SeparatorType } from "@/components/widgets/separators/Separator.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import PersistentScrollbar, { ScrollbarDirection } from "@/components/widgets/scrollbars/PersistentScrollbar.vue";
import CanvasRuler, { RulerDirection } from "@/components/widgets/rulers/CanvasRuler.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import NumberInput, { IncrementDirection } from "@/components/widgets/inputs/NumberInput.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import ToolOptions from "@/components/widgets/options/ToolOptions.vue";
@ -322,16 +335,14 @@ export default defineComponent({
registerResponseHandler(ResponseType.SetCanvasZoom, (responseData: Response) => {
const updateData = responseData as SetCanvasZoom;
if (updateData) {
const zoomWidget = this.$refs.zoom as typeof NumberInput;
zoomWidget.setValue(updateData.new_zoom * 100);
this.documentZoom = updateData.new_zoom * 100;
}
});
registerResponseHandler(ResponseType.SetCanvasRotation, (responseData: Response) => {
const updateData = responseData as SetCanvasRotation;
if (updateData) {
const rotationWidget = this.$refs.rotation as typeof NumberInput;
const newRotation = updateData.new_radians * (180 / Math.PI);
rotationWidget.setValue((360 + (newRotation % 360)) % 360);
this.documentRotation = (360 + (newRotation % 360)) % 360;
}
});
@ -341,8 +352,6 @@ export default defineComponent({
window.addEventListener("resize", () => this.viewportResize());
window.addEventListener("DOMContentLoaded", () => this.viewportResize());
this.$watch("viewModeIndex", this.viewModeChanged);
},
data() {
return {
@ -356,6 +365,9 @@ export default defineComponent({
snappingEnabled: true,
gridEnabled: true,
overlaysEnabled: true,
documentRotation: 0,
documentZoom: 100,
IncrementDirection,
MenuDirection,
SeparatorDirection,
ScrollbarDirection,

View File

@ -5,7 +5,7 @@
<Separator :type="SeparatorType.Related" />
<NumberInput v-model:value="opacity" :min="0" :max="100" :step="1" :unit="`%`" />
<NumberInput v-model:value="opacity" :min="0" :max="100" :unit="`%`" :displayDecimalPlaces="2" />
<Separator :type="SeparatorType.Related" />
@ -18,7 +18,12 @@
<LayoutCol :class="'list'" @click="deselectAllLayers">
<div class="layer-row" v-for="layer in layers" :key="layer.path">
<div class="layer-visibility">
<IconButton :icon="layer.visible ? 'EyeVisible' : 'EyeHidden'" @click.stop="toggleLayerVisibility(layer.path)" :size="24" :title="layer.visible ? 'Visible' : 'Hidden'" />
<IconButton
:action="(e) => (toggleLayerVisibility(layer.path), e.stopPropagation())"
:icon="layer.visible ? 'EyeVisible' : 'EyeHidden'"
:size="24"
:title="layer.visible ? 'Visible' : 'Hidden'"
/>
</div>
<div
class="layer"
@ -111,10 +116,13 @@
<script lang="ts">
import { defineComponent } from "vue";
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, LayerPanelEntry } from "@/utilities/response-handler";
import { SeparatorType } from "@/components/widgets/widgets";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import Separator, { SeparatorType } from "@/components/widgets/separators/Separator.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";

View File

@ -1,5 +1,5 @@
<template>
<button class="icon-button" :class="`size-${String(size)}`" @click="onClick">
<button class="icon-button" :class="`size-${String(size)}`" @click="action">
<IconLabel :icon="icon" />
</button>
</template>
@ -57,10 +57,10 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
action: { type: Function, required: true },
icon: { type: String, required: true },
size: { type: Number, required: true },
gapAfter: { type: Boolean, default: false },
onClick: Function,
},
components: { IconLabel },
});

View File

@ -1,6 +1,6 @@
<template>
<div class="popover-button">
<IconButton :icon="icon" :size="16" @click="clickButton" data-hover-menu-spawner />
<IconButton :action="handleClick" :icon="icon" :size="16" data-hover-menu-spawner />
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Bottom" ref="floatingMenu">
<slot></slot>
</FloatingMenu>
@ -63,11 +63,14 @@ export default defineComponent({
IconButton,
},
props: {
action: { type: Function, required: false },
icon: { type: String, default: PopoverButtonIcon.DropdownArrow },
},
methods: {
clickButton() {
handleClick() {
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
if (this.action) this.action();
},
},
data() {

View File

@ -131,9 +131,7 @@ const enum ColorPickerState {
export default defineComponent({
components: {},
props: {
color: {
type: Object,
},
color: { type: Object, required: true },
},
data() {
return {
@ -168,16 +166,13 @@ export default defineComponent({
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
},
removeEvents() {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
getRef<T>(name: string) {
return this.$refs[name] as T;
},
onPointerDown(e: PointerEvent) {
if (!(e.currentTarget instanceof Element)) return;
const picker = e.currentTarget.getAttribute("data-picker-action");
@ -200,7 +195,6 @@ export default defineComponent({
this.onPointerMove(e);
}
},
onPointerMove(e: PointerEvent) {
const { colorPicker } = this.$data._;
@ -217,14 +211,12 @@ export default defineComponent({
this.$emit("update:color", hsvToRgb(colorPicker.color));
}
},
onPointerUp() {
if (this.state !== ColorPickerState.Idle) {
this.state = ColorPickerState.Idle;
this.removeEvents();
}
},
updateRects() {
const { colorPicker } = this.$data._;
@ -249,7 +241,6 @@ export default defineComponent({
colorPicker.opacity.rect.left = opacity.left;
colorPicker.opacity.rect.top = opacity.top;
},
setSaturationPosition(x: number, y: number) {
const { colorPicker } = this.$data._;
const saturationCursor = this.getRef<HTMLDivElement>("saturationCursor");
@ -258,7 +249,6 @@ export default defineComponent({
colorPicker.color.s = saturationPosition[0] / colorPicker.saturation.rect.width;
colorPicker.color.v = (1 - saturationPosition[1] / colorPicker.saturation.rect.height) * 255;
},
setHuePosition(y: number) {
const { colorPicker } = this.$data._;
const hueCursor = this.getRef<HTMLDivElement>("hueCursor");
@ -266,7 +256,6 @@ export default defineComponent({
hueCursor.style.transform = `translateY(${huePosition}px)`;
colorPicker.color.h = clamp(1 - huePosition / colorPicker.hue.rect.height);
},
setOpacityPosition(y: number) {
const { colorPicker } = this.$data._;
const opacityCursor = this.getRef<HTMLDivElement>("opacityCursor");
@ -274,7 +263,6 @@ export default defineComponent({
opacityCursor.style.transform = `translateY(${opacityPosition}px)`;
colorPicker.color.a = clamp(1 - opacityPosition / colorPicker.opacity.rect.height);
},
updateHue() {
const { colorPicker } = this.$data._;
let color = hsvToRgb({ h: colorPicker.color.h, s: 1, v: 255, a: 1 });
@ -282,7 +270,6 @@ export default defineComponent({
color = hsvToRgb(colorPicker.color);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${color.r}, ${color.g}, ${color.b})`);
},
updateColor() {
if (this.state !== ColorPickerState.Idle) return;
const { color } = this;

View File

@ -120,8 +120,10 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { keyboardLockApiSupported } from "@/utilities/fullscreen";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import Separator, { SeparatorDirection, SeparatorType } from "@/components/widgets/separators/Separator.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";

View File

@ -112,7 +112,7 @@ export default defineComponent({
selectedIndex(newSelectedIndex: number) {
const entries = this.menuEntries.flat();
if (newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
if (!Number.isNaN(newSelectedIndex) && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
this.activeEntry = entries[newSelectedIndex];
} else {
this.activeEntry = { label: "-" };

View File

@ -1,8 +1,8 @@
<template>
<div class="number-input">
<button class="arrow left" @click="onIncrement(-1)"></button>
<button class="arrow right" @click="onIncrement(1)"></button>
<input type="text" spellcheck="false" v-model="text" @change="updateText($event.target.value)" /> />
<div class="number-input" :class="{ disabled }">
<input type="text" spellcheck="false" v-model="text" @change="onTextChanged()" @keydown.esc="onCancelTextChange" ref="input" :disabled="disabled" />
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement(IncrementDirection.Decrease)"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement(IncrementDirection.Increase)"></button>
</div>
</template>
@ -17,12 +17,12 @@
input {
width: calc(100% - 8px);
margin: 0 4px;
line-height: 18px;
margin: 3px 4px;
outline: none;
border: none;
background: none;
padding: 0;
line-height: 24px;
color: var(--color-e-nearwhite);
font-size: inherit;
text-align: center;
@ -31,6 +31,17 @@
&::selection {
background: var(--color-accent);
}
&:focus {
text-align: left;
width: calc(100% - 16px);
margin-left: 8px;
margin-right: 8px;
& ~ .arrow {
display: none;
}
}
}
&:not(:hover) .arrow {
@ -89,70 +100,136 @@
}
}
}
&.disabled {
background: var(--color-2-mildblack);
input {
color: var(--color-8-uppergray);
}
.arrow {
display: none;
}
}
}
</style>
<script lang="ts">
import { defineComponent } from "vue";
export enum IncrementDirection {
Decrease = "Decrease",
Increase = "Increase",
}
export default defineComponent({
components: {},
props: {
initialValue: { type: Number, default: 0 },
unit: { type: String, default: "" },
step: { type: Number, default: 1 },
displayDecimalPlaces: { type: Number, default: 3 },
increaseMultiplier: { type: Number, default: null },
decreaseMultiplier: { type: Number, default: null },
value: { type: Number, required: true },
min: { type: Number, required: false },
max: { type: Number, required: false },
callback: { type: Function, required: false },
updateOnCallback: { type: Boolean, default: true },
step: { type: Number, default: 1 },
stepIsMultiplier: { type: Boolean, default: false },
isInteger: { type: Boolean, default: false },
unit: { type: String, default: "" },
unitIsHiddenWhenEditing: { type: Boolean, default: true },
displayDecimalPlaces: { type: Number, default: 3 },
disabled: { type: Boolean, default: false },
},
data() {
return {
value: this.initialValue,
text: this.initialValue.toString() + this.unit,
text: `${this.value}${this.unit}`,
editing: false,
IncrementDirection,
};
},
methods: {
onIncrement(direction: number) {
if (direction === 1 && this.increaseMultiplier) this.updateValue(this.value * this.increaseMultiplier, true);
else if (direction === -1 && this.decreaseMultiplier) this.updateValue(this.value * this.decreaseMultiplier, true);
else this.updateValue(this.value + this.step * direction, true);
},
onTextFocused() {
if (Number.isNaN(this.value)) this.text = "";
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
else this.text = `${this.value}${this.unit}`;
updateText(newText: string) {
const newValue = parseInt(newText, 10);
this.updateValue(newValue, true);
this.editing = true;
const inputElement = this.$refs.input as HTMLInputElement;
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
},
// Called only when `value` is changed from the <input> element via user input and committed, either with the
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
onTextChanged() {
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
if (!this.editing) return;
clampValue(newValue: number, resetOnClamp: boolean) {
if (!Number.isFinite(newValue)) return this.value;
let result = newValue;
if (Number.isFinite(this.min) && typeof this.min === "number") {
if (resetOnClamp && newValue < this.min) return this.value;
result = Math.max(result, this.min);
}
if (Number.isFinite(this.max) && typeof this.max === "number") {
if (resetOnClamp && newValue > this.max) return this.value;
result = Math.min(result, this.max);
}
return result;
const newValue = parseFloat(this.text);
this.updateValue(newValue);
this.editing = false;
const inputElement = this.$refs.input as HTMLElement;
inputElement.blur();
},
setValue(newValue: number) {
this.value = newValue;
onCancelTextChange() {
this.updateValue(NaN);
this.editing = false;
const inputElement = this.$refs.input as HTMLElement;
inputElement.blur();
},
onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return;
if (this.stepIsMultiplier) {
const directionMultiplier = direction === IncrementDirection.Increase ? this.step : 1 / this.step;
this.updateValue(this.value * directionMultiplier);
} else {
const directionAddend = direction === IncrementDirection.Increase ? this.step : -this.step;
this.updateValue(this.value + directionAddend);
}
},
updateValue(newValue: number) {
let sanitized = newValue;
const invalid = Number.isNaN(newValue);
if (invalid) sanitized = this.value;
if (this.isInteger) sanitized = Math.round(sanitized);
if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min);
if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max);
if (!invalid) this.$emit("update:value", sanitized);
const roundingPower = 10 ** this.displayDecimalPlaces;
this.text = `${Math.round(this.value * roundingPower) / roundingPower}${this.unit}`;
const displayValue = Math.round(sanitized * roundingPower) / roundingPower;
this.text = `${displayValue}${this.unit}`;
},
updateValue(inValue: number, resetOnClamp: boolean) {
const newValue = this.clampValue(inValue, resetOnClamp);
},
watch: {
// Called only when `value` is changed from outside this component (with v-model)
value(newValue: number) {
if (Number.isNaN(newValue)) {
this.text = "-";
return;
}
if (this.callback) this.callback(newValue);
let sanitized = newValue;
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
if (this.updateOnCallback) this.setValue(newValue);
const roundingPower = 10 ** this.displayDecimalPlaces;
const displayValue = Math.round(sanitized * roundingPower) / roundingPower;
this.text = `${displayValue}${this.unit}`;
},
},
mounted() {
const inputElement = this.$refs.input as HTMLInputElement;
inputElement.addEventListener("focus", this.onTextFocused);
inputElement.addEventListener("blur", this.onTextChanged);
},
beforeUnmount() {
const inputElement = this.$refs.input as HTMLInputElement;
inputElement.removeEventListener("focus", this.onTextFocused);
inputElement.removeEventListener("blur", this.onTextChanged);
},
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="shelf-item-input" :class="{ active: active }">
<IconButton :icon="icon" :size="32" />
<IconButton :action="() => _" :icon="icon" :size="32" />
</div>
</template>

View File

@ -1,13 +1,25 @@
<template>
<div class="tool-options">
<template v-for="(option, index) in optionsMap.get(activeTool) || []" :key="index">
<IconButton v-if="option.kind === 'IconButton'" :icon="option.icon" :size="24" :title="option.title" :onClick="() => sendToolMessage(option.message)" />
<Separator v-if="option.kind === 'Separator'" :type="option.type" />
<PopoverButton v-if="option.kind === 'PopoverButton'">
<template v-for="(option, index) in toolOptions[activeTool] || []" :key="index">
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
<IconButton
v-if="option.kind === 'IconButton'"
:action="() => (option.message && sendToolMessage(option.message), option.callback && option.callback())"
:title="option.tooltip"
v-bind="option.props"
/>
<PopoverButton v-if="option.kind === 'PopoverButton'" :title="option.tooltip" :action="() => option.callback && option.callback()" v-bind="option.props">
<h3>{{ option.title }}</h3>
<p>{{ option.placeholderText }}</p>
<p>{{ option.text }}</p>
</PopoverButton>
<NumberInput v-if="option.kind === 'NumberInput'" :callback="option.callback" :initialValue="option.initial" :step="option.step" :min="option.min" :updateOnCallback="true" />
<NumberInput
v-if="option.kind === 'NumberInput'"
v-model:value="option.props.value"
@update:value="() => option.callback && option.callback()"
:title="option.tooltip"
v-bind="option.props"
/>
<Separator v-if="option.kind === 'Separator'" :type="option.type" />
</template>
</div>
</template>
@ -23,43 +35,15 @@
<script lang="ts">
import { defineComponent } from "vue";
import Separator, { SeparatorType } from "@/components/widgets/separators/Separator.vue";
import { WidgetRow, SeparatorType } from "@/components/widgets/widgets";
import Separator from "@/components/widgets/separators/Separator.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
const wasm = import("@/../wasm/pkg");
type ToolOptionsList = Array<ToolOptions>;
type ToolOptionsMap = Map<string, ToolOptionsList>;
type ToolOptions = IconButtonOption | SeparatorOption | PopoverButtonOption | NumberInputOption;
interface IconButtonOption {
kind: "IconButton";
icon: string;
title: string;
message?: string | object;
}
interface SeparatorOption {
kind: "Separator";
type: SeparatorType;
}
interface PopoverButtonOption {
kind: "PopoverButton";
title: string;
placeholderText: string;
}
interface NumberInputOption {
kind: "NumberInput";
initial: number;
step: number;
min?: number;
callback?: Function;
}
export default defineComponent({
props: {
activeTool: { type: String },
@ -75,59 +59,75 @@ export default defineComponent({
// This is a placeholder call, using the Shape tool as an example
set_tool_options(this.$props.activeTool || "", { Shape: { shape_type: { Polygon: { vertices: newValue } } } });
},
async sendToolMessage(message?: string) {
if (message) {
const { send_tool_message } = await wasm;
send_tool_message(this.$props.activeTool || "", message);
}
async sendToolMessage(message: string) {
const { send_tool_message } = await wasm;
send_tool_message(this.$props.activeTool || "", message);
},
},
data() {
const optionsMap: ToolOptionsMap = new Map([
[
"Select",
[
{ kind: "IconButton", icon: "AlignLeft", title: "Align Left", message: { Align: ["X", "Min"] } },
{ kind: "IconButton", icon: "AlignHorizontalCenter", title: "Align Horizontal Center", message: { Align: ["X", "Center"] } },
{ kind: "IconButton", icon: "AlignRight", title: "Align Right", message: { Align: ["X", "Max"] } },
const toolOptions: Record<string, WidgetRow> = {
Select: [
{ kind: "IconButton", message: { Align: ["X", "Min"] }, tooltip: "Align Left", props: { icon: "AlignLeft", size: 24 } },
{ kind: "IconButton", message: { Align: ["X", "Center"] }, tooltip: "Align Horizontal Center", props: { icon: "AlignHorizontalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: ["X", "Max"] }, tooltip: "Align Right", props: { icon: "AlignRight", size: 24 } },
{ kind: "Separator", type: SeparatorType.Unrelated },
{ kind: "Separator", props: { type: SeparatorType.Unrelated } },
{ kind: "IconButton", icon: "AlignTop", title: "Align Top", message: { Align: ["Y", "Min"] } },
{ kind: "IconButton", icon: "AlignVerticalCenter", title: "Align Vertical Center", message: { Align: ["Y", "Center"] } },
{ kind: "IconButton", icon: "AlignBottom", title: "Align Bottom", message: { Align: ["Y", "Max"] } },
{ kind: "IconButton", message: { Align: ["Y", "Min"] }, tooltip: "Align Top", props: { icon: "AlignTop", size: 24 } },
{ kind: "IconButton", message: { Align: ["Y", "Center"] }, tooltip: "Align Vertical Center", props: { icon: "AlignVerticalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: ["Y", "Max"] }, tooltip: "Align Bottom", props: { icon: "AlignBottom", size: 24 } },
{ kind: "Separator", type: SeparatorType.Related },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "PopoverButton", title: "Align", placeholderText: "More alignment-related buttons will be here" },
{
kind: "PopoverButton",
popover: {
title: "Align",
text: "More alignment-related buttons will be here",
},
props: {},
},
{ kind: "Separator", type: SeparatorType.Section },
{ kind: "Separator", props: { type: SeparatorType.Section } },
{ kind: "IconButton", icon: "FlipHorizontal", title: "Flip Horizontal", message: "FlipHorizontal" },
{ kind: "IconButton", icon: "FlipVertical", title: "Flip Vertical", message: "FlipVertical" },
{ kind: "IconButton", message: "FlipHorizontal", tooltip: "Flip Horizontal", props: { icon: "FlipHorizontal", size: 24 } },
{ kind: "IconButton", message: "FlipVertical", tooltip: "Flip Vertical", props: { icon: "FlipVertical", size: 24 } },
{ kind: "Separator", type: SeparatorType.Related },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "PopoverButton", title: "Flip", placeholderText: "More flip-related buttons will be here" },
{
kind: "PopoverButton",
popover: {
title: "Flip",
text: "More flip-related buttons will be here",
},
props: {},
},
{ kind: "Separator", type: SeparatorType.Section },
{ kind: "Separator", props: { type: SeparatorType.Section } },
{ kind: "IconButton", icon: "BooleanUnion", title: "Boolean Union" },
{ kind: "IconButton", icon: "BooleanSubtractFront", title: "Boolean Subtract Front" },
{ kind: "IconButton", icon: "BooleanSubtractBack", title: "Boolean Subtract Back" },
{ kind: "IconButton", icon: "BooleanIntersect", title: "Boolean Intersect" },
{ kind: "IconButton", icon: "BooleanDifference", title: "Boolean Difference" },
{ kind: "IconButton", tooltip: "Boolean Union", props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", props: { icon: "BooleanDifference", size: 24 } },
{ kind: "Separator", type: SeparatorType.Related },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "PopoverButton", title: "Boolean", placeholderText: "More boolean-related buttons will be here" },
],
{
kind: "PopoverButton",
popover: {
title: "Boolean",
text: "More boolean-related buttons will be here",
},
props: {},
},
],
["Shape", [{ kind: "NumberInput", initial: 6, step: 1, min: 3, callback: this.setToolOptions }]],
]);
Shape: [{ kind: "NumberInput", callback: this.setToolOptions, props: { value: 6, min: 3, isInteger: true } }],
};
return {
optionsMap,
toolOptions,
SeparatorType,
};
},

View File

@ -70,18 +70,7 @@
<script lang="ts">
import { defineComponent } from "vue";
export enum SeparatorDirection {
"Horizontal" = "Horizontal",
"Vertical" = "Vertical",
}
export enum SeparatorType {
"Related" = "Related",
"Unrelated" = "Unrelated",
"Section" = "Section",
"List" = "List",
}
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
export default defineComponent({
components: {},

View File

@ -0,0 +1,77 @@
export type Widgets = IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget;
export type WidgetRow = Array<Widgets>;
export type WidgetLayout = Array<WidgetRow>;
// Icon Button
export interface IconButtonWidget {
kind: "IconButton";
tooltip?: string;
message?: string | object;
callback?: Function;
props: IconButtonProps;
}
export interface IconButtonProps {
// `action` is used via `IconButtonWidget.callback`
icon: string;
size: number;
gapAfter?: boolean;
}
// Popover Button
export interface PopoverButtonWidget {
kind: "PopoverButton";
tooltip?: string;
callback?: Function;
// popover: WidgetLayout;
popover: { title: string; text: string }; // TODO: Replace this with a `WidgetLayout` like above for arbitrary layouts
props: PopoverButtonProps;
}
export interface PopoverButtonProps {
// `action` is used via `PopoverButtonWidget.callback`
icon?: string;
}
// Number Input
export interface NumberInputWidget {
kind: "NumberInput";
tooltip?: string;
callback?: Function;
props: NumberInputProps;
}
export interface NumberInputProps {
value: number;
min?: number;
max?: number;
step?: number;
stepIsMultiplier?: boolean;
isInteger?: boolean;
unit?: string;
unitIsHiddenWhenEditing?: boolean;
displayDecimalPlaces?: number;
}
// Separator
export interface SeparatorWidget {
kind: "Separator";
props: SeparatorProps;
}
export interface SeparatorProps {
direction?: SeparatorDirection;
type?: SeparatorType;
}
export enum SeparatorDirection {
"Horizontal" = "Horizontal",
"Vertical" = "Vertical",
}
export enum SeparatorType {
"Related" = "Related",
"Unrelated" = "Unrelated",
"Section" = "Section",
"List" = "List",
}

View File

@ -55,8 +55,11 @@
<script lang="ts">
import { defineComponent } from "vue";
import { SeparatorType } from "@/components/widgets/widgets";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
import Separator, { SeparatorType } from "@/components/widgets/separators/Separator.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export default defineComponent({
components: {

View File

@ -11,7 +11,7 @@
@click="handleTabClick(tabIndex)"
>
<span>{{ tabLabel }}</span>
<IconButton :icon="'CloseX'" :size="16" v-if="tabCloseButtons" @click.stop="handleTabClose(tabIndex)" />
<IconButton :action="() => handleTabClose(tabIndex)" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</div>
</div>
<PopoverButton :icon="PopoverButtonIcon.VerticalEllipsis">