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:
parent
121a68ad3c
commit
96d3ef2650
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ pub enum Message {
|
|||
#[child]
|
||||
InputPreprocessor(InputPreprocessorMessage),
|
||||
#[child]
|
||||
Layout(LayoutMessage),
|
||||
#[child]
|
||||
Portfolio(PortfolioMessage),
|
||||
#[child]
|
||||
Tool(ToolMessage),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -60,5 +60,6 @@ pub enum PortfolioMessage {
|
|||
SelectDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
UpdateDocumentBar,
|
||||
UpdateOpenDocumentsList,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pub mod layout_message;
|
||||
pub mod layout_message_handler;
|
||||
pub mod widgets;
|
||||
|
||||
pub use layout_message::{LayoutMessage, LayoutMessageDiscriminant};
|
||||
|
|
@ -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<()>,
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue