Layout system implementation and applied to tool options bar (#499)

* initial layout system with tool options

* cargo fmt

* cargo fmt again

* document bar defined on the backend

* cargo fmt

* removed RC<RefCell>

* cargo fmt

* - fix increment behavior
- removed hashmap from layout message handler
- removed no op message from layoutMessage

* cargo fmt

* only send documentBar when zoom or rotation is updated

* ctrl-0 changes zoom properly

* Code review changes

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mfish33 2022-01-30 17:53:37 -08:00 committed by Keavon Chambers
parent 121a68ad3c
commit 96d3ef2650
44 changed files with 1357 additions and 532 deletions

12
Cargo.lock generated
View File

@ -62,6 +62,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_logger"
version = "0.8.4"
@ -95,6 +106,7 @@ name = "graphite-editor"
version = "0.0.0"
dependencies = [
"bitflags",
"derivative",
"env_logger",
"glam",
"graphite-graphene",

View File

@ -24,6 +24,7 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
"serde",
] }
remain = "0.2.2"
derivative = "2.2.0"
[dependencies.graphene]
path = "../graphene"

View File

@ -1,6 +1,7 @@
use crate::document::PortfolioMessageHandler;
use crate::global::GlobalMessageHandler;
use crate::input::{InputMapperMessageHandler, InputPreprocessorMessageHandler};
use crate::layout::layout_message_handler::LayoutMessageHandler;
use crate::message_prelude::*;
use crate::viewport_tools::tool_message_handler::ToolMessageHandler;
@ -19,6 +20,7 @@ struct DispatcherMessageHandlers {
global_message_handler: GlobalMessageHandler,
input_mapper_message_handler: InputMapperMessageHandler,
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
layout_message_handler: LayoutMessageHandler,
portfolio_message_handler: PortfolioMessageHandler,
tool_message_handler: ToolMessageHandler,
}
@ -76,6 +78,7 @@ impl Dispatcher {
InputPreprocessor(message) => {
self.message_handlers.input_preprocessor_message_handler.process_action(message, (), &mut self.message_queue);
}
Layout(message) => self.message_handlers.layout_message_handler.process_action(message, (), &mut self.message_queue),
Portfolio(message) => {
self.message_handlers
.portfolio_message_handler

View File

@ -31,6 +31,8 @@ pub enum Message {
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
Layout(LayoutMessage),
#[child]
Portfolio(PortfolioMessage),
#[child]
Tool(ToolMessage),

View File

@ -7,6 +7,10 @@ use crate::consts::{
ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{
IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget,
WidgetCallback, WidgetHolder, WidgetLayout,
};
use crate::message_prelude::*;
use crate::EditorError;
@ -459,6 +463,154 @@ impl DocumentMessageHandler {
}
}
impl PropertyHolder for DocumentMessageHandler {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![
WidgetHolder::new(Widget::OptionalInput(OptionalInput {
checked: self.snapping_enabled,
icon: "Snapping".into(),
tooltip: "Snapping".into(),
on_update: WidgetCallback::new(|updated_optional_input| DocumentMessage::SetSnapping { snap: updated_optional_input.checked }.into()),
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Snapping".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::OptionalInput(OptionalInput {
checked: true,
icon: "Grid".into(),
tooltip: "Grid".into(),
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(318) }.into()),
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Grid".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::OptionalInput(OptionalInput {
checked: self.overlays_visible,
icon: "Overlays".into(),
tooltip: "Overlays".into(),
on_update: WidgetCallback::new(|updated_optional_input| {
DocumentMessage::SetOverlaysVisibility {
visible: updated_optional_input.checked,
}
.into()
}),
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Overlays".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::RadioInput(RadioInput {
selected_index: if self.view_mode == ViewMode::Normal { 0 } else { 1 },
entries: vec![
RadioEntryData {
value: "normal".into(),
icon: "ViewModeNormal".into(),
tooltip: "View Mode: Normal".into(),
on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()),
..RadioEntryData::default()
},
RadioEntryData {
value: "outline".into(),
icon: "ViewModeOutline".into(),
tooltip: "View Mode: Outline".into(),
on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()),
..RadioEntryData::default()
},
RadioEntryData {
value: "pixels".into(),
icon: "ViewModePixels".into(),
tooltip: "View Mode: Pixels".into(),
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(320) }.into()),
..RadioEntryData::default()
},
],
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "View Mode".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Section,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: "°".into(),
value: self.movement_handler.tilt / (std::f64::consts::PI / 180.),
increment_factor: 15.,
on_update: WidgetCallback::new(|number_input| {
MovementMessage::SetCanvasRotation {
angle_radians: number_input.value * (std::f64::consts::PI / 180.),
}
.into()
}),
..NumberInput::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Section,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "ZoomIn".into(),
tooltip: "Zoom In".into(),
on_update: WidgetCallback::new(|_| MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "ZoomOut".into(),
tooltip: "Zoom Out".into(),
on_update: WidgetCallback::new(|_| MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "ZoomReset".into(),
tooltip: "Zoom to 100%".into(),
on_update: WidgetCallback::new(|_| MovementMessage::SetCanvasZoom { zoom_factor: 1. }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: "%".into(),
value: self.movement_handler.zoom * 100.,
min: Some(0.000001),
max: Some(1000000.),
on_update: WidgetCallback::new(|number_input| {
MovementMessage::SetCanvasZoom {
zoom_factor: number_input.value / 100.,
}
.into()
}),
increment_behavior: NumberInputIncrementBehavior::Callback,
increment_callback_decrease: WidgetCallback::new(|_| MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()),
increment_callback_increase: WidgetCallback::new(|_| MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()),
..NumberInput::default()
})),
],
}])
}
}
impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for DocumentMessageHandler {
#[remain::check]
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@ -674,7 +826,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
}
GroupSelectedLayers => {
let mut new_folder_path: Vec<u64> = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
// Required for grouping parent folders with their own children
if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) {

View File

@ -156,6 +156,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
IncreaseCanvasZoom { center_on_mouse } => {
@ -245,12 +246,14 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
self.create_document_transform(&ipp.viewport_bounds, responses);
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into());
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
}
SetCanvasZoom { zoom_factor } => {
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TransformCanvasEnd => {

View File

@ -60,5 +60,6 @@ pub enum PortfolioMessage {
SelectDocument {
document_id: u64,
},
UpdateDocumentBar,
UpdateOpenDocumentsList,
}

View File

@ -3,6 +3,8 @@ use super::DocumentMessageHandler;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::frontend::utility_types::FrontendDocumentDetails;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use graphene::Operation as DocumentOperation;
@ -369,6 +371,11 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
}
UpdateDocumentBar => {
let active_document = self.active_document();
active_document.register_properties(responses, LayoutTarget::DocumentBar)
}
UpdateOpenDocumentsList => {
// Send the list of document tab names

View File

@ -1,8 +1,9 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::SubLayout;
use crate::message_prelude::*;
use crate::misc::HintData;
use crate::viewport_tools::tool_options::ToolOptions;
use crate::Color;
use serde::{Deserialize, Serialize};
@ -15,6 +16,7 @@ pub enum FrontendMessage {
DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument { document_id: u64 },
DisplayDialogAboutGraphite,
DisplayDialogComingSoon { issue: Option<i32> },
DisplayDialogError { title: String, description: String },
DisplayDialogPanic { panic_info: String, title: String, description: String },
DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer },
@ -30,11 +32,12 @@ pub enum FrontendMessage {
// Update prefix: give the frontend a new value or state for it to use
UpdateActiveDocument { document_id: u64 },
UpdateActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
UpdateActiveTool { tool_name: String },
UpdateCanvasRotation { angle_radians: f64 },
UpdateCanvasZoom { factor: f64 },
UpdateDocumentArtboards { svg: String },
UpdateDocumentArtwork { svg: String },
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateDocumentLayer { data: LayerPanelEntry },
UpdateDocumentOverlays { svg: String },
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
@ -42,5 +45,6 @@ pub enum FrontendMessage {
UpdateInputHints { hint_data: HintData },
UpdateMouseCursor { cursor: MouseCursorIcon },
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateWorkingColors { primary: Color, secondary: Color },
}

View File

@ -0,0 +1,25 @@
use super::widgets::WidgetLayout;
use crate::message_prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Layout)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
pub enum LayoutMessage {
SendLayout { layout: WidgetLayout, layout_target: LayoutTarget },
UpdateLayout { layout_target: LayoutTarget, widget_id: u64, value: serde_json::Value },
}
#[remain::sorted]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)]
#[repr(u8)]
pub enum LayoutTarget {
DocumentBar,
ToolOptions,
// KEEP THIS ENUM LAST
// This is a marker that is used to define an array that is used to hold widgets
#[remain::unsorted]
LayoutTargetLength,
}

View File

@ -0,0 +1,88 @@
use super::layout_message::LayoutTarget;
use super::widgets::WidgetLayout;
use crate::layout::widgets::Widget;
use crate::message_prelude::*;
use serde_json::Value;
use std::collections::VecDeque;
#[derive(Debug, Clone, Default)]
pub struct LayoutMessageHandler {
layouts: [WidgetLayout; LayoutTarget::LayoutTargetLength as usize],
}
impl LayoutMessageHandler {
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
let widget_layout = &self.layouts[layout_target as usize];
let message = match layout_target {
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
layout_target,
layout: widget_layout.layout.clone(),
},
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
layout_target,
layout: widget_layout.layout.clone(),
},
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
};
responses.push_back(message.into());
}
}
impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) {
use LayoutMessage::*;
match action {
SendLayout { layout, layout_target } => {
self.layouts[layout_target as usize] = layout;
self.send_layout(layout_target, responses);
}
UpdateLayout { layout_target, widget_id, value } => {
let layout = &mut self.layouts[layout_target as usize];
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id).expect("Received invalid widget_id from the frontend");
match &mut widget_holder.widget {
Widget::NumberInput(number_input) => match value {
Value::Number(num) => {
let update_value = num.as_f64().unwrap();
number_input.value = update_value;
let callback_message = (number_input.on_update.callback)(number_input);
responses.push_back(callback_message);
}
Value::String(str) => match str.as_str() {
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
_ => {
panic!("Invalid string found when updating `NumberInput`")
}
},
_ => panic!("Invalid type found when updating `NumberInput`"),
},
Widget::Separator(_) => {}
Widget::IconButton(icon_button) => {
let callback_message = (icon_button.on_update.callback)(icon_button);
responses.push_back(callback_message);
}
Widget::PopoverButton(_) => {}
Widget::OptionalInput(optional_input) => {
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
optional_input.checked = update_value;
let callback_message = (optional_input.on_update.callback)(optional_input);
responses.push_back(callback_message);
}
Widget::RadioInput(radio_input) => {
let update_value = value.as_u64().expect("OptionalInput update was not of type: u64");
radio_input.selected_index = update_value as u32;
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
responses.push_back(callback_message);
}
};
self.send_layout(layout_target, responses);
}
}
}
fn actions(&self) -> crate::message_prelude::ActionList {
actions!()
}
}

5
editor/src/layout/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod layout_message;
pub mod layout_message_handler;
pub mod widgets;
pub use layout_message::{LayoutMessage, LayoutMessageDiscriminant};

View File

@ -0,0 +1,283 @@
use super::layout_message::LayoutTarget;
use crate::message_prelude::*;
use derivative::*;
use serde::{Deserialize, Serialize};
pub trait PropertyHolder {
fn properties(&self) -> WidgetLayout {
WidgetLayout::default()
}
fn register_properties(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
responses.push_back(
LayoutMessage::SendLayout {
layout: self.properties(),
layout_target,
}
.into(),
)
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct WidgetLayout {
pub layout: SubLayout,
}
impl WidgetLayout {
pub fn new(layout: SubLayout) -> Self {
Self { layout }
}
pub fn iter(&self) -> WidgetIter<'_> {
WidgetIter {
stack: self.layout.iter().collect(),
current_slice: None,
}
}
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: self.layout.iter_mut().collect(),
current_slice: None,
}
}
}
pub type SubLayout = Vec<LayoutRow>;
#[remain::sorted]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LayoutRow {
Row { name: String, widgets: Vec<WidgetHolder> },
Section { name: String, layout: SubLayout },
}
impl LayoutRow {
pub fn widgets(&self) -> Vec<WidgetHolder> {
match &self {
Self::Row { name: _, widgets } => widgets.to_vec(),
Self::Section { name: _, layout } => layout.iter().flat_map(|row| row.widgets()).collect(),
}
}
}
#[derive(Debug, Default)]
pub struct WidgetIter<'a> {
pub stack: Vec<&'a LayoutRow>,
pub current_slice: Option<&'a [WidgetHolder]>,
}
impl<'a> Iterator for WidgetIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.current_slice.map(|slice| slice.first()).flatten() {
self.current_slice = Some(&self.current_slice.unwrap()[1..]);
return Some(item);
}
match self.stack.pop() {
Some(LayoutRow::Row { name: _, widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutRow::Section { name: _, layout }) => {
for layout_row in layout {
self.stack.push(layout_row);
}
self.next()
}
None => None,
}
}
}
#[derive(Debug, Default)]
pub struct WidgetIterMut<'a> {
pub stack: Vec<&'a mut LayoutRow>,
pub current_slice: Option<&'a mut [WidgetHolder]>,
}
impl<'a> Iterator for WidgetIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some((first, rest)) = self.current_slice.take().map(|slice| slice.split_first_mut()).flatten() {
self.current_slice = Some(rest);
return Some(first);
};
match self.stack.pop() {
Some(LayoutRow::Row { name: _, widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutRow::Section { name: _, layout }) => {
for layout_row in layout {
self.stack.push(layout_row);
}
self.next()
}
None => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WidgetHolder {
pub widget_id: u64,
pub widget: Widget,
}
impl WidgetHolder {
pub fn new(widget: Widget) -> Self {
Self { widget_id: generate_uuid(), widget }
}
}
#[derive(Clone)]
pub struct WidgetCallback<T> {
pub callback: fn(&T) -> Message,
}
impl<T> WidgetCallback<T> {
pub fn new(callback: fn(&T) -> Message) -> Self {
Self { callback }
}
}
impl<T> Default for WidgetCallback<T> {
fn default() -> Self {
Self { callback: |_| Message::NoOp }
}
}
#[remain::sorted]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Widget {
IconButton(IconButton),
NumberInput(NumberInput),
OptionalInput(OptionalInput),
PopoverButton(PopoverButton),
RadioInput(RadioInput),
Separator(Separator),
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {
pub value: f64,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<NumberInput>,
pub min: Option<f64>,
pub max: Option<f64>,
#[serde(rename = "isInteger")]
pub is_integer: bool,
#[serde(rename = "incrementBehavior")]
pub increment_behavior: NumberInputIncrementBehavior,
#[serde(rename = "incrementFactor")]
#[derivative(Default(value = "1."))]
pub increment_factor: f64,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_increase: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_decrease: WidgetCallback<NumberInput>,
pub label: String,
pub unit: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum NumberInputIncrementBehavior {
Add,
Multiply,
Callback,
}
impl Default for NumberInputIncrementBehavior {
fn default() -> Self {
Self::Add
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Separator {
pub direction: SeparatorDirection,
#[serde(rename = "type")]
pub separator_type: SeparatorType,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SeparatorDirection {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SeparatorType {
Related,
Unrelated,
Section,
List,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct IconButton {
pub icon: String,
#[serde(rename = "title")]
pub tooltip: String,
pub size: u32,
#[serde(rename = "gapAfter")]
pub gap_after: bool,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<IconButton>,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct OptionalInput {
pub checked: bool,
pub icon: String,
#[serde(rename = "title")]
pub tooltip: String,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<OptionalInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct PopoverButton {
pub title: String,
pub text: String,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct RadioInput {
pub entries: Vec<RadioEntryData>,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number
// TODO(mfish33): Replace with usize when using native UI
#[serde(rename = "selectedIndex")]
pub selected_index: u32,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct RadioEntryData {
pub value: String,
pub label: String,
pub icon: String,
pub tooltip: String,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
}

View File

@ -8,6 +8,7 @@ pub mod document;
pub mod frontend;
pub mod global;
pub mod input;
pub mod layout;
pub mod viewport_tools;
#[doc(inline)]
@ -67,6 +68,7 @@ pub mod message_prelude {
pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant};
pub use crate::input::{InputMapperMessage, InputMapperMessageDiscriminant, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant};
pub use crate::layout::{LayoutMessage, LayoutMessageDiscriminant};
pub use crate::misc::derivable_custom_traits::{ToDiscriminant, TransitiveChild};
pub use crate::viewport_tools::tool_message::{ToolMessage, ToolMessageDiscriminant};
pub use crate::viewport_tools::tools::crop::{CropMessage, CropMessageDiscriminant};

View File

@ -41,7 +41,7 @@ macro_rules! count_args {
/// ```
macro_rules! gen_tools_hash_map {
($($enum_variant:ident => $struct_path:ty),* $(,)?) => {{
let mut hash_map: ::std::collections::HashMap<$crate::viewport_tools::tool::ToolType, ::std::boxed::Box<dyn for<'a> $crate::message_prelude::MessageHandler<$crate::viewport_tools::tool_message::ToolMessage,$crate::viewport_tools::tool::ToolActionHandlerData<'a>>>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*));
let mut hash_map: ::std::collections::HashMap<$crate::viewport_tools::tool::ToolType, ::std::boxed::Box<$crate::viewport_tools::tool::Tool>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*));
$(hash_map.insert($crate::viewport_tools::tool::ToolType::$enum_variant, ::std::boxed::Box::new(<$struct_path>::default()));)*
hash_map

View File

@ -2,5 +2,4 @@ pub mod snapping;
pub mod tool;
pub mod tool_message;
pub mod tool_message_handler;
pub mod tool_options;
pub mod tools;

View File

@ -1,8 +1,8 @@
use super::tool_options::{SelectAppendMode, ShapeType, ToolOptions};
use super::tools::*;
use crate::communication::message_handler::MessageHandler;
use crate::document::DocumentMessageHandler;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use graphene::color::Color;
@ -15,6 +15,7 @@ pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentTo
pub trait Fsm {
type ToolData;
type ToolOptions;
#[must_use]
fn transition(
@ -23,6 +24,7 @@ pub trait Fsm {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
messages: &mut VecDeque<Message>,
) -> Self;
@ -35,14 +37,16 @@ pub trait Fsm {
pub struct DocumentToolData {
pub primary_color: Color,
pub secondary_color: Color,
pub tool_options: HashMap<ToolType, ToolOptions>,
}
type SubToolMessageHandler = dyn for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>>;
pub trait ToolCommon: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder {}
impl<T> ToolCommon for T where T: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder {}
type Tool = dyn ToolCommon;
pub struct ToolData {
pub active_tool_type: ToolType,
pub tools: HashMap<ToolType, Box<SubToolMessageHandler>>,
pub tools: HashMap<ToolType, Box<Tool>>,
}
impl fmt::Debug for ToolData {
@ -52,10 +56,11 @@ impl fmt::Debug for ToolData {
}
impl ToolData {
pub fn active_tool_mut(&mut self) -> &mut Box<SubToolMessageHandler> {
pub fn active_tool_mut(&mut self) -> &mut Box<Tool> {
self.tools.get_mut(&self.active_tool_type).expect("The active tool is not initialized")
}
pub fn active_tool(&self) -> &SubToolMessageHandler {
pub fn active_tool(&self) -> &Tool {
self.tools.get(&self.active_tool_type).map(|x| x.as_ref()).expect("The active tool is not initialized")
}
}
@ -98,7 +103,6 @@ impl Default for ToolFsmState {
document_tool_data: DocumentToolData {
primary_color: Color::BLACK,
secondary_color: Color::WHITE,
tool_options: default_tool_options(),
},
}
}
@ -114,35 +118,6 @@ impl ToolFsmState {
}
}
fn default_tool_options() -> HashMap<ToolType, ToolOptions> {
let tool_init = |tool: ToolType| (tool, tool.default_options());
[
tool_init(ToolType::Select),
tool_init(ToolType::Crop),
tool_init(ToolType::Navigate),
tool_init(ToolType::Eyedropper),
tool_init(ToolType::Text),
tool_init(ToolType::Fill),
tool_init(ToolType::Gradient),
tool_init(ToolType::Brush),
tool_init(ToolType::Heal),
tool_init(ToolType::Clone),
tool_init(ToolType::Patch),
tool_init(ToolType::BlurSharpen),
tool_init(ToolType::Relight),
tool_init(ToolType::Path),
tool_init(ToolType::Pen),
tool_init(ToolType::Freehand),
tool_init(ToolType::Spline),
tool_init(ToolType::Line),
tool_init(ToolType::Rectangle),
tool_init(ToolType::Ellipse),
tool_init(ToolType::Shape),
]
.into_iter()
.collect()
}
#[repr(usize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ToolType {
@ -201,36 +176,6 @@ impl fmt::Display for ToolType {
}
}
impl ToolType {
fn default_options(&self) -> ToolOptions {
match self {
ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New },
ToolType::Crop => ToolOptions::Crop {},
ToolType::Navigate => ToolOptions::Navigate {},
ToolType::Eyedropper => ToolOptions::Eyedropper {},
ToolType::Text => ToolOptions::Text { font_size: 14 },
ToolType::Fill => ToolOptions::Fill {},
ToolType::Gradient => ToolOptions::Gradient {},
ToolType::Brush => ToolOptions::Brush {},
ToolType::Heal => ToolOptions::Heal {},
ToolType::Clone => ToolOptions::Clone {},
ToolType::Patch => ToolOptions::Patch {},
ToolType::BlurSharpen => ToolOptions::BlurSharpen {},
ToolType::Relight => ToolOptions::Relight {},
ToolType::Path => ToolOptions::Path {},
ToolType::Pen => ToolOptions::Pen { weight: 5 },
ToolType::Freehand => ToolOptions::Freehand { weight: 5 },
ToolType::Spline => ToolOptions::Spline {},
ToolType::Line => ToolOptions::Line { weight: 5 },
ToolType::Rectangle => ToolOptions::Rectangle {},
ToolType::Ellipse => ToolOptions::Ellipse {},
ToolType::Shape => ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices: 6 },
},
}
}
}
pub enum StandardToolMessageType {
Abort,
DocumentIsDirty,

View File

@ -1,5 +1,4 @@
use super::tool::ToolType;
use super::tool_options::ToolOptions;
use crate::message_prelude::*;
use graphene::color::Color;
@ -92,10 +91,6 @@ pub enum ToolMessage {
SelectSecondaryColor {
color: Color,
},
SetToolOptions {
tool_type: ToolType,
tool_options: ToolOptions,
},
SwapColors,
UpdateCursor,
UpdateHints,

View File

@ -1,6 +1,7 @@
use super::tool::{message_to_tool_type, standard_tool_message, update_working_colors, StandardToolMessageType, ToolFsmState};
use crate::document::DocumentMessageHandler;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget;
use crate::message_prelude::*;
use graphene::color::Color;
@ -60,8 +61,10 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
// Notify the frontend about the new active tool to be displayed
let tool_name = tool_type.to_string();
let tool_options = self.tool_state.document_tool_data.tool_options.get(&tool_type).copied();
responses.push_back(FrontendMessage::UpdateActiveTool { tool_name, tool_options }.into());
responses.push_back(FrontendMessage::UpdateActiveTool { tool_name }.into());
// Send Properties to the frontend
tool_data.tools.get(&tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
}
DocumentIsDirty => {
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
@ -90,11 +93,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
update_working_colors(document_data, responses);
}
SetToolOptions { tool_type, tool_options } => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.tool_options.insert(tool_type, tool_options);
}
SwapColors => {
let document_data = &mut self.tool_state.document_tool_data;
@ -123,7 +121,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
}
fn actions(&self) -> ActionList {
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions);
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool);
list.extend(self.tool_state.tool_data.active_tool().actions());
list

View File

@ -1,40 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ToolOptions {
Select { append_mode: SelectAppendMode },
Crop {},
Navigate {},
Eyedropper {},
Text { font_size: u32 },
Fill {},
Gradient {},
Brush {},
Heal {},
Clone {},
Patch {},
BlurSharpen {},
Relight {},
Path {},
Pen { weight: u32 },
Freehand { weight: u32 },
Spline {},
Line { weight: u32 },
Rectangle {},
Ellipse {},
Shape { shape_type: ShapeType },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum SelectAppendMode {
New,
Add,
Subtract,
Intersect,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ShapeType {
Star { vertices: u32 },
Polygon { vertices: u32 },
}

View File

@ -1,3 +1,4 @@
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::viewport_tools::tool::ToolActionHandlerData;
@ -25,3 +26,5 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
advertise_actions!();
}
impl PropertyHolder for Crop {}

View File

@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -36,6 +37,8 @@ pub enum EllipseMessage {
},
}
impl PropertyHolder for Ellipse {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -48,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -86,6 +89,7 @@ struct EllipseToolData {
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
type ToolOptions = ();
fn transition(
self,
@ -93,6 +97,7 @@ impl Fsm for EllipseToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -32,6 +33,8 @@ pub enum EyedropperMessage {
RightMouseDown,
}
impl PropertyHolder for Eyedropper {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -44,7 +47,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -72,6 +75,7 @@ struct EyedropperToolData {}
impl Fsm for EyedropperToolFsmState {
type ToolData = EyedropperToolData;
type ToolOptions = ();
fn transition(
self,
@ -79,6 +83,7 @@ impl Fsm for EyedropperToolFsmState {
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -32,6 +33,8 @@ pub enum FillMessage {
RightMouseDown,
}
impl PropertyHolder for Fill {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -44,7 +47,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -72,6 +75,7 @@ struct FillToolData {}
impl Fsm for FillToolFsmState {
type ToolData = FillToolData;
type ToolOptions = ();
fn transition(
self,
@ -79,6 +83,7 @@ impl Fsm for FillToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -2,10 +2,10 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
@ -17,6 +17,17 @@ use serde::{Deserialize, Serialize};
pub struct Freehand {
fsm_state: FreehandToolFsmState,
data: FreehandToolData,
options: FreehandOptions,
}
pub struct FreehandOptions {
line_weight: u32,
}
impl Default for FreehandOptions {
fn default() -> Self {
Self { line_weight: 5 }
}
}
#[remain::sorted]
@ -31,6 +42,13 @@ pub enum FreehandMessage {
DragStart,
DragStop,
PointerMove,
UpdateOptions(FreehandMessageOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum FreehandMessageOptionsUpdate {
LineWeight(u32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -39,6 +57,23 @@ enum FreehandToolFsmState {
Drawing,
}
impl PropertyHolder for Freehand {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: self.options.line_weight as f64,
is_integer: true,
min: Some(1.),
on_update: WidgetCallback::new(|number_input| FreehandMessage::UpdateOptions(FreehandMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()),
..NumberInput::default()
}))],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Freehand {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -51,7 +86,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Freehand {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if let ToolMessage::Freehand(FreehandMessage::UpdateOptions(action)) = action {
match action {
FreehandMessageOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -84,6 +126,7 @@ struct FreehandToolData {
impl Fsm for FreehandToolFsmState {
type ToolData = FreehandToolData;
type ToolOptions = FreehandOptions;
fn transition(
self,
@ -91,6 +134,7 @@ impl Fsm for FreehandToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
@ -110,10 +154,7 @@ impl Fsm for FreehandToolFsmState {
data.points.push(pos);
data.weight = match tool_data.tool_options.get(&ToolType::Freehand) {
Some(&ToolOptions::Freehand { weight }) => weight,
_ => 5,
};
data.weight = tool_options.line_weight;
responses.push_back(make_operation(data, tool_data));

View File

@ -4,11 +4,11 @@ use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
@ -20,6 +20,17 @@ use serde::{Deserialize, Serialize};
pub struct Line {
fsm_state: LineToolFsmState,
data: LineToolData,
options: LineOptions,
}
pub struct LineOptions {
line_weight: u32,
}
impl Default for LineOptions {
fn default() -> Self {
Self { line_weight: 5 }
}
}
#[remain::sorted]
@ -38,6 +49,30 @@ pub enum LineMessage {
lock_angle: Key,
snap_angle: Key,
},
UpdateOptions(LineOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum LineOptionsUpdate {
LineWeight(u32),
}
impl PropertyHolder for Line {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: self.options.line_weight as f64,
is_integer: true,
min: Some(0.),
on_update: WidgetCallback::new(|number_input| LineMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()),
..NumberInput::default()
}))],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
@ -52,7 +87,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if let ToolMessage::Line(LineMessage::UpdateOptions(action)) = action {
match action {
LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -95,6 +137,7 @@ struct LineToolData {
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
type ToolOptions = LineOptions;
fn transition(
self,
@ -102,6 +145,7 @@ impl Fsm for LineToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
@ -118,10 +162,7 @@ impl Fsm for LineToolFsmState {
data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.weight = match tool_data.tool_options.get(&ToolType::Line) {
Some(&ToolOptions::Line { weight }) => weight,
_ => 5,
};
data.weight = tool_options.line_weight;
responses.push_back(
Operation::AddLine {

View File

@ -2,6 +2,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -37,6 +38,8 @@ pub enum NavigateMessage {
ZoomCanvasBegin,
}
impl PropertyHolder for Navigate {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -49,7 +52,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -89,6 +92,7 @@ struct NavigateToolData {
impl Fsm for NavigateToolFsmState {
type ToolData = NavigateToolData;
type ToolOptions = ();
fn transition(
self,
@ -96,6 +100,7 @@ impl Fsm for NavigateToolFsmState {
_document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
messages: &mut VecDeque<Message>,
) -> Self {

View File

@ -4,6 +4,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -38,6 +39,8 @@ pub enum PathMessage {
PointerMove,
}
impl PropertyHolder for Path {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -50,7 +53,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -105,6 +108,7 @@ struct PathToolSelection {
impl Fsm for PathToolFsmState {
type ToolData = PathToolData;
type ToolOptions = ();
fn transition(
self,
@ -112,6 +116,7 @@ impl Fsm for PathToolFsmState {
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -2,11 +2,11 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
@ -18,6 +18,17 @@ use serde::{Deserialize, Serialize};
pub struct Pen {
fsm_state: PenToolFsmState,
data: PenToolData,
options: PenOptions,
}
pub struct PenOptions {
line_weight: u32,
}
impl Default for PenOptions {
fn default() -> Self {
Self { line_weight: 5 }
}
}
#[remain::sorted]
@ -34,6 +45,7 @@ pub enum PenMessage {
DragStop,
PointerMove,
Undo,
UpdateOptions(PenOptionsUpdate),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -42,6 +54,29 @@ enum PenToolFsmState {
Drawing,
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PenOptionsUpdate {
LineWeight(u32),
}
impl PropertyHolder for Pen {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: self.options.line_weight as f64,
is_integer: true,
min: Some(0.),
on_update: WidgetCallback::new(|number_input| PenMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()),
..NumberInput::default()
}))],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -54,7 +89,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if let ToolMessage::Pen(PenMessage::UpdateOptions(action)) = action {
match action {
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -89,6 +131,7 @@ struct PenToolData {
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
type ToolOptions = PenOptions;
fn transition(
self,
@ -96,6 +139,7 @@ impl Fsm for PenToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
@ -119,10 +163,7 @@ impl Fsm for PenToolFsmState {
data.points.push(pos);
data.next_point = pos;
data.weight = match tool_data.tool_options.get(&ToolType::Pen) {
Some(&ToolOptions::Pen { weight }) => weight,
_ => 5,
};
data.weight = tool_options.line_weight;
responses.push_back(make_operation(data, tool_data, true));

View File

@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
@ -36,6 +37,8 @@ pub enum RectangleMessage {
},
}
impl PropertyHolder for Rectangle {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -48,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -85,6 +88,7 @@ struct RectangleToolData {
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
type ToolOptions = ();
fn transition(
self,
@ -92,6 +96,7 @@ impl Fsm for RectangleToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -5,6 +5,7 @@ use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{IconButton, LayoutRow, PopoverButton, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
@ -51,6 +52,179 @@ pub enum SelectMessage {
},
}
impl PropertyHolder for Select {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignLeft".into(),
tooltip: "Align Left".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Min,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignHorizontalCenter".into(),
tooltip: "Align Horizontal Center".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Center,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignRight".into(),
tooltip: "Align Right".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Max,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Unrelated,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignTop".into(),
tooltip: "Align Top".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Min,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignVerticalCenter".into(),
tooltip: "Align Vertical Center".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Center,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignBottom".into(),
tooltip: "Align Bottom".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Max,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Align".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Section,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "FlipHorizontal".into(),
tooltip: "Flip Horizontal".into(),
size: 24,
on_update: WidgetCallback::new(|_| SelectMessage::FlipHorizontal.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "FlipVertical".into(),
tooltip: "Flip Vertical".into(),
size: 24,
on_update: WidgetCallback::new(|_| SelectMessage::FlipVertical.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Flip".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Section,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanUnion".into(),
tooltip: "Boolean Union".into(),
size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractFront".into(),
tooltip: "Boolean Subtract Front".into(),
size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractBack".into(),
tooltip: "Boolean Subtract Back".into(),
size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanIntersect".into(),
tooltip: "Boolean Intersect".into(),
size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanDifference".into(),
tooltip: "Boolean Difference".into(),
size: 24,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
title: "Boolean".into(),
text: "The contents of this popover menu are coming soon".into(),
})),
],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
@ -63,7 +237,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -141,6 +315,7 @@ fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
type ToolOptions = ();
fn transition(
self,
@ -148,6 +323,7 @@ impl Fsm for SelectToolFsmState {
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {

View File

@ -3,10 +3,10 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::{ShapeType, ToolOptions};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
@ -18,6 +18,17 @@ use serde::{Deserialize, Serialize};
pub struct Shape {
fsm_state: ShapeToolFsmState,
data: ShapeToolData,
options: ShapeOptions,
}
pub struct ShapeOptions {
vertices: u8,
}
impl Default for ShapeOptions {
fn default() -> Self {
Self { vertices: 6 }
}
}
#[remain::sorted]
@ -35,6 +46,30 @@ pub enum ShapeMessage {
center: Key,
lock_ratio: Key,
},
UpdateOptions(ShapeOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ShapeOptionsUpdate {
Vertices(u8),
}
impl PropertyHolder for Shape {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
label: "Sides".into(),
value: self.options.vertices as f64,
is_integer: true,
min: Some(3.),
max: Some(256.),
on_update: WidgetCallback::new(|number_input| ShapeMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()),
..NumberInput::default()
}))],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
@ -49,7 +84,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if let ToolMessage::Shape(ShapeMessage::UpdateOptions(action)) = action {
match action {
ShapeOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices,
}
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -87,6 +129,7 @@ struct ShapeToolData {
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
type ToolOptions = ShapeOptions;
fn transition(
self,
@ -94,6 +137,7 @@ impl Fsm for ShapeToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
@ -109,12 +153,7 @@ impl Fsm for ShapeToolFsmState {
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.sides = match tool_data.tool_options.get(&ToolType::Shape) {
Some(&ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices },
}) => vertices as u8,
_ => 6,
};
data.sides = tool_options.vertices;
responses.push_back(
Operation::AddNgon {

View File

@ -3,10 +3,10 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use glam::{DAffine2, DVec2};
use graphene::intersection::Quad;
@ -19,6 +19,17 @@ use serde::{Deserialize, Serialize};
pub struct Text {
fsm_state: TextToolFsmState,
data: TextToolData,
options: TextOptions,
}
pub struct TextOptions {
font_size: u32,
}
impl Default for TextOptions {
fn default() -> Self {
Self { font_size: 14 }
}
}
#[remain::sorted]
@ -41,6 +52,30 @@ pub enum TextMessage {
UpdateBounds {
new_text: String,
},
UpdateOptions(TextOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum TextOptionsUpdate {
FontSize(u32),
}
impl PropertyHolder for Text {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
name: "".into(),
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Font Size".into(),
value: self.options.font_size as f64,
is_integer: true,
min: Some(1.),
on_update: WidgetCallback::new(|number_input| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
..NumberInput::default()
}))],
}])
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Text {
@ -55,7 +90,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Text {
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if let ToolMessage::Text(TextMessage::UpdateOptions(action)) = action {
match action {
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
}
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
@ -137,6 +179,7 @@ fn update_overlays(document: &DocumentMessageHandler, data: &mut TextToolData, r
impl Fsm for TextToolFsmState {
type ToolData = TextToolData;
type ToolOptions = TextOptions;
fn transition(
self,
@ -144,6 +187,7 @@ impl Fsm for TextToolFsmState {
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
@ -200,10 +244,7 @@ impl Fsm for TextToolFsmState {
// Creating new text
else if state == TextToolFsmState::Ready {
let transform = DAffine2::from_translation(input.mouse.position).to_cols_array();
let font_size = match tool_data.tool_options.get(&ToolType::Text) {
Some(&ToolOptions::Text { font_size }) => font_size,
_ => 14,
};
let font_size = tool_options.font_size;
data.path = vec![generate_uuid()];
responses.push_back(

View File

@ -6,67 +6,12 @@
<Separator :type="'Section'" />
<ToolOptions :activeTool="activeTool" :activeToolOptions="activeToolOptions" />
<WidgetLayout :layout="toolOptionsLayout" />
</LayoutRow>
<LayoutRow class="spacer"></LayoutRow>
<LayoutRow class="right side">
<OptionalInput v-model:checked="snappingEnabled" @update:checked="(snap: boolean) => setSnapping(snap)" :icon="'Snapping'" title="Snapping" />
<PopoverButton>
<h3>Snapping</h3>
<p>The contents of this popover menu are coming soon</p>
</PopoverButton>
<Separator :type="'Unrelated'" />
<OptionalInput v-model:checked="gridEnabled" @update:checked="() => dialog.comingSoon(318)" :icon="'Grid'" title="Grid" />
<PopoverButton>
<h3>Grid</h3>
<p>The contents of this popover menu are coming soon</p>
</PopoverButton>
<Separator :type="'Unrelated'" />
<OptionalInput v-model:checked="overlaysEnabled" @update:checked="(visible: boolean) => setOverlaysVisibility(visible)" :icon="'Overlays'" title="Overlays" />
<PopoverButton>
<h3>Overlays</h3>
<p>The contents of this popover menu are coming soon</p>
</PopoverButton>
<Separator :type="'Unrelated'" />
<RadioInput :entries="viewModeEntries" v-model:selectedIndex="viewModeIndex" class="combined-after" />
<PopoverButton>
<h3>View Mode</h3>
<p>The contents of this popover menu are coming soon</p>
</PopoverButton>
<Separator :type="'Section'" />
<NumberInput @update:value="(newRotation: number) => setRotation(newRotation)" v-model:value="documentRotation" :incrementFactor="15" :unit="'°'" />
<Separator :type="'Section'" />
<IconButton :action="increaseCanvasZoom" :icon="'ZoomIn'" :size="24" title="Zoom In" />
<IconButton :action="decreaseCanvasZoom" :icon="'ZoomOut'" :size="24" title="Zoom Out" />
<IconButton :action="() => setCanvasZoom(100)" :icon="'ZoomReset'" :size="24" title="Zoom to 100%" />
<Separator :type="'Related'" />
<NumberInput
v-model:value="documentZoom"
@update:value="(newZoom: number) => setCanvasZoom(newZoom)"
:min="0.000001"
:max="1000000"
:incrementBehavior="'Callback'"
:incrementCallbackIncrease="increaseCanvasZoom"
:incrementCallbackDecrease="decreaseCanvasZoom"
:unit="'%'"
:displayDecimalPlaces="4"
ref="zoom"
/>
</LayoutRow>
<WidgetLayout :layout="documentBarLayout" class="right side" />
</LayoutRow>
<LayoutRow class="shelf-and-viewport">
<LayoutCol class="shelf">
@ -292,6 +237,9 @@ import {
ToolName,
UpdateDocumentArtboards,
UpdateMouseCursor,
UpdateToolOptionsLayout,
defaultWidgetLayout,
UpdateDocumentBarLayout,
TriggerTextCommit,
DisplayRemoveEditableTextbox,
DisplayEditableTextbox,
@ -300,31 +248,19 @@ import {
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput, { RadioEntries } from "@/components/widgets/inputs/RadioInput.vue";
import { RadioEntries } from "@/components/widgets/inputs/RadioInput.vue";
import ShelfItemInput from "@/components/widgets/inputs/ShelfItemInput.vue";
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
import ToolOptions from "@/components/widgets/options/ToolOptions.vue";
import CanvasRuler from "@/components/widgets/rulers/CanvasRuler.vue";
import PersistentScrollbar from "@/components/widgets/scrollbars/PersistentScrollbar.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
export default defineComponent({
inject: ["editor", "dialog"],
methods: {
setSnapping(snap: boolean) {
this.editor.instance.set_snapping(snap);
},
setOverlaysVisibility(visible: boolean) {
this.editor.instance.set_overlays_visibility(visible);
},
setViewMode(newViewMode: string) {
this.editor.instance.set_view_mode(newViewMode);
},
viewportResize() {
const canvas = this.$refs.canvas as HTMLElement;
// Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing
@ -336,18 +272,6 @@ export default defineComponent({
this.canvasSvgWidth = `${width}px`;
this.canvasSvgHeight = `${height}px`;
},
setCanvasZoom(newZoom: number) {
this.editor.instance.set_canvas_zoom(newZoom / 100);
},
increaseCanvasZoom() {
this.editor.instance.increase_canvas_zoom();
},
decreaseCanvasZoom() {
this.editor.instance.decrease_canvas_zoom();
},
setRotation(newRotation: number) {
this.editor.instance.set_rotation(newRotation * (Math.PI / 180));
},
translateCanvasX(newValue: number) {
const delta = newValue - this.scrollbarPos.x;
this.scrollbarPos.x = newValue;
@ -440,7 +364,6 @@ export default defineComponent({
this.editor.dispatcher.subscribeJsMessage(UpdateActiveTool, (updateActiveTool) => {
this.activeTool = updateActiveTool.tool_name;
this.activeToolOptions = updateActiveTool.tool_options;
});
this.editor.dispatcher.subscribeJsMessage(UpdateCanvasZoom, (updateCanvasZoom) => {
@ -481,6 +404,14 @@ export default defineComponent({
);
});
this.editor.dispatcher.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => {
this.toolOptionsLayout = updateToolOptionsLayout;
});
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => {
this.documentBarLayout = updateDocumentBarLayout;
});
window.addEventListener("resize", this.viewportResize);
window.addEventListener("DOMContentLoaded", this.viewportResize);
},
@ -506,7 +437,8 @@ export default defineComponent({
canvasSvgHeight: "100%",
canvasCursor: "default",
activeTool: "Select" as ToolName,
activeToolOptions: {},
toolOptionsLayout: defaultWidgetLayout(),
documentBarLayout: defaultWidgetLayout(),
documentModeEntries,
viewModeEntries,
documentModeSelectionIndex: 0,
@ -534,12 +466,8 @@ export default defineComponent({
PersistentScrollbar,
CanvasRuler,
IconButton,
PopoverButton,
RadioInput,
NumberInput,
DropdownInput,
OptionalInput,
ToolOptions,
WidgetLayout,
},
});
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="widget-layout">
<template v-for="(layoutRow, index) in layout.layout" :key="index">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target"></component>
</template>
</div>
</template>
<style lang="scss">
.widget-layout {
height: 100%;
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/dispatcher/js-messages";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
import WidgetSection from "@/components/widgets/WidgetSection.vue";
export default defineComponent({
props: {
layout: { type: Object as PropType<WidgetLayout>, required: true },
},
methods: {
layoutRowType(layoutRow: LayoutRow): unknown {
if (isWidgetRow(layoutRow)) return WidgetRow;
if (isWidgetSection(layoutRow)) return WidgetSection;
throw new Error("Layout row type does not exist");
},
},
data: () => {
return {
isWidgetRow,
isWidgetSection,
};
},
components: {
WidgetRow,
WidgetSection,
},
});
</script>

View File

@ -0,0 +1,66 @@
<template>
<div class="widget-row">
<template v-for="(component, index) in widgetData.widgets" :key="index">
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
<PopoverButton v-if="component.kind === 'PopoverButton'">
<h3>{{ component.props.title }}</h3>
<p>{{ component.props.text }}</p>
</PopoverButton>
<NumberInput
v-if="component.kind === 'NumberInput'"
v-bind="component.props"
@update:value="(value: number) => updateLayout(component.widget_id, value)"
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
/>
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
</template>
</div>
</template>
<style lang="scss">
.widget-row {
height: 100%;
flex: 0 0 auto;
display: flex;
align-items: center;
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { WidgetRow } from "@/dispatcher/js-messages";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export default defineComponent({
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetRow>, required: true },
layoutTarget: { required: true },
},
methods: {
updateLayout(widgetId: BigInt, value: unknown) {
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
},
},
components: {
Separator,
PopoverButton,
NumberInput,
IconButton,
OptionalInput,
RadioInput,
},
});
</script>

View File

@ -0,0 +1,56 @@
<!-- TODO: Implement collapsable sections with properties system -->
<template>
<div class="widget-section">
<template v-for="(layoutRow, index) in widgetData.layout" :key="index">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget"></component>
</template>
</div>
</template>
<style lang="scss">
.widget-section {
height: 100%;
flex: 0 0 auto;
display: flex;
align-items: center;
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetSection as WidgetSectionFromJsMessages } from "@/dispatcher/js-messages";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
const WidgetSection = defineComponent({
name: "WidgetSection",
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetSectionFromJsMessages>, required: true },
layoutTarget: { required: true },
},
data: () => {
return {
isWidgetRow,
isWidgetSection,
};
},
methods: {
updateLayout(widgetId: BigInt, value: unknown) {
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
},
layoutRowType(layoutRow: LayoutRow): unknown {
if (isWidgetRow(layoutRow)) return WidgetRow;
if (isWidgetSection(layoutRow)) return WidgetSection;
throw new Error("Layout row type does not exist");
},
},
components: {
WidgetRow,
},
});
export default WidgetSection;
</script>

View File

@ -167,7 +167,7 @@ export default defineComponent({
},
data() {
return {
text: `${this.value}${this.unit}`,
text: this.generateText(this.value),
editing: false,
id: `${Math.random()}`.substring(2),
};
@ -230,9 +230,9 @@ export default defineComponent({
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);
this.setText(sanitized);
this.text = this.generateText(sanitized);
},
setText(value: number) {
generateText(value: number): string {
// Find the amount of digits on the left side of the decimal
// 10.25 == 2
// 1.23 == 1
@ -240,7 +240,7 @@ export default defineComponent({
const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value);
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
const displayValue = Math.round(value * roundingPower) / roundingPower;
this.text = `${displayValue}${this.unit}`;
return `${displayValue}${this.unit}`;
},
},
watch: {
@ -254,7 +254,7 @@ export default defineComponent({
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);
this.setText(sanitized);
this.text = this.generateText(sanitized);
},
},
mounted() {

View File

@ -1,188 +0,0 @@
<template>
<LayoutRow class="tool-options">
<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'"
@update:value="(value: number) => 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>
</LayoutRow>
</template>
<style lang="scss">
.tool-options {
height: 100%;
flex: 0 0 auto;
align-items: center;
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { ToolName } from "@/dispatcher/js-messages";
import { WidgetRow, IconButtonWidget } from "@/utilities/widgets";
import LayoutRow from "@/components/layout/LayoutRow.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";
import Separator from "@/components/widgets/separators/Separator.vue";
export default defineComponent({
inject: ["editor", "dialog"],
props: {
activeTool: { type: String as PropType<ToolName>, required: true },
activeToolOptions: { type: Object as PropType<Record<string, object>>, required: true },
},
methods: {
async updateToolOptions(path: string[], newValue: number) {
this.setToolOption(path, newValue);
this.editor.instance.set_tool_options(this.activeTool || "", this.activeToolOptions);
},
async sendToolMessage(message: string | object) {
this.editor.instance.send_tool_message(this.activeTool || "", message);
},
// Traverses the given path and returns the direct parent of the option
getRecordContainingOption(optionPath: string[]): Record<string, number> {
// TODO: Formalize types and avoid casting with `as`
let currentRecord = this.activeToolOptions as Record<string, object | number>;
const allButLastOptions = optionPath.slice(0, -1);
[this.activeTool || "", ...allButLastOptions].forEach((attr) => {
// Dig into the tree in each loop iteration
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) {
if (option.message) {
this.sendToolMessage(option.message);
return;
}
if (option.callback) {
option.callback();
return;
}
this.dialog.comingSoon();
},
},
data() {
const toolOptionsWidgets: Record<ToolName, WidgetRow> = {
Select: [
{ kind: "IconButton", message: { Align: { axis: "X", aggregate: "Min" } }, tooltip: "Align Left", props: { icon: "AlignLeft", size: 24 } },
{ kind: "IconButton", message: { Align: { axis: "X", aggregate: "Center" } }, tooltip: "Align Horizontal Center", props: { icon: "AlignHorizontalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: { axis: "X", aggregate: "Max" } }, tooltip: "Align Right", props: { icon: "AlignRight", size: 24 } },
{ kind: "Separator", props: { type: "Unrelated" } },
{ kind: "IconButton", message: { Align: { axis: "Y", aggregate: "Min" } }, tooltip: "Align Top", props: { icon: "AlignTop", size: 24 } },
{ kind: "IconButton", message: { Align: { axis: "Y", aggregate: "Center" } }, tooltip: "Align Vertical Center", props: { icon: "AlignVerticalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: { axis: "Y", aggregate: "Max" } }, tooltip: "Align Bottom", props: { icon: "AlignBottom", size: 24 } },
{ kind: "Separator", props: { type: "Related" } },
{
kind: "PopoverButton",
popover: {
title: "Align",
text: "The contents of this popover menu are coming soon",
},
props: {},
},
{ kind: "Separator", props: { type: "Section" } },
{ 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", props: { type: "Related" } },
{
kind: "PopoverButton",
popover: {
title: "Flip",
text: "The contents of this popover menu are coming soon",
},
props: {},
},
{ kind: "Separator", props: { type: "Section" } },
{ kind: "IconButton", tooltip: "Boolean Union", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
{ kind: "Separator", props: { type: "Related" } },
{
kind: "PopoverButton",
popover: {
title: "Boolean",
text: "The contents of this popover menu are coming soon",
},
props: {},
},
],
Crop: [],
Navigate: [],
Eyedropper: [],
Text: [{ kind: "NumberInput", optionPath: ["font_size"], props: { min: 1, isInteger: true, unit: " px", label: "Font size" } }],
Fill: [],
Gradient: [],
Brush: [],
Heal: [],
Clone: [],
Patch: [],
Detail: [],
Relight: [],
Path: [],
Pen: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
Freehand: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
Spline: [],
Line: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }],
Rectangle: [],
Ellipse: [],
Shape: [{ kind: "NumberInput", optionPath: ["shape_type", "Polygon", "vertices"], props: { min: 3, isInteger: true, label: "Sides" } }],
};
return {
toolOptionsWidgets,
};
},
components: {
Separator,
IconButton,
PopoverButton,
NumberInput,
LayoutRow,
},
});
</script>

View File

@ -137,8 +137,6 @@ export type ToolName =
export class UpdateActiveTool extends JsMessage {
readonly tool_name!: ToolName;
readonly tool_options!: object;
}
export class UpdateActiveDocument extends JsMessage {
@ -386,6 +384,87 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage {
document_id!: string;
}
export interface WidgetLayout {
layout_target: unknown;
layout: LayoutRow[];
}
export function defaultWidgetLayout(): WidgetLayout {
return {
layout: [],
layout_target: null,
};
}
export type LayoutRow = WidgetRow | WidgetSection;
export type WidgetRow = { name: string; widgets: Widget[] };
export function isWidgetRow(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetRow {
return Boolean((layoutRow as WidgetRow).widgets);
}
export type WidgetSection = { name: string; layout: LayoutRow[] };
export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetSection {
return Boolean((layoutRow as WidgetSection).layout);
}
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput";
export interface Widget {
kind: WidgetKind;
widget_id: BigInt;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props: any;
}
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
}
export class UpdateDocumentBarLayout extends JsMessage {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
}
// Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return widgetLayout.map((rowOrSection) => {
if (rowOrSection.Row) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
const { widget_id } = widgetHolder;
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
return { widget_id, kind, props };
});
return {
name: rowOrSection.Row.name,
widgets,
};
}
if (rowOrSection.Section) {
return {
name: rowOrSection.Section.name,
layout: createWidgetLayout(rowOrSection.Section),
};
}
throw new Error("Layout row type does not exist");
});
}
export class DisplayDialogComingSoon extends JsMessage {
issue: number | undefined;
}
export class TriggerTextCommit extends JsMessage {}
// Any is used since the type of the object should be known from the rust side
@ -421,5 +500,8 @@ export const messageConstructors: Record<string, MessageMaker> = {
TriggerIndexedDbRemoveDocument,
TriggerTextCommit,
UpdateDocumentArtboards,
UpdateToolOptionsLayout,
DisplayDialogComingSoon,
UpdateDocumentBarLayout,
} as const;
export type JsMessageType = keyof typeof messageConstructors;

View File

@ -1,6 +1,6 @@
import { reactive, readonly } from "vue";
import { DisplayDialogAboutGraphite } from "@/dispatcher/js-messages";
import { DisplayDialogAboutGraphite, DisplayDialogComingSoon } from "@/dispatcher/js-messages";
import { EditorState } from "@/state/wasm-loader";
import { IconName } from "@/utilities/icons";
import { stripIndents } from "@/utilities/strip-indents";
@ -107,6 +107,7 @@ export function createDialogState(editor: EditorState) {
// Run on creation
editor.dispatcher.subscribeJsMessage(DisplayDialogAboutGraphite, () => onAboutHandler());
editor.dispatcher.subscribeJsMessage(DisplayDialogComingSoon, (displayDialogComingSoon) => comingSoon(displayDialogComingSoon.issue));
return {
state: readonly(state),

View File

@ -1,9 +1,5 @@
import { IconName, IconSize } from "@/utilities/icons";
export type Widgets = TextButtonWidget | IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget;
export type WidgetRow = Widgets[];
export type WidgetLayout = WidgetRow[];
// Text Button
export interface TextButtonWidget {
kind: "TextButton";

View File

@ -3,7 +3,7 @@
// on the dispatcher messaging system and more complex Rust data types.
use crate::helpers::Error;
use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type, translate_view_mode};
use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type};
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES};
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
@ -12,14 +12,13 @@ use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::message_prelude::*;
use editor::misc::EditorError;
use editor::viewport_tools::tool::ToolType;
use editor::viewport_tools::tool_options::ToolOptions;
use editor::viewport_tools::tools;
use editor::Color;
use editor::Editor;
use editor::LayerId;
use serde::Serialize;
use serde_wasm_bindgen;
use serde_wasm_bindgen::{self, from_value};
use std::sync::atomic::Ordering;
use wasm_bindgen::prelude::*;
@ -105,19 +104,15 @@ impl JsEditorHandle {
}
}
/// Update the options for a given tool
pub fn set_tool_options(&self, tool: String, options: &JsValue) -> Result<(), JsValue> {
match serde_wasm_bindgen::from_value::<ToolOptions>(options.clone()) {
Ok(tool_options) => match translate_tool_type(&tool) {
Some(tool_type) => {
let message = ToolMessage::SetToolOptions { tool_type, tool_options };
self.dispatch(message);
Ok(())
}
None => Err(Error::new(&format!("Couldn't set options for {} because it was not recognized as a valid tool", tool)).into()),
},
Err(err) => Err(Error::new(&format!("Invalid JSON for ToolOptions: {}", err)).into()),
/// Update layout of a given UI
pub fn update_layout(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
match (from_value(layout_target), from_value(value)) {
(Ok(layout_target), Ok(value)) => {
let message = LayoutMessage::UpdateLayout { layout_target, widget_id, value };
self.dispatch(message);
Ok(())
}
_ => Err(Error::new("Could not update UI").into()),
}
}
@ -217,7 +212,6 @@ impl JsEditorHandle {
self.dispatch(message);
}
#[wasm_bindgen]
pub fn request_about_graphite_dialog(&self) {
let message = PortfolioMessage::RequestAboutGraphiteDialog;
self.dispatch(message);
@ -225,7 +219,6 @@ impl JsEditorHandle {
/// Send new bounds when document panel viewports get resized or moved within the editor
/// [left, top, right, bottom]...
#[wasm_bindgen]
pub fn bounds_of_viewports(&self, bounds_of_viewports: &[f64]) {
let chunked: Vec<_> = bounds_of_viewports.chunks(4).map(ViewportBounds::from_slice).collect();
@ -454,52 +447,6 @@ impl JsEditorHandle {
self.dispatch(message);
}
/// Set snapping on or off
pub fn set_snapping(&self, snap: bool) {
let message = DocumentMessage::SetSnapping { snap };
self.dispatch(message);
}
/// Set display of overlays on or off
pub fn set_overlays_visibility(&self, visible: bool) {
let message = DocumentMessage::SetOverlaysVisibility { visible };
self.dispatch(message);
}
/// Set the view mode to change the way layers are drawn in the viewport
pub fn set_view_mode(&self, view_mode: String) -> Result<(), JsValue> {
if let Some(view_mode) = translate_view_mode(view_mode.as_str()) {
self.dispatch(DocumentMessage::SetViewMode { view_mode });
Ok(())
} else {
Err(Error::new("Invalid view mode").into())
}
}
/// Sets the zoom to the value
pub fn set_canvas_zoom(&self, zoom_factor: f64) {
let message = MovementMessage::SetCanvasZoom { zoom_factor };
self.dispatch(message);
}
/// Zoom in to the next step
pub fn increase_canvas_zoom(&self) {
let message = MovementMessage::IncreaseCanvasZoom { center_on_mouse: false };
self.dispatch(message);
}
/// Zoom out to the next step
pub fn decrease_canvas_zoom(&self) {
let message = MovementMessage::DecreaseCanvasZoom { center_on_mouse: false };
self.dispatch(message);
}
/// Sets the rotation to the new value (in radians)
pub fn set_rotation(&self, angle_radians: f64) {
let message = MovementMessage::SetCanvasRotation { angle_radians };
self.dispatch(message);
}
/// Translates document (in viewport coords)
pub fn translate_canvas(&self, delta_x: f64, delta_y: f64) {
let message = MovementMessage::TranslateCanvas { delta: (delta_x, delta_y).into() };

View File

@ -3,7 +3,6 @@ use crate::helpers::match_string_to_enum;
use editor::input::keyboard::Key;
use editor::viewport_tools::tool::ToolType;
use graphene::layers::blend_mode::BlendMode;
use graphene::layers::style::ViewMode;
pub fn translate_tool_type(name: &str) -> Option<ToolType> {
use ToolType::*;
@ -130,12 +129,3 @@ pub fn translate_key(name: &str) -> Key {
_ => UnknownKey,
}
}
pub fn translate_view_mode(name: &str) -> Option<ViewMode> {
Some(match name {
"Normal" => ViewMode::Normal,
"Outline" => ViewMode::Outline,
"Pixels" => ViewMode::Pixels,
_ => return None,
})
}