Add two-way tool option messaging system between frontend/backend (#361)
* Add two-way tool option messaging system * Rename tool option functions * Move repeated frontend messaging code to function * Address style comments * Rename variable to be more descriptive * Move tool options update to SetActiveTool message * Refactor record of all tool options * Only pass active tool options to bar Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
05ac4ac9b8
commit
9e73cce281
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::frontend::layer_panel::LayerPanelEntry;
|
use crate::frontend::layer_panel::LayerPanelEntry;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
use crate::tool::tool_options::ToolOptions;
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -10,7 +11,7 @@ pub type Callback = Box<dyn Fn(FrontendMessage)>;
|
||||||
pub enum FrontendMessage {
|
pub enum FrontendMessage {
|
||||||
CollapseFolder { path: Vec<LayerId> },
|
CollapseFolder { path: Vec<LayerId> },
|
||||||
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
|
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
|
||||||
SetActiveTool { tool_name: String },
|
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
||||||
SetActiveDocument { document_index: usize },
|
SetActiveDocument { document_index: usize },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
||||||
DisplayError { description: String },
|
DisplayError { description: String },
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ fn default_tool_options() -> HashMap<ToolType, ToolOptions> {
|
||||||
let tool_init = |tool: ToolType| (tool, tool.default_options());
|
let tool_init = |tool: ToolType| (tool, tool.default_options());
|
||||||
std::array::IntoIter::new([
|
std::array::IntoIter::new([
|
||||||
tool_init(ToolType::Select),
|
tool_init(ToolType::Select),
|
||||||
|
tool_init(ToolType::Pen),
|
||||||
|
tool_init(ToolType::Line),
|
||||||
tool_init(ToolType::Ellipse),
|
tool_init(ToolType::Ellipse),
|
||||||
tool_init(ToolType::Shape), // TODO: Add more tool defaults
|
tool_init(ToolType::Shape), // TODO: Add more tool defaults
|
||||||
])
|
])
|
||||||
|
|
@ -185,6 +187,8 @@ impl ToolType {
|
||||||
fn default_options(&self) -> ToolOptions {
|
fn default_options(&self) -> ToolOptions {
|
||||||
match self {
|
match self {
|
||||||
ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New },
|
ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New },
|
||||||
|
ToolType::Pen => ToolOptions::Pen { weight: 5 },
|
||||||
|
ToolType::Line => ToolOptions::Line { weight: 5 },
|
||||||
ToolType::Ellipse => ToolOptions::Ellipse,
|
ToolType::Ellipse => ToolOptions::Ellipse,
|
||||||
ToolType::Shape => ToolOptions::Shape {
|
ToolType::Shape => ToolOptions::Shape {
|
||||||
shape_type: ShapeType::Polygon { vertices: 6 },
|
shape_type: ShapeType::Polygon { vertices: 6 },
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
|
||||||
tool_data.active_tool_type = new_tool;
|
tool_data.active_tool_type = new_tool;
|
||||||
|
|
||||||
// Notify the frontend about the new active tool to be displayed
|
// Notify the frontend about the new active tool to be displayed
|
||||||
responses.push_back(FrontendMessage::SetActiveTool { tool_name: new_tool.to_string() }.into());
|
let tool_name = new_tool.to_string();
|
||||||
|
let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).map(|tool_options| *tool_options);
|
||||||
|
responses.push_back(FrontendMessage::SetActiveTool { tool_name, tool_options }.into());
|
||||||
}
|
}
|
||||||
SwapColors => {
|
SwapColors => {
|
||||||
let document_data = &mut self.tool_state.document_tool_data;
|
let document_data = &mut self.tool_state.document_tool_data;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<Separator :type="SeparatorType.Section" />
|
<Separator :type="SeparatorType.Section" />
|
||||||
|
|
||||||
<ToolOptions :activeTool="activeTool" />
|
<ToolOptions :activeTool="activeTool" :activeToolOptions="activeToolOptions" />
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="right side">
|
<div class="right side">
|
||||||
|
|
@ -330,7 +330,10 @@ export default defineComponent({
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
|
||||||
const toolData = responseData as SetActiveTool;
|
const toolData = responseData as SetActiveTool;
|
||||||
if (toolData) this.activeTool = toolData.tool_name;
|
if (toolData) {
|
||||||
|
this.activeTool = toolData.tool_name;
|
||||||
|
this.activeToolOptions = toolData.tool_options;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.SetCanvasZoom, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetCanvasZoom, (responseData: Response) => {
|
||||||
|
|
@ -357,6 +360,7 @@ export default defineComponent({
|
||||||
canvasSvgWidth: "100%",
|
canvasSvgWidth: "100%",
|
||||||
canvasSvgHeight: "100%",
|
canvasSvgHeight: "100%",
|
||||||
activeTool: "Select",
|
activeTool: "Select",
|
||||||
|
activeToolOptions: {},
|
||||||
documentModeEntries,
|
documentModeEntries,
|
||||||
viewModeEntries,
|
viewModeEntries,
|
||||||
documentModeSelectionIndex: 0,
|
documentModeSelectionIndex: 0,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tool-options">
|
<div class="tool-options">
|
||||||
<template v-for="(option, index) in toolOptions[activeTool] || []" :key="index">
|
<template v-for="(option, index) in toolOptionsWidgets[activeTool] || []" :key="index">
|
||||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||||
<IconButton v-if="option.kind === 'IconButton'" :action="() => handleIconButtonAction(option)" :title="option.tooltip" v-bind="option.props" />
|
<IconButton v-if="option.kind === 'IconButton'" :action="() => handleIconButtonAction(option)" :title="option.tooltip" v-bind="option.props" />
|
||||||
<PopoverButton v-if="option.kind === 'PopoverButton'" :title="option.tooltip" :action="option.callback" v-bind="option.props">
|
<PopoverButton v-if="option.kind === 'PopoverButton'" :title="option.tooltip" :action="option.callback" v-bind="option.props">
|
||||||
<h3>{{ option.popover.title }}</h3>
|
<h3>{{ option.popover.title }}</h3>
|
||||||
<p>{{ option.popover.text }}</p>
|
<p>{{ option.popover.text }}</p>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
<NumberInput v-if="option.kind === 'NumberInput'" v-model:value="option.props.value" @update:value="option.callback" :title="option.tooltip" v-bind="option.props" />
|
<NumberInput
|
||||||
|
v-if="option.kind === 'NumberInput'"
|
||||||
|
@update:value="(value) => updateToolOptions(option.optionPath, value)"
|
||||||
|
:title="option.tooltip"
|
||||||
|
:value="getToolOption(option.optionPath)"
|
||||||
|
v-bind="option.props"
|
||||||
|
/>
|
||||||
<Separator v-if="option.kind === 'Separator'" v-bind="option.props" />
|
<Separator v-if="option.kind === 'Separator'" v-bind="option.props" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,7 +29,7 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { comingSoon } from "@/utilities/errors";
|
import { comingSoon } from "@/utilities/errors";
|
||||||
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
|
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
|
||||||
|
|
@ -38,28 +44,36 @@ const wasm = import("@/../wasm/pkg");
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
activeTool: { type: String },
|
activeTool: { type: String },
|
||||||
|
activeToolOptions: { type: Object as PropType<Record<string, object>> },
|
||||||
},
|
},
|
||||||
computed: {},
|
|
||||||
methods: {
|
methods: {
|
||||||
async setShapeOptions(newValue: number) {
|
async updateToolOptions(path: string[], newValue: number) {
|
||||||
// TODO: Each value-input widget (i.e. not a button) should map to a field in an options struct,
|
this.setToolOption(path, newValue);
|
||||||
// and updating a widget should send the whole updated struct to the backend.
|
(await wasm).set_tool_options(this.activeTool || "", this.activeToolOptions);
|
||||||
// Later, it could send a single-field update to the backend.
|
|
||||||
|
|
||||||
// This is a placeholder call, using the Shape tool as an example
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
(await wasm).set_tool_options(this.$props.activeTool || "", { Shape: { shape_type: { Polygon: { vertices: newValue } } } });
|
|
||||||
},
|
|
||||||
async setLineOptions(newValue: number) {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
(await wasm).set_tool_options(this.$props.activeTool || "", { Line: { weight: newValue } });
|
|
||||||
},
|
|
||||||
async setPenOptions(newValue: number) {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
(await wasm).set_tool_options(this.$props.activeTool || "", { Pen: { weight: newValue } });
|
|
||||||
},
|
},
|
||||||
async sendToolMessage(message: string | object) {
|
async sendToolMessage(message: string | object) {
|
||||||
(await wasm).send_tool_message(this.$props.activeTool || "", message);
|
(await wasm).send_tool_message(this.activeTool || "", message);
|
||||||
|
},
|
||||||
|
// Traverses the given path and returns the direct parent of the option
|
||||||
|
getRecordContainingOption(optionPath: string[]): Record<string, number> {
|
||||||
|
const allButLast = optionPath.slice(0, -1);
|
||||||
|
let currentRecord = this.activeToolOptions as Record<string, object | number>;
|
||||||
|
[this.activeTool || "", ...allButLast].forEach((attr) => {
|
||||||
|
currentRecord = currentRecord[attr] as Record<string, object | number>;
|
||||||
|
});
|
||||||
|
return currentRecord as Record<string, number>;
|
||||||
|
},
|
||||||
|
// Traverses the given path into the active tool's option struct, and sets the value at the path tail
|
||||||
|
setToolOption(optionPath: string[], newValue: number) {
|
||||||
|
const last = optionPath.slice(-1)[0];
|
||||||
|
const recordContainingOption = this.getRecordContainingOption(optionPath);
|
||||||
|
recordContainingOption[last] = newValue;
|
||||||
|
},
|
||||||
|
// Traverses the given path into the active tool's option struct, and returns the value at the path tail
|
||||||
|
getToolOption(optionPath: string[]): number {
|
||||||
|
const last = optionPath.slice(-1)[0];
|
||||||
|
const recordContainingOption = this.getRecordContainingOption(optionPath);
|
||||||
|
return recordContainingOption[last];
|
||||||
},
|
},
|
||||||
handleIconButtonAction(option: IconButtonWidget) {
|
handleIconButtonAction(option: IconButtonWidget) {
|
||||||
if (option.message) {
|
if (option.message) {
|
||||||
|
|
@ -76,7 +90,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const toolOptions: Record<string, WidgetRow> = {
|
const toolOptionsWidgets: Record<string, WidgetRow> = {
|
||||||
Select: [
|
Select: [
|
||||||
{ kind: "IconButton", message: { Align: ["X", "Min"] }, tooltip: "Align Left", props: { icon: "AlignLeft", size: 24 } },
|
{ 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", "Center"] }, tooltip: "Align Horizontal Center", props: { icon: "AlignHorizontalCenter", size: 24 } },
|
||||||
|
|
@ -134,13 +148,13 @@ export default defineComponent({
|
||||||
props: {},
|
props: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Shape: [{ kind: "NumberInput", callback: this.setShapeOptions, props: { value: 6, min: 3, isInteger: true, label: "Sides" } }],
|
Shape: [{ kind: "NumberInput", optionPath: ["shape_type", "Polygon", "vertices"], props: { min: 3, isInteger: true, label: "Sides" } }],
|
||||||
Line: [{ kind: "NumberInput", callback: this.setLineOptions, props: { value: 5, min: 1, isInteger: true, unit: " px", label: "Weight" } }],
|
Line: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
|
||||||
Pen: [{ kind: "NumberInput", callback: this.setPenOptions, props: { value: 5, min: 1, isInteger: true, unit: " px", label: "Weight" } }],
|
Pen: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolOptions,
|
toolOptionsWidgets,
|
||||||
SeparatorType,
|
SeparatorType,
|
||||||
comingSoon,
|
comingSoon,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ export interface PopoverButtonProps {
|
||||||
export interface NumberInputWidget {
|
export interface NumberInputWidget {
|
||||||
kind: "NumberInput";
|
kind: "NumberInput";
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
callback?: Function;
|
optionPath: string[];
|
||||||
props: NumberInputProps;
|
props: Omit<NumberInputProps, "value">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberInputProps {
|
export interface NumberInputProps {
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,12 @@ function newUpdateWorkingColors(input: any): UpdateWorkingColors {
|
||||||
|
|
||||||
export interface SetActiveTool {
|
export interface SetActiveTool {
|
||||||
tool_name: string;
|
tool_name: string;
|
||||||
|
tool_options: object;
|
||||||
}
|
}
|
||||||
function newSetActiveTool(input: any): SetActiveTool {
|
function newSetActiveTool(input: any): SetActiveTool {
|
||||||
return {
|
return {
|
||||||
tool_name: input.tool_name,
|
tool_name: input.tool_name,
|
||||||
|
tool_options: input.tool_options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue