Skip to content

Commit 5446fa3

Browse files
henryksloanKeavon
authored andcommitted
Options bar widgets for tools to control tool behavior (#283)
* Add JSON-backed options widget * Add initial tool settings messaging to backend * Add shape side selection with JSON deserialization * Enforce minimum number of n-gon sides * Make tool settings JSON errors safer * Make tool settings JSON errors safer * Refactor ToolOptions to ToolSettings * Revert "Refactor ToolOptions to ToolSettings" This reverts commit 651161f. * Refactor all instances of "settings" to "options" * Fix names and formatting * Rearrange ToolOptions data to enforce types
1 parent 552cee4 commit 5446fa3

File tree

8 files changed

+198
-80
lines changed

8 files changed

+198
-80
lines changed

client/web/src/components/panels/Document.vue

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,7 @@
66

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

9-
<IconButton :icon="'AlignHorizontalLeft'" :size="24" title="Horizontal Align Left" />
10-
<IconButton :icon="'AlignHorizontalCenter'" :size="24" title="Horizontal Align Center" />
11-
<IconButton :icon="'AlignHorizontalRight'" :size="24" gapAfter title="Horizontal Align Right" />
12-
13-
<Separator :type="SeparatorType.Unrelated" />
14-
15-
<IconButton :icon="'AlignVerticalTop'" :size="24" title="Vertical Align Top" />
16-
<IconButton :icon="'AlignVerticalCenter'" :size="24" title="Vertical Align Center" />
17-
<IconButton :icon="'AlignVerticalBottom'" :size="24" title="Vertical Align Bottom" />
18-
19-
<Separator :type="SeparatorType.Related" />
20-
21-
<PopoverButton>
22-
<h3>Align</h3>
23-
<p>More alignment-related buttons will be here</p>
24-
</PopoverButton>
25-
26-
<Separator :type="SeparatorType.Section" />
27-
28-
<IconButton :icon="'FlipHorizontal'" :size="24" title="Flip Horizontal" />
29-
<IconButton :icon="'FlipVertical'" :size="24" title="Flip Vertical" />
30-
31-
<Separator :type="SeparatorType.Related" />
32-
33-
<PopoverButton>
34-
<h3>Flip</h3>
35-
<p>More flip-related buttons will be here</p>
36-
</PopoverButton>
37-
38-
<Separator :type="SeparatorType.Section" />
39-
40-
<IconButton :icon="'BooleanUnion'" :size="24" title="Boolean Union" />
41-
<IconButton :icon="'BooleanSubtractFront'" :size="24" title="Boolean Subtract Front" />
42-
<IconButton :icon="'BooleanSubtractBack'" :size="24" title="Boolean Subtract Back" />
43-
<IconButton :icon="'BooleanIntersect'" :size="24" title="Boolean Intersect" />
44-
<IconButton :icon="'BooleanDifference'" :size="24" title="Boolean Difference" />
45-
46-
<Separator :type="SeparatorType.Related" />
47-
48-
<PopoverButton>
49-
<h3>Boolean</h3>
50-
<p>More boolean-related buttons will be here</p>
51-
</PopoverButton>
9+
<ToolOptions :activeTool="activeTool" />
5210
</div>
5311
<div class="spacer"></div>
5412
<div class="right side">
@@ -255,6 +213,7 @@ import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
255213
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
256214
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
257215
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
216+
import ToolOptions from "@/components/widgets/options/ToolOptions.vue";
258217
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
259218
260219
const modeMenuEntries: SectionsOfMenuListEntries = [
@@ -401,6 +360,7 @@ export default defineComponent({
401360
NumberInput,
402361
DropdownInput,
403362
OptionalInput,
363+
ToolOptions,
404364
},
405365
});
406366
</script>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<template>
2+
<div class="tool-options">
3+
<template v-for="(option, index) in optionsMap.get(activeTool) || []" :key="index">
4+
<IconButton v-if="option.kind === 'icon_button'" :icon="option.icon" :size="24" :title="option.title" />
5+
<Separator v-if="option.kind === 'separator'" :type="option.type" />
6+
<PopoverButton v-if="option.kind === 'popover_button'">
7+
<h3>{{ option.title }}</h3>
8+
<p>{{ option.placeholderText }}</p>
9+
</PopoverButton>
10+
<NumberInput v-if="option.kind === 'number_input'" :callback="option.callback" :initialValue="option.initial" :step="option.step" :min="option.min" :updateOnCallback="true" />
11+
</template>
12+
</div>
13+
</template>
14+
15+
<style lang="scss">
16+
.tool-options {
17+
height: 100%;
18+
flex: 0 0 auto;
19+
display: flex;
20+
align-items: center;
21+
}
22+
</style>
23+
24+
<script lang="ts">
25+
import { defineComponent } from "vue";
26+
import Separator, { SeparatorType } from "@/components/widgets/separators/Separator.vue";
27+
import IconButton from "@/components/widgets/buttons/IconButton.vue";
28+
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
29+
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
30+
31+
const wasm = import("@/../wasm/pkg");
32+
33+
type ToolOptionsList = Array<ToolOptions>;
34+
type ToolOptionsMap = Map<string, ToolOptionsList>;
35+
type ToolOptions = IconButtonOption | SeparatorOption | PopoverButtonOption | NumberInputOption;
36+
37+
interface IconButtonOption {
38+
kind: "icon_button";
39+
icon: string;
40+
title: string;
41+
}
42+
43+
interface SeparatorOption {
44+
kind: "separator";
45+
type: SeparatorType;
46+
}
47+
48+
interface PopoverButtonOption {
49+
kind: "popover_button";
50+
title: string;
51+
placeholderText: string;
52+
}
53+
54+
interface NumberInputOption {
55+
kind: "number_input";
56+
initial: number;
57+
step: number;
58+
min?: number;
59+
callback?: Function;
60+
}
61+
62+
export default defineComponent({
63+
props: {
64+
activeTool: { type: String },
65+
},
66+
computed: {},
67+
methods: {
68+
async setToolOptions(newValue: number) {
69+
// TODO: Each value-input widget (i.e. not a button) should map to a field in an options struct,
70+
// and updating a widget should send the whole updated struct to the backend.
71+
// Later, it could send a single-field update to the backend.
72+
73+
const { set_tool_options } = await wasm;
74+
// This is a placeholder call, using the Shape tool as an example
75+
set_tool_options(this.$props.activeTool || "", { Shape: { shape_type: { Polygon: { vertices: newValue } } } });
76+
},
77+
},
78+
data() {
79+
const optionsMap: ToolOptionsMap = new Map([
80+
[
81+
"Select",
82+
[
83+
{ kind: "icon_button", icon: "AlignHorizontalLeft", title: "Horizontal Align Left" },
84+
{ kind: "icon_button", icon: "AlignHorizontalCenter", title: "Horizontal Align Center" },
85+
{ kind: "icon_button", icon: "AlignHorizontalRight", title: "Horizontal Align Right" },
86+
87+
{ kind: "separator", type: SeparatorType.Unrelated },
88+
89+
{ kind: "icon_button", icon: "AlignVerticalTop", title: "Vertical Align Top" },
90+
{ kind: "icon_button", icon: "AlignVerticalCenter", title: "Vertical Align Center" },
91+
{ kind: "icon_button", icon: "AlignVerticalBottom", title: "Vertical Align Bottom" },
92+
93+
{ kind: "separator", type: SeparatorType.Related },
94+
95+
{ kind: "popover_button", title: "Align", placeholderText: "More alignment-related buttons will be here" },
96+
97+
{ kind: "separator", type: SeparatorType.Section },
98+
99+
{ kind: "icon_button", icon: "FlipHorizontal", title: "Flip Horizontal" },
100+
{ kind: "icon_button", icon: "FlipVertical", title: "Flip Vertical" },
101+
102+
{ kind: "separator", type: SeparatorType.Related },
103+
104+
{ kind: "popover_button", title: "Flip", placeholderText: "More flip-related buttons will be here" },
105+
106+
{ kind: "separator", type: SeparatorType.Section },
107+
108+
{ kind: "icon_button", icon: "BooleanUnion", title: "Boolean Union" },
109+
{ kind: "icon_button", icon: "BooleanSubtractFront", title: "Boolean Subtract Front" },
110+
{ kind: "icon_button", icon: "BooleanSubtractBack", title: "Boolean Subtract Back" },
111+
{ kind: "icon_button", icon: "BooleanIntersect", title: "Boolean Intersect" },
112+
{ kind: "icon_button", icon: "BooleanDifference", title: "Boolean Difference" },
113+
114+
{ kind: "separator", type: SeparatorType.Related },
115+
116+
{ kind: "popover_button", title: "Boolean", placeholderText: "More boolean-related buttons will be here" },
117+
],
118+
],
119+
["Shape", [{ kind: "number_input", initial: 6, step: 1, min: 3, callback: this.setToolOptions }]],
120+
]);
121+
122+
return {
123+
optionsMap,
124+
SeparatorType,
125+
};
126+
},
127+
components: {
128+
Separator,
129+
IconButton,
130+
PopoverButton,
131+
NumberInput,
132+
},
133+
});
134+
</script>

client/web/wasm/src/document.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::EDITOR_STATE;
44
use editor_core::input::input_preprocessor::ModifierKeys;
55
use editor_core::input::mouse::ScrollDelta;
66
use editor_core::message_prelude::*;
7+
use editor_core::tool::tool_options::ToolOptions;
78
use editor_core::{
89
input::mouse::{MouseState, ViewportPosition},
910
LayerId,
@@ -23,6 +24,18 @@ pub fn select_tool(tool: String) -> Result<(), JsValue> {
2324
})
2425
}
2526

27+
/// Update the options for a given tool
28+
#[wasm_bindgen]
29+
pub fn set_tool_options(tool: String, options: &JsValue) -> Result<(), JsValue> {
30+
match options.into_serde::<ToolOptions>() {
31+
Ok(options) => EDITOR_STATE.with(|editor| match translate_tool(&tool) {
32+
Some(tool) => editor.borrow_mut().handle_message(ToolMessage::SetToolOptions(tool, options)).map_err(convert_error),
33+
None => Err(Error::new(&format!("Couldn't select {} because it was not recognized as a valid tool", tool)).into()),
34+
}),
35+
Err(err) => Err(Error::new(&format!("Invalud JSON for ToolOptions: {}", err)).into()),
36+
}
37+
}
38+
2639
#[wasm_bindgen]
2740
pub fn select_document(document: usize) -> Result<(), JsValue> {
2841
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectDocument(document)).map_err(convert_error))

core/editor/src/tool/mod.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub mod tool_message_handler;
2-
pub mod tool_settings;
2+
pub mod tool_options;
33
pub mod tools;
44

55
use crate::document::Document;
@@ -15,8 +15,8 @@ use std::{
1515
fmt::{self, Debug},
1616
};
1717
pub use tool_message_handler::ToolMessageHandler;
18-
use tool_settings::ToolSettings;
19-
pub use tool_settings::*;
18+
use tool_options::ToolOptions;
19+
pub use tool_options::*;
2020
use tools::*;
2121

2222
pub mod tool_messages {
@@ -37,7 +37,7 @@ pub trait Fsm {
3737
pub struct DocumentToolData {
3838
pub primary_color: Color,
3939
pub secondary_color: Color,
40-
tool_settings: HashMap<ToolType, ToolSettings>,
40+
pub tool_options: HashMap<ToolType, ToolOptions>,
4141
}
4242

4343
type SubToolMessageHandler = dyn for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>>;
@@ -48,7 +48,7 @@ pub struct ToolData {
4848

4949
impl fmt::Debug for ToolData {
5050
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51-
f.debug_struct("ToolData").field("active_tool_type", &self.active_tool_type).field("tool_settings", &"[…]").finish()
51+
f.debug_struct("ToolData").field("active_tool_type", &self.active_tool_type).field("tool_options", &"[…]").finish()
5252
}
5353
}
5454

@@ -89,7 +89,7 @@ impl Default for ToolFsmState {
8989
document_tool_data: DocumentToolData {
9090
primary_color: Color::BLACK,
9191
secondary_color: Color::WHITE,
92-
tool_settings: default_tool_settings(),
92+
tool_options: default_tool_options(),
9393
},
9494
}
9595
}
@@ -105,8 +105,8 @@ impl ToolFsmState {
105105
}
106106
}
107107

108-
fn default_tool_settings() -> HashMap<ToolType, ToolSettings> {
109-
let tool_init = |tool: ToolType| (tool, tool.default_settings());
108+
fn default_tool_options() -> HashMap<ToolType, ToolOptions> {
109+
let tool_init = |tool: ToolType| (tool, tool.default_options());
110110
std::array::IntoIter::new([
111111
tool_init(ToolType::Select),
112112
tool_init(ToolType::Ellipse),
@@ -174,12 +174,12 @@ impl fmt::Display for ToolType {
174174
}
175175

176176
impl ToolType {
177-
fn default_settings(&self) -> ToolSettings {
177+
fn default_options(&self) -> ToolOptions {
178178
match self {
179-
ToolType::Select => ToolSettings::Select { append_mode: SelectAppendMode::New },
180-
ToolType::Ellipse => ToolSettings::Ellipse,
181-
ToolType::Shape => ToolSettings::Shape {
182-
shape: Shape::Polygon { vertices: 3 },
179+
ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New },
180+
ToolType::Ellipse => ToolOptions::Ellipse,
181+
ToolType::Shape => ToolOptions::Shape {
182+
shape_type: ShapeType::Polygon { vertices: 6 },
183183
},
184184
_ => todo!(),
185185
}

core/editor/src/tool/tool_message_handler.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use document_core::color::Color;
44
use crate::input::InputPreprocessor;
55
use crate::{
66
document::Document,
7-
tool::{ToolFsmState, ToolType},
7+
tool::{tool_options::ToolOptions, ToolFsmState, ToolType},
88
};
99
use std::collections::VecDeque;
1010

@@ -16,6 +16,7 @@ pub enum ToolMessage {
1616
SelectSecondaryColor(Color),
1717
SwapColors,
1818
ResetColors,
19+
SetToolOptions(ToolType, ToolOptions),
1920
#[child]
2021
Fill(FillMessage),
2122
#[child]
@@ -89,6 +90,9 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
8990
.into(),
9091
)
9192
}
93+
SetToolOptions(tool_type, tool_options) => {
94+
self.tool_state.document_tool_data.tool_options.insert(tool_type, tool_options);
95+
}
9296
message => {
9397
let tool_type = match message {
9498
Fill(_) => ToolType::Fill,
@@ -111,7 +115,7 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
111115
}
112116
}
113117
fn actions(&self) -> ActionList {
114-
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, SelectTool);
118+
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, SelectTool, SetToolOptions);
115119
list.extend(self.tool_state.tool_data.active_tool().actions());
116120
list
117121
}

core/editor/src/tool/tool_options.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
4+
pub enum ToolOptions {
5+
Select { append_mode: SelectAppendMode },
6+
Ellipse,
7+
Shape { shape_type: ShapeType },
8+
}
9+
10+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
11+
pub enum SelectAppendMode {
12+
New,
13+
Add,
14+
Subtract,
15+
Intersect,
16+
}
17+
18+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
19+
pub enum ShapeType {
20+
Star { vertices: u32 },
21+
Polygon { vertices: u32 },
22+
}

core/editor/src/tool/tool_settings.rs

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)