Skip to content

Add two-way tool option messaging system #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 29, 2021
3 changes: 2 additions & 1 deletion editor/src/frontend/frontend_message_handler.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::frontend::layer_panel::LayerPanelEntry;
use crate::message_prelude::*;
use crate::tool::tool_options::ToolOptions;
use crate::Color;
use serde::{Deserialize, Serialize};

Expand All @@ -10,7 +11,7 @@ pub type Callback = Box<dyn Fn(FrontendMessage)>;
pub enum FrontendMessage {
CollapseFolder { path: Vec<LayerId> },
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
SetActiveTool { tool_name: String },
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
SetActiveDocument { document_index: usize },
UpdateOpenDocumentsList { open_documents: Vec<String> },
DisplayError { description: String },
Expand Down
4 changes: 4 additions & 0 deletions editor/src/tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ fn default_tool_options() -> HashMap<ToolType, ToolOptions> {
let tool_init = |tool: ToolType| (tool, tool.default_options());
std::array::IntoIter::new([
tool_init(ToolType::Select),
tool_init(ToolType::Pen),
tool_init(ToolType::Line),
tool_init(ToolType::Ellipse),
tool_init(ToolType::Shape), // TODO: Add more tool defaults
])
Expand Down Expand Up @@ -185,6 +187,8 @@ impl ToolType {
fn default_options(&self) -> ToolOptions {
match self {
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::Shape => ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices: 6 },
Expand Down
4 changes: 3 additions & 1 deletion editor/src/tool/tool_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
tool_data.active_tool_type = new_tool;

// 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 => {
let document_data = &mut self.tool_state.document_tool_data;
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/panels/Document.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<Separator :type="SeparatorType.Section" />

<ToolOptions :activeTool="activeTool" />
<ToolOptions :activeTool="activeTool" :activeToolOptions="activeToolOptions" />
</div>
<div class="spacer"></div>
<div class="right side">
Expand Down Expand Up @@ -330,7 +330,10 @@ export default defineComponent({

registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
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) => {
Expand All @@ -357,6 +360,7 @@ export default defineComponent({
canvasSvgWidth: "100%",
canvasSvgHeight: "100%",
activeTool: "Select",
activeToolOptions: {},
documentModeEntries,
viewModeEntries,
documentModeSelectionIndex: 0,
Expand Down
64 changes: 39 additions & 25 deletions frontend/src/components/widgets/options/ToolOptions.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
<template>
<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` -->
<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">
<h3>{{ option.popover.title }}</h3>
<p>{{ option.popover.text }}</p>
</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" />
</template>
</div>
Expand All @@ -23,7 +29,7 @@
</style>

<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";

import { comingSoon } from "@/utilities/errors";
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
Expand All @@ -38,28 +44,36 @@ const wasm = import("@/../wasm/pkg");
export default defineComponent({
props: {
activeTool: { type: String },
activeToolOptions: { type: Object as PropType<Record<string, object>> },
},
computed: {},
methods: {
async setShapeOptions(newValue: number) {
// TODO: Each value-input widget (i.e. not a button) should map to a field in an options struct,
// and updating a widget should send the whole updated struct to the backend.
// 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 updateToolOptions(path: string[], newValue: number) {
this.setToolOption(path, newValue);
(await wasm).set_tool_options(this.activeTool || "", this.activeToolOptions);
},
async setLineOptions(newValue: number) {
// eslint-disable-next-line camelcase
(await wasm).set_tool_options(this.$props.activeTool || "", { Line: { weight: newValue } });
async sendToolMessage(message: string | object) {
(await wasm).send_tool_message(this.activeTool || "", message);
},
async setPenOptions(newValue: number) {
// eslint-disable-next-line camelcase
(await wasm).set_tool_options(this.$props.activeTool || "", { Pen: { weight: newValue } });
// 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>;
},
async sendToolMessage(message: string | object) {
(await wasm).send_tool_message(this.$props.activeTool || "", message);
// 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) {
if (option.message) {
Expand All @@ -76,7 +90,7 @@ export default defineComponent({
},
},
data() {
const toolOptions: Record<string, WidgetRow> = {
const toolOptionsWidgets: 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 } },
Expand Down Expand Up @@ -134,13 +148,13 @@ export default defineComponent({
props: {},
},
],
Shape: [{ kind: "NumberInput", callback: this.setShapeOptions, props: { value: 6, min: 3, isInteger: true, label: "Sides" } }],
Line: [{ kind: "NumberInput", callback: this.setLineOptions, props: { value: 5, 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" } }],
Shape: [{ kind: "NumberInput", optionPath: ["shape_type", "Polygon", "vertices"], props: { min: 3, isInteger: true, label: "Sides" } }],
Line: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
Pen: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
};

return {
toolOptions,
toolOptionsWidgets,
SeparatorType,
comingSoon,
};
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/widgets/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export interface PopoverButtonProps {
export interface NumberInputWidget {
kind: "NumberInput";
tooltip?: string;
callback?: Function;
props: NumberInputProps;
optionPath: string[];
props: Omit<NumberInputProps, "value">;
}

export interface NumberInputProps {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/utilities/response-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,12 @@ function newUpdateWorkingColors(input: any): UpdateWorkingColors {

export interface SetActiveTool {
tool_name: string;
tool_options: object;
}
function newSetActiveTool(input: any): SetActiveTool {
return {
tool_name: input.tool_name,
tool_options: input.tool_options,
};
}

Expand Down