mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 07:38:11 +08:00
Move layouts definitions to backend and fix Firefox overlay scrollbars (#647)
* Fix two-axis scrollbars in scrollable regions on Firefox * Move Document Mode dropdown to the backend; and related code cleanup * Port the Layer Tree options bar layout to the backend * Port the tool shelf to the backend * Clean up initialization and wasm wrapper * Fix crash * Fix missing document bar * Remove unused functions in api.rs * Code review * Tool initalisation * Remove some frontend functions * Initalise -> Init so en-US/GB doesn't have to matter :) * Remove blend_mode and opacity from LayerPanelEntry Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
co-authored by
0hypercube
parent
95435d8bf1
commit
a8f09da5a2
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"[rust]": {
|
"[rust]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnPaste": true,
|
"editor.formatOnPaste": true,
|
||||||
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
"editor.defaultFormatter": "matklad.rust-analyzer",
|
||||||
},
|
},
|
||||||
// Web: save on format
|
// Web: save on format
|
||||||
"[typescript][javascript][vue]": {
|
"[typescript][javascript][vue]": {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
|||||||
ArtboardMessageDiscriminant::RenderArtboards,
|
ArtboardMessageDiscriminant::RenderArtboards,
|
||||||
))),
|
))),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerDetails),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList),
|
||||||
MessageDiscriminant::Tool(ToolMessageDiscriminant::DocumentIsDirty),
|
MessageDiscriminant::Tool(ToolMessageDiscriminant::DocumentIsDirty),
|
||||||
@@ -142,12 +142,7 @@ impl Dispatcher {
|
|||||||
fn log_message(&self, message: &Message) {
|
fn log_message(&self, message: &Message) {
|
||||||
use Message::*;
|
use Message::*;
|
||||||
|
|
||||||
if log::max_level() == log::LevelFilter::Trace
|
if log::max_level() == log::LevelFilter::Trace && !(matches!(message, InputPreprocessor(_)) || MessageDiscriminant::from(message).local_name().ends_with("PointerMove")) {
|
||||||
&& !(matches!(
|
|
||||||
message,
|
|
||||||
InputPreprocessor(_) | Frontend(FrontendMessage::UpdateCanvasZoom { .. }) | Frontend(FrontendMessage::UpdateCanvasRotation { .. })
|
|
||||||
) || MessageDiscriminant::from(message).local_name().ends_with("PointerMove"))
|
|
||||||
{
|
|
||||||
log::trace!("Message: {:?}", message);
|
log::trace!("Message: {:?}", message);
|
||||||
// log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions()));
|
// log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions()));
|
||||||
}
|
}
|
||||||
@@ -447,6 +442,8 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_if_graphite_file_version_upgrade_is_needed() {
|
fn check_if_graphite_file_version_upgrade_is_needed() {
|
||||||
|
use crate::layout::widgets::{LayoutRow, TextLabel, Widget};
|
||||||
|
|
||||||
init_logger();
|
init_logger();
|
||||||
set_uuid_seed(0);
|
set_uuid_seed(0);
|
||||||
let mut editor = Editor::new();
|
let mut editor = Editor::new();
|
||||||
@@ -458,8 +455,8 @@ mod test {
|
|||||||
|
|
||||||
for response in responses {
|
for response in responses {
|
||||||
if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response {
|
if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response {
|
||||||
if let crate::layout::widgets::LayoutRow::Row { widgets } = &layout[0] {
|
if let LayoutRow::Row { widgets } = &layout[0] {
|
||||||
if let crate::layout::widgets::Widget::TextLabel(crate::layout::widgets::TextLabel { value, .. }) = &widgets[0].widget {
|
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
|
||||||
println!();
|
println!();
|
||||||
println!("-------------------------------------------------");
|
println!("-------------------------------------------------");
|
||||||
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
|
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"graphene_document":{"font_cache": {"data":{},"default":null}, "root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":919378319526168453,"layer_ids":[919378319526168452],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[303.890625,0.0,-0.0,362.10546875],"translation":[-148.83984375,-235.8828125]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":0,"name":"Untitled Document","layer_metadata":[[[919378319526168452],{"selected":true,"expanded":false}],[[],{"selected":false,"expanded":true}]],"layer_range_selection_reference":[919378319526168452],"movement_handler":{"pan":[-118.8,-45.60000000000001],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"font_cache": {"data": {}}, "root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[919378319526168452],"Artwork"]},"overlays_visible":true,"snapping_enabled":true,"view_mode":"Normal","version":"0.0.7"}
|
{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":9872175665704159397,"layer_ids":[9872175665704159396],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[376.0,0.0,-0.0,214.0],"translation":[903.0,393.0]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},"font_cache":{"data":{},"default_font":null}},"saved_document_identifier":15130871412783076140,"name":"Untitled Document","version":"0.0.8","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[9872175665704159396],{"selected":false,"expanded":false}]],"layer_range_selection_reference":[],"movement_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":11149268176678832548,"layer_ids":[11149268176678832547],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[885.0,0.0,-0.0,461.0],"translation":[657.0,273.0]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},"font_cache":{"data":{},"default_font":null}},"artboard_ids":[11149268176678832547]},"properties_panel_message_handler":{"active_selection":null}}
|
||||||
@@ -66,5 +66,5 @@ pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
|||||||
pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.);
|
pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.);
|
||||||
|
|
||||||
// Document
|
// Document
|
||||||
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.7";
|
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.8"; // Remember to save a simple document and replace the test file at: editor\src\communication\graphite-test-document.graphite
|
||||||
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ impl PropertyHolder for Export {
|
|||||||
let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())];
|
let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())];
|
||||||
export_area_options.extend(artboards);
|
export_area_options.extend(artboards);
|
||||||
let index = export_area_options.iter().position(|(val, _)| val == &self.bounds).unwrap();
|
let index = export_area_options.iter().position(|(val, _)| val == &self.bounds).unwrap();
|
||||||
let menu_entries = vec![export_area_options
|
let entries = vec![export_area_options
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(val, name)| DropdownEntryData {
|
.map(|(val, name)| DropdownEntryData {
|
||||||
label: name,
|
label: name,
|
||||||
@@ -84,8 +84,8 @@ impl PropertyHolder for Export {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
||||||
selected_index: index as u32,
|
selected_index: Some(index as u32),
|
||||||
menu_entries,
|
entries,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
@@ -101,19 +101,19 @@ impl PropertyHolder for Export {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: self.scale_factor,
|
value: Some(self.scale_factor),
|
||||||
label: "".into(),
|
label: "".into(),
|
||||||
unit: " ".into(),
|
unit: " ".into(),
|
||||||
disabled: self.file_type == FileType::Svg,
|
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| ExportDialogUpdate::ScaleFactor(number_input.value).into()),
|
disabled: self.file_type == FileType::Svg,
|
||||||
|
on_update: WidgetCallback::new(|number_input: &NumberInput| ExportDialogUpdate::ScaleFactor(number_input.value.unwrap()).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
let button_widgets = vec![
|
let button_widgets = vec![
|
||||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
label: "OK".to_string(),
|
label: "Export".to_string(),
|
||||||
min_width: 96,
|
min_width: 96,
|
||||||
emphasized: true,
|
emphasized: true,
|
||||||
on_update: WidgetCallback::new(|_| {
|
on_update: WidgetCallback::new(|_| {
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ impl PropertyHolder for NewDocument {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: self.dimensions.x as f64,
|
value: Some(self.dimensions.x as f64),
|
||||||
label: "W".into(),
|
label: "W".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
disabled: self.infinite,
|
disabled: self.infinite,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsX(number_input.value).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsX(number_input.value.unwrap()).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
@@ -80,13 +80,13 @@ impl PropertyHolder for NewDocument {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: self.dimensions.y as f64,
|
value: Some(self.dimensions.y as f64),
|
||||||
label: "H".into(),
|
label: "H".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
disabled: self.infinite,
|
disabled: self.infinite,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsY(number_input.value).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsY(number_input.value.unwrap()).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use super::clipboards::Clipboard;
|
use super::clipboards::Clipboard;
|
||||||
use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer};
|
use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer};
|
||||||
use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData;
|
use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData;
|
||||||
use super::utility_types::TargetDocument;
|
|
||||||
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
||||||
|
use super::utility_types::{DocumentMode, TargetDocument};
|
||||||
use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler};
|
use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler};
|
||||||
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
|
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
|
||||||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
||||||
use crate::frontend::utility_types::{FileType, FrontendImageData};
|
use crate::frontend::utility_types::{FileType, FrontendImageData};
|
||||||
use crate::input::InputPreprocessorMessageHandler;
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
use crate::layout::widgets::{
|
use crate::layout::widgets::{
|
||||||
IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget,
|
DropdownEntryData, DropdownInput, IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection,
|
||||||
WidgetCallback, WidgetHolder, WidgetLayout,
|
SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
|
||||||
};
|
};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
|
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
|
||||||
@@ -18,6 +19,7 @@ use crate::EditorError;
|
|||||||
|
|
||||||
use graphene::color::Color;
|
use graphene::color::Color;
|
||||||
use graphene::document::Document as GrapheneDocument;
|
use graphene::document::Document as GrapheneDocument;
|
||||||
|
use graphene::layers::blend_mode::BlendMode;
|
||||||
use graphene::layers::folder_layer::FolderLayer;
|
use graphene::layers::folder_layer::FolderLayer;
|
||||||
use graphene::layers::layer_info::LayerDataType;
|
use graphene::layers::layer_info::LayerDataType;
|
||||||
use graphene::layers::style::{Fill, ViewMode};
|
use graphene::layers::style::{Fill, ViewMode};
|
||||||
@@ -32,15 +34,24 @@ use std::collections::VecDeque;
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DocumentMessageHandler {
|
pub struct DocumentMessageHandler {
|
||||||
pub graphene_document: GrapheneDocument,
|
pub graphene_document: GrapheneDocument,
|
||||||
|
pub saved_document_identifier: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
|
||||||
|
pub document_mode: DocumentMode,
|
||||||
|
pub view_mode: ViewMode,
|
||||||
|
pub snapping_enabled: bool,
|
||||||
|
pub overlays_visible: bool,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub document_undo_history: Vec<DocumentSave>,
|
pub document_undo_history: Vec<DocumentSave>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub document_redo_history: Vec<DocumentSave>,
|
pub document_redo_history: Vec<DocumentSave>,
|
||||||
pub saved_document_identifier: u64,
|
|
||||||
pub name: String,
|
|
||||||
#[serde(with = "vectorize_layer_metadata")]
|
#[serde(with = "vectorize_layer_metadata")]
|
||||||
pub layer_metadata: HashMap<Vec<LayerId>, LayerMetadata>,
|
pub layer_metadata: HashMap<Vec<LayerId>, LayerMetadata>,
|
||||||
layer_range_selection_reference: Vec<LayerId>,
|
layer_range_selection_reference: Vec<LayerId>,
|
||||||
|
|
||||||
movement_handler: MovementMessageHandler,
|
movement_handler: MovementMessageHandler,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
overlays_message_handler: OverlaysMessageHandler,
|
overlays_message_handler: OverlaysMessageHandler,
|
||||||
@@ -48,31 +59,32 @@ pub struct DocumentMessageHandler {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
transform_layer_handler: TransformLayerMessageHandler,
|
transform_layer_handler: TransformLayerMessageHandler,
|
||||||
properties_panel_message_handler: PropertiesPanelMessageHandler,
|
properties_panel_message_handler: PropertiesPanelMessageHandler,
|
||||||
pub overlays_visible: bool,
|
|
||||||
pub snapping_enabled: bool,
|
|
||||||
pub view_mode: ViewMode,
|
|
||||||
pub version: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DocumentMessageHandler {
|
impl Default for DocumentMessageHandler {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
graphene_document: GrapheneDocument::default(),
|
graphene_document: GrapheneDocument::default(),
|
||||||
document_undo_history: Vec::new(),
|
|
||||||
document_redo_history: Vec::new(),
|
|
||||||
saved_document_identifier: 0,
|
saved_document_identifier: 0,
|
||||||
name: String::from("Untitled Document"),
|
name: String::from("Untitled Document"),
|
||||||
|
version: GRAPHITE_DOCUMENT_VERSION.to_string(),
|
||||||
|
|
||||||
|
document_mode: DocumentMode::DesignMode,
|
||||||
|
view_mode: ViewMode::default(),
|
||||||
|
snapping_enabled: true,
|
||||||
|
overlays_visible: true,
|
||||||
|
|
||||||
|
document_undo_history: Vec::new(),
|
||||||
|
document_redo_history: Vec::new(),
|
||||||
|
|
||||||
layer_metadata: vec![(vec![], LayerMetadata::new(true))].into_iter().collect(),
|
layer_metadata: vec![(vec![], LayerMetadata::new(true))].into_iter().collect(),
|
||||||
layer_range_selection_reference: Vec::new(),
|
layer_range_selection_reference: Vec::new(),
|
||||||
|
|
||||||
movement_handler: MovementMessageHandler::default(),
|
movement_handler: MovementMessageHandler::default(),
|
||||||
overlays_message_handler: OverlaysMessageHandler::default(),
|
overlays_message_handler: OverlaysMessageHandler::default(),
|
||||||
artboard_message_handler: ArtboardMessageHandler::default(),
|
artboard_message_handler: ArtboardMessageHandler::default(),
|
||||||
transform_layer_handler: TransformLayerMessageHandler::default(),
|
transform_layer_handler: TransformLayerMessageHandler::default(),
|
||||||
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
|
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
|
||||||
snapping_enabled: true,
|
|
||||||
overlays_visible: true,
|
|
||||||
view_mode: ViewMode::default(),
|
|
||||||
version: GRAPHITE_DOCUMENT_VERSION.to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +142,7 @@ impl DocumentMessageHandler {
|
|||||||
if let Some(layer) = self.layer_metadata.get_mut(path) {
|
if let Some(layer) = self.layer_metadata.get_mut(path) {
|
||||||
layer.selected = true;
|
layer.selected = true;
|
||||||
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
||||||
(!path.is_empty()).then(|| FrontendMessage::UpdateDocumentLayer { data }.into())
|
(!path.is_empty()).then(|| FrontendMessage::UpdateDocumentLayerDetails { data }.into())
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Tried to select non existing layer {:?}", path);
|
log::warn!("Tried to select non existing layer {:?}", path);
|
||||||
None
|
None
|
||||||
@@ -516,11 +528,9 @@ impl DocumentMessageHandler {
|
|||||||
responses.push_back(FrontendMessage::TriggerFontLoadDefault.into())
|
responses.push_back(FrontendMessage::TriggerFontLoadDefault.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl PropertyHolder for DocumentMessageHandler {
|
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||||
fn properties(&self) -> WidgetLayout {
|
let document_bar_layout = WidgetLayout::new(vec![LayoutRow::Row {
|
||||||
WidgetLayout::new(vec![LayoutRow::Row {
|
|
||||||
widgets: vec![
|
widgets: vec![
|
||||||
WidgetHolder::new(Widget::OptionalInput(OptionalInput {
|
WidgetHolder::new(Widget::OptionalInput(OptionalInput {
|
||||||
checked: self.snapping_enabled,
|
checked: self.snapping_enabled,
|
||||||
@@ -600,11 +610,11 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: "°".into(),
|
unit: "°".into(),
|
||||||
value: self.movement_handler.tilt / (std::f64::consts::PI / 180.),
|
value: Some(self.movement_handler.tilt / (std::f64::consts::PI / 180.)),
|
||||||
increment_factor: 15.,
|
increment_factor: 15.,
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
MovementMessage::SetCanvasRotation {
|
MovementMessage::SetCanvasRotation {
|
||||||
angle_radians: number_input.value * (std::f64::consts::PI / 180.),
|
angle_radians: number_input.value.unwrap() * (std::f64::consts::PI / 180.),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@@ -641,12 +651,12 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: "%".into(),
|
unit: "%".into(),
|
||||||
value: self.movement_handler.zoom * 100.,
|
value: Some(self.movement_handler.zoom * 100.),
|
||||||
min: Some(0.000001),
|
min: Some(0.000001),
|
||||||
max: Some(1000000.),
|
max: Some(1000000.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
MovementMessage::SetCanvasZoom {
|
MovementMessage::SetCanvasZoom {
|
||||||
zoom_factor: number_input.value / 100.,
|
zoom_factor: number_input.value.unwrap() / 100.,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@@ -656,7 +666,163 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
}])
|
}]);
|
||||||
|
|
||||||
|
let document_mode_layout = WidgetLayout::new(vec![LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
||||||
|
entries: vec![vec![
|
||||||
|
DropdownEntryData {
|
||||||
|
label: DocumentMode::DesignMode.to_string(),
|
||||||
|
icon: DocumentMode::DesignMode.icon_name(),
|
||||||
|
..DropdownEntryData::default()
|
||||||
|
},
|
||||||
|
DropdownEntryData {
|
||||||
|
label: DocumentMode::SelectMode.to_string(),
|
||||||
|
icon: DocumentMode::SelectMode.icon_name(),
|
||||||
|
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(330) }.into()),
|
||||||
|
..DropdownEntryData::default()
|
||||||
|
},
|
||||||
|
DropdownEntryData {
|
||||||
|
label: DocumentMode::GuideMode.to_string(),
|
||||||
|
icon: DocumentMode::GuideMode.icon_name(),
|
||||||
|
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(331) }.into()),
|
||||||
|
..DropdownEntryData::default()
|
||||||
|
},
|
||||||
|
]],
|
||||||
|
selected_index: Some(self.document_mode as u32),
|
||||||
|
draw_icon: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
}]);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
LayoutMessage::SendLayout {
|
||||||
|
layout: document_bar_layout,
|
||||||
|
layout_target: LayoutTarget::DocumentBar,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
LayoutMessage::SendLayout {
|
||||||
|
layout: document_mode_layout,
|
||||||
|
layout_target: LayoutTarget::DocumentMode,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_layer_tree_options_bar_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
let mut opacity = None;
|
||||||
|
let mut opacity_is_mixed = false;
|
||||||
|
|
||||||
|
let mut blend_mode = None;
|
||||||
|
let mut blend_mode_is_mixed = false;
|
||||||
|
|
||||||
|
self.layer_metadata
|
||||||
|
.keys()
|
||||||
|
.filter_map(|path| self.layer_panel_entry_from_path(path))
|
||||||
|
.filter(|layer_panel_entry| layer_panel_entry.layer_metadata.selected)
|
||||||
|
.flat_map(|layer_panel_entry| self.graphene_document.layer(layer_panel_entry.path.as_slice()))
|
||||||
|
.for_each(|layer| {
|
||||||
|
match opacity {
|
||||||
|
None => opacity = Some(layer.opacity),
|
||||||
|
Some(opacity) => {
|
||||||
|
if (opacity - layer.opacity).abs() > (1. / 1_000_000.) {
|
||||||
|
opacity_is_mixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match blend_mode {
|
||||||
|
None => blend_mode = Some(layer.blend_mode),
|
||||||
|
Some(blend_mode) => {
|
||||||
|
if blend_mode != layer.blend_mode {
|
||||||
|
blend_mode_is_mixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if opacity_is_mixed {
|
||||||
|
opacity = None;
|
||||||
|
}
|
||||||
|
if blend_mode_is_mixed {
|
||||||
|
blend_mode = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let blend_mode_menu_entries = BlendMode::list_modes_in_groups()
|
||||||
|
.iter()
|
||||||
|
.map(|modes| {
|
||||||
|
modes
|
||||||
|
.iter()
|
||||||
|
.map(|mode| DropdownEntryData {
|
||||||
|
label: mode.to_string(),
|
||||||
|
value: mode.to_string(),
|
||||||
|
on_update: WidgetCallback::new(|_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode: *mode }.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let layer_tree_options = WidgetLayout::new(vec![LayoutRow::Row {
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
||||||
|
entries: blend_mode_menu_entries,
|
||||||
|
selected_index: blend_mode.map(|blend_mode| blend_mode as u32),
|
||||||
|
disabled: blend_mode.is_none() && !blend_mode_is_mixed,
|
||||||
|
draw_icon: false,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
|
label: "Opacity".into(),
|
||||||
|
unit: "%".into(),
|
||||||
|
display_decimal_places: 2,
|
||||||
|
disabled: opacity.is_none() && !opacity_is_mixed,
|
||||||
|
value: opacity.map(|opacity| opacity * 100.),
|
||||||
|
min: Some(0.),
|
||||||
|
max: Some(100.),
|
||||||
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
|
if let Some(value) = number_input.value {
|
||||||
|
DocumentMessage::SetOpacityForSelectedLayers { opacity: value / 100. }.into()
|
||||||
|
} else {
|
||||||
|
Message::NoOp
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
..NumberInput::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Section,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||||
|
icon: "NodeFolder".into(),
|
||||||
|
tooltip: "New Folder (Ctrl+Shift+N)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
|
||||||
|
size: 24,
|
||||||
|
on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||||
|
icon: "Trash".into(),
|
||||||
|
tooltip: "Delete Selected (Del)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
|
||||||
|
size: 24,
|
||||||
|
on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}]);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
LayoutMessage::SendLayout {
|
||||||
|
layout: layer_tree_options,
|
||||||
|
layout_target: LayoutTarget::LayerTreeOptions,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +923,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
||||||
responses.push_back(DocumentMessage::SelectionChanged.into());
|
responses.push_back(DocumentMessage::SelectionChanged.into());
|
||||||
|
|
||||||
|
self.update_layer_tree_options_bar_widgets(responses);
|
||||||
}
|
}
|
||||||
AlignSelectedLayers { axis, aggregate } => {
|
AlignSelectedLayers { axis, aggregate } => {
|
||||||
self.backup(responses);
|
self.backup(responses);
|
||||||
@@ -796,7 +964,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BooleanOperation(op) => {
|
BooleanOperation(op) => {
|
||||||
// convert Vec<&[LayerId]> to Vec<Vec<&LayerId>> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum
|
// Convert Vec<&[LayerId]> to Vec<Vec<&LayerId>> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum
|
||||||
responses.push_back(StartTransaction.into());
|
responses.push_back(StartTransaction.into());
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
DocumentOperation::BooleanOperation {
|
DocumentOperation::BooleanOperation {
|
||||||
@@ -974,9 +1142,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||||||
}
|
}
|
||||||
LayerChanged { affected_layer_path } => {
|
LayerChanged { affected_layer_path } => {
|
||||||
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone()) {
|
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone()) {
|
||||||
responses.push_back(FrontendMessage::UpdateDocumentLayer { data: layer_entry }.into());
|
responses.push_back(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }.into());
|
||||||
}
|
}
|
||||||
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
|
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
|
||||||
|
self.update_layer_tree_options_bar_widgets(responses);
|
||||||
}
|
}
|
||||||
LoadFont { font } => {
|
LoadFont { font } => {
|
||||||
if !self.graphene_document.font_cache.loaded_font(&font) {
|
if !self.graphene_document.font_cache.loaded_font(&font) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use graphene::document::FontCache;
|
use graphene::document::FontCache;
|
||||||
use graphene::layers::blend_mode::BlendMode;
|
|
||||||
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType};
|
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType};
|
||||||
use graphene::layers::style::ViewMode;
|
use graphene::layers::style::ViewMode;
|
||||||
use graphene::LayerId;
|
use graphene::LayerId;
|
||||||
@@ -48,8 +47,6 @@ pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, la
|
|||||||
LayerPanelEntry {
|
LayerPanelEntry {
|
||||||
name,
|
name,
|
||||||
visible: layer.visible,
|
visible: layer.visible,
|
||||||
blend_mode: layer.blend_mode,
|
|
||||||
opacity: layer.opacity,
|
|
||||||
layer_type: (&layer.data).into(),
|
layer_type: (&layer.data).into(),
|
||||||
layer_metadata: *layer_metadata,
|
layer_metadata: *layer_metadata,
|
||||||
path,
|
path,
|
||||||
@@ -88,8 +85,6 @@ impl Serialize for RawBuffer {
|
|||||||
pub struct LayerPanelEntry {
|
pub struct LayerPanelEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
pub blend_mode: BlendMode,
|
|
||||||
pub opacity: f64,
|
|
||||||
pub layer_type: LayerDataTypeDiscriminant,
|
pub layer_type: LayerDataTypeDiscriminant,
|
||||||
pub layer_metadata: LayerMetadata,
|
pub layer_metadata: LayerMetadata,
|
||||||
pub path: Vec<LayerId>,
|
pub path: Vec<LayerId>,
|
||||||
|
|||||||
@@ -153,10 +153,9 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
|||||||
self.zoom = 1.
|
self.zoom = 1.
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into());
|
|
||||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
||||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||||
}
|
}
|
||||||
IncreaseCanvasZoom { center_on_mouse } => {
|
IncreaseCanvasZoom { center_on_mouse } => {
|
||||||
@@ -245,15 +244,13 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
|||||||
self.tilt = angle_radians;
|
self.tilt = angle_radians;
|
||||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into());
|
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
|
||||||
}
|
}
|
||||||
SetCanvasZoom { zoom_factor } => {
|
SetCanvasZoom { zoom_factor } => {
|
||||||
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
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(ToolMessage::DocumentIsDirty.into());
|
||||||
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
||||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||||
}
|
}
|
||||||
TransformCanvasEnd => {
|
TransformCanvasEnd => {
|
||||||
|
|||||||
@@ -67,6 +67,6 @@ pub enum PortfolioMessage {
|
|||||||
SetActiveDocument {
|
SetActiveDocument {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
},
|
},
|
||||||
UpdateDocumentBar,
|
UpdateDocumentWidgets,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ impl PortfolioMessageHandler {
|
|||||||
.layer_metadata
|
.layer_metadata
|
||||||
.keys()
|
.keys()
|
||||||
.filter_map(|path| new_document.layer_panel_entry_from_path(path))
|
.filter_map(|path| new_document.layer_panel_entry_from_path(path))
|
||||||
.map(|entry| FrontendMessage::UpdateDocumentLayer { data: entry }.into())
|
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
new_document.update_layer_tree_options_bar_widgets(responses);
|
||||||
|
|
||||||
new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new());
|
new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new());
|
||||||
new_document.load_default_font(responses);
|
new_document.load_default_font(responses);
|
||||||
@@ -432,14 +433,14 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
||||||
}
|
}
|
||||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||||
}
|
}
|
||||||
SetActiveDocument { document_id } => {
|
SetActiveDocument { document_id } => {
|
||||||
self.active_document_id = document_id;
|
self.active_document_id = document_id;
|
||||||
}
|
}
|
||||||
UpdateDocumentBar => {
|
UpdateDocumentWidgets => {
|
||||||
let active_document = self.active_document();
|
let active_document = self.active_document();
|
||||||
active_document.register_properties(responses, LayoutTarget::DocumentBar)
|
active_document.update_document_widgets(responses);
|
||||||
}
|
}
|
||||||
UpdateOpenDocumentsList => {
|
UpdateOpenDocumentsList => {
|
||||||
// Send the list of document tab names
|
// Send the list of document tab names
|
||||||
|
|||||||
@@ -137,14 +137,14 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(vec![]),
|
layout: WidgetLayout::new(vec![]),
|
||||||
layout_target: LayoutTarget::PropertiesOptionsPanel,
|
layout_target: LayoutTarget::PropertiesOptions,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(vec![]),
|
layout: WidgetLayout::new(vec![]),
|
||||||
layout_target: LayoutTarget::PropertiesSectionsPanel,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -217,14 +217,14 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||||||
self.active_selection = None;
|
self.active_selection = None;
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout_target: LayoutTarget::PropertiesOptionsPanel,
|
layout_target: LayoutTarget::PropertiesOptions,
|
||||||
layout: WidgetLayout::default(),
|
layout: WidgetLayout::default(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout_target: LayoutTarget::PropertiesSectionsPanel,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
layout: WidgetLayout::default(),
|
layout: WidgetLayout::default(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
@@ -308,12 +308,12 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.x(),
|
value: Some(layer.transform.x()),
|
||||||
label: "X".into(),
|
label: "X".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::X,
|
transform_op: TransformOp::X,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -325,12 +325,12 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.y(),
|
value: Some(layer.transform.y()),
|
||||||
label: "Y".into(),
|
label: "Y".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Y,
|
transform_op: TransformOp::Y,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -350,12 +350,12 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.bounding_transform(font_cache).scale_x(),
|
value: Some(layer.bounding_transform(font_cache).scale_x()),
|
||||||
label: "W".into(),
|
label: "W".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Width,
|
transform_op: TransformOp::Width,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -367,12 +367,12 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.bounding_transform(font_cache).scale_y(),
|
value: Some(layer.bounding_transform(font_cache).scale_y()),
|
||||||
label: "H".into(),
|
label: "H".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Height,
|
transform_op: TransformOp::Height,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -416,14 +416,14 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(options_bar),
|
layout: WidgetLayout::new(options_bar),
|
||||||
layout_target: LayoutTarget::PropertiesOptionsPanel,
|
layout_target: LayoutTarget::PropertiesOptions,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(properties_body),
|
layout: WidgetLayout::new(properties_body),
|
||||||
layout_target: LayoutTarget::PropertiesSectionsPanel,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -504,14 +504,14 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Mes
|
|||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(options_bar),
|
layout: WidgetLayout::new(options_bar),
|
||||||
layout_target: LayoutTarget::PropertiesOptionsPanel,
|
layout_target: LayoutTarget::PropertiesOptions,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
LayoutMessage::SendLayout {
|
LayoutMessage::SendLayout {
|
||||||
layout: WidgetLayout::new(properties_body),
|
layout: WidgetLayout::new(properties_body),
|
||||||
layout_target: LayoutTarget::PropertiesSectionsPanel,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
@@ -532,12 +532,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.x(),
|
value: Some(layer.transform.x()),
|
||||||
label: "X".into(),
|
label: "X".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::X,
|
transform_op: TransformOp::X,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -549,12 +549,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.y(),
|
value: Some(layer.transform.y()),
|
||||||
label: "Y".into(),
|
label: "Y".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Y,
|
transform_op: TransformOp::Y,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -574,12 +574,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.rotation() * 180. / PI,
|
value: Some(layer.transform.rotation() * 180. / PI),
|
||||||
label: "".into(),
|
label: "".into(),
|
||||||
unit: "°".into(),
|
unit: "°".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value / 180. * PI,
|
value: number_input.value.unwrap() / 180. * PI,
|
||||||
transform_op: TransformOp::Rotation,
|
transform_op: TransformOp::Rotation,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -599,12 +599,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.scale_x(),
|
value: Some(layer.transform.scale_x()),
|
||||||
label: "X".into(),
|
label: "X".into(),
|
||||||
unit: "".into(),
|
unit: "".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::ScaleX,
|
transform_op: TransformOp::ScaleX,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -616,12 +616,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.transform.scale_y(),
|
value: Some(layer.transform.scale_y()),
|
||||||
label: "Y".into(),
|
label: "Y".into(),
|
||||||
unit: "".into(),
|
unit: "".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::ScaleY,
|
transform_op: TransformOp::ScaleY,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -641,12 +641,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.bounding_transform(font_cache).scale_x(),
|
value: Some(layer.bounding_transform(font_cache).scale_x()),
|
||||||
label: "W".into(),
|
label: "W".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Width,
|
transform_op: TransformOp::Width,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -658,12 +658,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.bounding_transform(font_cache).scale_y(),
|
value: Some(layer.bounding_transform(font_cache).scale_y()),
|
||||||
label: "H".into(),
|
label: "H".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value.unwrap(),
|
||||||
transform_op: TransformOp::Height,
|
transform_op: TransformOp::Height,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -765,7 +765,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: layer.size,
|
value: Some(layer.size),
|
||||||
min: Some(1.),
|
min: Some(1.),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||||
@@ -773,7 +773,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow {
|
|||||||
font_family: font_family.clone(),
|
font_family: font_family.clone(),
|
||||||
font_style: font_style.clone(),
|
font_style: font_style.clone(),
|
||||||
font_file: font_file.clone(),
|
font_file: font_file.clone(),
|
||||||
size: number_input.value,
|
size: number_input.value.unwrap(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@@ -954,13 +954,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: stroke.weight() as f64,
|
value: Some(stroke.weight() as f64),
|
||||||
is_integer: false,
|
is_integer: false,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyStroke {
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
stroke: internal_stroke2.clone().with_weight(number_input.value),
|
stroke: internal_stroke2.clone().with_weight(number_input.value.unwrap()),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@@ -1000,13 +1000,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: stroke.dash_offset() as f64,
|
value: Some(stroke.dash_offset() as f64),
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyStroke {
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
stroke: internal_stroke4.clone().with_dash_offset(number_input.value),
|
stroke: internal_stroke4.clone().with_dash_offset(number_input.value.unwrap()),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@@ -1120,13 +1120,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: stroke.line_join_miter_limit() as f64,
|
value: Some(stroke.line_join_miter_limit() as f64),
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
unit: "".into(),
|
unit: "".into(),
|
||||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyStroke {
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
stroke: internal_stroke5.clone().with_line_join_miter_limit(number_input.value),
|
stroke: internal_stroke5.clone().with_line_join_miter_limit(number_input.value.unwrap()),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use graphene::LayerId;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
pub type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerMetadata>);
|
pub type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerMetadata>);
|
||||||
|
|
||||||
@@ -32,3 +33,31 @@ pub enum TargetDocument {
|
|||||||
Artboard,
|
Artboard,
|
||||||
Artwork,
|
Artwork,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum DocumentMode {
|
||||||
|
DesignMode,
|
||||||
|
SelectMode,
|
||||||
|
GuideMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DocumentMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
DocumentMode::DesignMode => "Design Mode".to_string(),
|
||||||
|
DocumentMode::SelectMode => "Select Mode".to_string(),
|
||||||
|
DocumentMode::GuideMode => "Guide Mode".to_string(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentMode {
|
||||||
|
pub fn icon_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
DocumentMode::DesignMode => "ViewportDesignMode".to_string(),
|
||||||
|
DocumentMode::SelectMode => "ViewportSelectMode".to_string(),
|
||||||
|
DocumentMode::GuideMode => "ViewportGuideMode".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,25 +34,25 @@ pub enum FrontendMessage {
|
|||||||
|
|
||||||
// Update prefix: give the frontend a new value or state for it to use
|
// Update prefix: give the frontend a new value or state for it to use
|
||||||
UpdateActiveDocument { document_id: u64 },
|
UpdateActiveDocument { document_id: u64 },
|
||||||
UpdateActiveTool { tool_name: String },
|
|
||||||
UpdateCanvasRotation { angle_radians: f64 },
|
|
||||||
UpdateCanvasZoom { factor: f64 },
|
|
||||||
UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout },
|
UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateDocumentArtboards { svg: String },
|
UpdateDocumentArtboards { svg: String },
|
||||||
UpdateDocumentArtwork { svg: String },
|
UpdateDocumentArtwork { svg: String },
|
||||||
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateDocumentLayer { data: LayerPanelEntry },
|
UpdateDocumentLayerDetails { data: LayerPanelEntry },
|
||||||
UpdateDocumentLayerTreeStructure { data_buffer: RawBuffer },
|
UpdateDocumentLayerTreeStructure { data_buffer: RawBuffer },
|
||||||
|
UpdateDocumentModeLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateDocumentOverlays { svg: String },
|
UpdateDocumentOverlays { svg: String },
|
||||||
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
||||||
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
||||||
UpdateImageData { image_data: Vec<FrontendImageData> },
|
UpdateImageData { image_data: Vec<FrontendImageData> },
|
||||||
UpdateInputHints { hint_data: HintData },
|
UpdateInputHints { hint_data: HintData },
|
||||||
|
UpdateLayerTreeOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateMouseCursor { cursor: MouseCursorIcon },
|
UpdateMouseCursor { cursor: MouseCursorIcon },
|
||||||
UpdateNodeGraphVisibility { visible: bool },
|
UpdateNodeGraphVisibility { visible: bool },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
||||||
UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
|
UpdateToolShelfLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ pub enum LayoutMessage {
|
|||||||
pub enum LayoutTarget {
|
pub enum LayoutTarget {
|
||||||
DialogDetails,
|
DialogDetails,
|
||||||
DocumentBar,
|
DocumentBar,
|
||||||
PropertiesOptionsPanel,
|
DocumentMode,
|
||||||
PropertiesSectionsPanel,
|
LayerTreeOptions,
|
||||||
|
PropertiesOptions,
|
||||||
|
PropertiesSections,
|
||||||
ToolOptions,
|
ToolOptions,
|
||||||
|
ToolShelf,
|
||||||
|
|
||||||
// KEEP THIS ENUM LAST
|
// KEEP THIS ENUM LAST
|
||||||
// This is a marker that is used to define an array that is used to hold widgets
|
// This is a marker that is used to define an array that is used to hold widgets
|
||||||
|
|||||||
@@ -12,30 +12,45 @@ pub struct LayoutMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutMessageHandler {
|
impl LayoutMessageHandler {
|
||||||
|
#[remain::check]
|
||||||
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
|
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
|
||||||
let widget_layout = &self.layouts[layout_target as usize];
|
let widget_layout = &self.layouts[layout_target as usize];
|
||||||
|
#[remain::sorted]
|
||||||
let message = match layout_target {
|
let message = match layout_target {
|
||||||
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
|
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
},
|
},
|
||||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
|
||||||
layout_target,
|
|
||||||
layout: widget_layout.layout.clone(),
|
|
||||||
},
|
|
||||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
|
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
},
|
},
|
||||||
LayoutTarget::PropertiesOptionsPanel => FrontendMessage::UpdatePropertyPanelOptionsLayout {
|
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
},
|
},
|
||||||
LayoutTarget::PropertiesSectionsPanel => FrontendMessage::UpdatePropertyPanelSectionsLayout {
|
LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout {
|
||||||
|
layout_target,
|
||||||
|
layout: widget_layout.layout.clone(),
|
||||||
|
},
|
||||||
|
LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout {
|
||||||
|
layout_target,
|
||||||
|
layout: widget_layout.layout.clone(),
|
||||||
|
},
|
||||||
|
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout {
|
||||||
|
layout_target,
|
||||||
|
layout: widget_layout.layout.clone(),
|
||||||
|
},
|
||||||
|
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
||||||
|
layout_target,
|
||||||
|
layout: widget_layout.layout.clone(),
|
||||||
|
},
|
||||||
|
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[remain::unsorted]
|
||||||
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
||||||
};
|
};
|
||||||
responses.push_back(message.into());
|
responses.push_back(message.into());
|
||||||
@@ -72,8 +87,8 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
}
|
}
|
||||||
Widget::DropdownInput(dropdown_input) => {
|
Widget::DropdownInput(dropdown_input) => {
|
||||||
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
||||||
dropdown_input.selected_index = update_value as u32;
|
dropdown_input.selected_index = Some(update_value as u32);
|
||||||
let callback_message = (dropdown_input.menu_entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&());
|
let callback_message = (dropdown_input.entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&());
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
Widget::FontInput(font_input) => {
|
Widget::FontInput(font_input) => {
|
||||||
@@ -102,7 +117,7 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
Widget::NumberInput(number_input) => match value {
|
Widget::NumberInput(number_input) => match value {
|
||||||
Value::Number(num) => {
|
Value::Number(num) => {
|
||||||
let update_value = num.as_f64().unwrap();
|
let update_value = num.as_f64().unwrap();
|
||||||
number_input.value = update_value;
|
number_input.value = Some(update_value);
|
||||||
let callback_message = (number_input.on_update.callback)(number_input);
|
let callback_message = (number_input.on_update.callback)(number_input);
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,18 @@ pub type SubLayout = Vec<LayoutRow>;
|
|||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum LayoutRow {
|
pub enum LayoutRow {
|
||||||
Row { widgets: Vec<WidgetHolder> },
|
Column {
|
||||||
Section { name: String, layout: SubLayout },
|
#[serde(rename = "columnWidgets")]
|
||||||
|
widgets: Vec<WidgetHolder>,
|
||||||
|
},
|
||||||
|
Row {
|
||||||
|
#[serde(rename = "rowWidgets")]
|
||||||
|
widgets: Vec<WidgetHolder>,
|
||||||
|
},
|
||||||
|
Section {
|
||||||
|
name: String,
|
||||||
|
layout: SubLayout,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -72,6 +82,10 @@ impl<'a> Iterator for WidgetIter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.stack.pop() {
|
match self.stack.pop() {
|
||||||
|
Some(LayoutRow::Column { widgets }) => {
|
||||||
|
self.current_slice = Some(widgets);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
Some(LayoutRow::Row { widgets }) => {
|
Some(LayoutRow::Row { widgets }) => {
|
||||||
self.current_slice = Some(widgets);
|
self.current_slice = Some(widgets);
|
||||||
self.next()
|
self.next()
|
||||||
@@ -103,6 +117,10 @@ impl<'a> Iterator for WidgetIterMut<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match self.stack.pop() {
|
match self.stack.pop() {
|
||||||
|
Some(LayoutRow::Column { widgets }) => {
|
||||||
|
self.current_slice = Some(widgets);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
Some(LayoutRow::Row { widgets }) => {
|
Some(LayoutRow::Row { widgets }) => {
|
||||||
self.current_slice = Some(widgets);
|
self.current_slice = Some(widgets);
|
||||||
self.next()
|
self.next()
|
||||||
@@ -170,7 +188,7 @@ pub enum Widget {
|
|||||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
pub struct NumberInput {
|
pub struct NumberInput {
|
||||||
pub value: f64,
|
pub value: Option<f64>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_update: WidgetCallback<NumberInput>,
|
pub on_update: WidgetCallback<NumberInput>,
|
||||||
@@ -285,6 +303,7 @@ pub struct IconButton {
|
|||||||
#[serde(rename = "title")]
|
#[serde(rename = "title")]
|
||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
pub size: u32,
|
pub size: u32,
|
||||||
|
pub active: bool,
|
||||||
#[serde(rename = "gapAfter")]
|
#[serde(rename = "gapAfter")]
|
||||||
pub gap_after: bool,
|
pub gap_after: bool,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -298,12 +317,12 @@ pub struct IconButton {
|
|||||||
pub struct TextButton {
|
pub struct TextButton {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub emphasized: bool,
|
pub emphasized: bool,
|
||||||
pub disabled: bool,
|
|
||||||
pub min_width: u32,
|
pub min_width: u32,
|
||||||
pub gap_after: bool,
|
pub gap_after: bool,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_update: WidgetCallback<TextButton>,
|
pub on_update: WidgetCallback<TextButton>,
|
||||||
|
pub disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
@@ -342,16 +361,14 @@ pub struct PopoverButton {
|
|||||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
#[derivative(Debug, PartialEq)]
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct DropdownInput {
|
pub struct DropdownInput {
|
||||||
#[serde(rename = "menuEntries")]
|
pub entries: Vec<Vec<DropdownEntryData>>,
|
||||||
pub menu_entries: Vec<Vec<DropdownEntryData>>,
|
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace with usize when we switch to a native UI)
|
||||||
|
|
||||||
// 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")]
|
#[serde(rename = "selectedIndex")]
|
||||||
pub selected_index: u32,
|
pub selected_index: Option<u32>,
|
||||||
|
|
||||||
#[serde(rename = "drawIcon")]
|
#[serde(rename = "drawIcon")]
|
||||||
pub draw_icon: bool,
|
pub draw_icon: bool,
|
||||||
|
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
|
||||||
|
pub disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::tools::*;
|
|||||||
use crate::communication::message_handler::MessageHandler;
|
use crate::communication::message_handler::MessageHandler;
|
||||||
use crate::document::DocumentMessageHandler;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::input::InputPreprocessorMessageHandler;
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
use crate::layout::widgets::PropertyHolder;
|
use crate::layout::widgets::{IconButton, LayoutRow, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
use graphene::color::Color;
|
use graphene::color::Color;
|
||||||
@@ -65,6 +65,43 @@ impl ToolData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for ToolData {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let tool_groups_layout = ToolType::list_tools_in_groups()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|group| {
|
||||||
|
let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
direction: SeparatorDirection::Vertical,
|
||||||
|
separator_type: SeparatorType::Section,
|
||||||
|
})));
|
||||||
|
let buttons = group.iter().map(|tool_type| {
|
||||||
|
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||||
|
icon: tool_type.icon_name(),
|
||||||
|
size: 32,
|
||||||
|
tooltip: tool_type.tooltip(),
|
||||||
|
active: self.active_tool_type == *tool_type,
|
||||||
|
on_update: WidgetCallback::new(|_| {
|
||||||
|
if !tool_type.tooltip().contains("Coming Soon") {
|
||||||
|
ToolMessage::ActivateTool { tool_type: *tool_type }.into()
|
||||||
|
} else {
|
||||||
|
DialogMessage::RequestComingSoonDialog { issue: None }.into()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
separator.chain(buttons)
|
||||||
|
})
|
||||||
|
// Skip the initial separator
|
||||||
|
.skip(1)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
WidgetLayout {
|
||||||
|
layout: vec![LayoutRow::Column { widgets: tool_groups_layout }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ToolFsmState {
|
pub struct ToolFsmState {
|
||||||
pub document_tool_data: DocumentToolData,
|
pub document_tool_data: DocumentToolData,
|
||||||
@@ -126,19 +163,15 @@ impl ToolFsmState {
|
|||||||
#[repr(usize)]
|
#[repr(usize)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub enum ToolType {
|
pub enum ToolType {
|
||||||
|
// General tool group
|
||||||
Select,
|
Select,
|
||||||
Artboard,
|
Artboard,
|
||||||
Navigate,
|
Navigate,
|
||||||
Eyedropper,
|
Eyedropper,
|
||||||
Text,
|
|
||||||
Fill,
|
Fill,
|
||||||
Gradient,
|
Gradient,
|
||||||
Brush,
|
|
||||||
Heal,
|
// Vector tool group
|
||||||
Clone,
|
|
||||||
Patch,
|
|
||||||
Detail,
|
|
||||||
Relight,
|
|
||||||
Path,
|
Path,
|
||||||
Pen,
|
Pen,
|
||||||
Freehand,
|
Freehand,
|
||||||
@@ -147,6 +180,115 @@ pub enum ToolType {
|
|||||||
Rectangle,
|
Rectangle,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
Shape,
|
Shape,
|
||||||
|
Text,
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
Brush,
|
||||||
|
Heal,
|
||||||
|
Clone,
|
||||||
|
Patch,
|
||||||
|
Detail,
|
||||||
|
Relight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolType {
|
||||||
|
/// List of all the tools in their conventional ordering and grouping.
|
||||||
|
pub fn list_tools_in_groups() -> [&'static [ToolType]; 3] {
|
||||||
|
[
|
||||||
|
&[
|
||||||
|
// General tool group
|
||||||
|
ToolType::Select,
|
||||||
|
ToolType::Artboard,
|
||||||
|
ToolType::Navigate,
|
||||||
|
ToolType::Eyedropper,
|
||||||
|
ToolType::Fill,
|
||||||
|
ToolType::Gradient,
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// Vector tool group
|
||||||
|
ToolType::Path,
|
||||||
|
ToolType::Pen,
|
||||||
|
ToolType::Freehand,
|
||||||
|
ToolType::Spline,
|
||||||
|
ToolType::Line,
|
||||||
|
ToolType::Rectangle,
|
||||||
|
ToolType::Ellipse,
|
||||||
|
ToolType::Shape,
|
||||||
|
ToolType::Text,
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
// Raster tool group
|
||||||
|
ToolType::Brush,
|
||||||
|
ToolType::Heal,
|
||||||
|
ToolType::Clone,
|
||||||
|
ToolType::Patch,
|
||||||
|
ToolType::Detail,
|
||||||
|
ToolType::Relight,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
// General tool group
|
||||||
|
ToolType::Select => "GeneralSelectTool".into(),
|
||||||
|
ToolType::Artboard => "GeneralArtboardTool".into(),
|
||||||
|
ToolType::Navigate => "GeneralNavigateTool".into(),
|
||||||
|
ToolType::Eyedropper => "GeneralEyedropperTool".into(),
|
||||||
|
ToolType::Fill => "GeneralFillTool".into(),
|
||||||
|
ToolType::Gradient => "GeneralGradientTool".into(),
|
||||||
|
|
||||||
|
// Vector tool group
|
||||||
|
ToolType::Path => "VectorPathTool".into(),
|
||||||
|
ToolType::Pen => "VectorPenTool".into(),
|
||||||
|
ToolType::Freehand => "VectorFreehandTool".into(),
|
||||||
|
ToolType::Spline => "VectorSplineTool".into(),
|
||||||
|
ToolType::Line => "VectorLineTool".into(),
|
||||||
|
ToolType::Rectangle => "VectorRectangleTool".into(),
|
||||||
|
ToolType::Ellipse => "VectorEllipseTool".into(),
|
||||||
|
ToolType::Shape => "VectorShapeTool".into(),
|
||||||
|
ToolType::Text => "VectorTextTool".into(),
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
ToolType::Brush => "RasterBrushTool".into(),
|
||||||
|
ToolType::Heal => "RasterHealTool".into(),
|
||||||
|
ToolType::Clone => "RasterCloneTool".into(),
|
||||||
|
ToolType::Patch => "RasterPatchTool".into(),
|
||||||
|
ToolType::Detail => "RasterDetailTool".into(),
|
||||||
|
ToolType::Relight => "RasterRelightTool".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tooltip(&self) -> String {
|
||||||
|
match self {
|
||||||
|
// General tool group
|
||||||
|
ToolType::Select => "Select Tool (V)".into(),
|
||||||
|
ToolType::Artboard => "Artboard Tool".into(),
|
||||||
|
ToolType::Navigate => "Navigate Tool (Z)".into(),
|
||||||
|
ToolType::Eyedropper => "Eyedropper Tool (I)".into(),
|
||||||
|
ToolType::Fill => "Fill Tool (F)".into(),
|
||||||
|
ToolType::Gradient => "Gradient Tool (H)".into(),
|
||||||
|
|
||||||
|
// Vector tool group
|
||||||
|
ToolType::Path => "Path Tool (A)".into(),
|
||||||
|
ToolType::Pen => "Pen Tool (P)".into(),
|
||||||
|
ToolType::Freehand => "Freehand Tool (N)".into(),
|
||||||
|
ToolType::Spline => "Spline Tool".into(),
|
||||||
|
ToolType::Line => "Line Tool (L)".into(),
|
||||||
|
ToolType::Rectangle => "Rectangle Tool (M)".into(),
|
||||||
|
ToolType::Ellipse => "Ellipse Tool (E)".into(),
|
||||||
|
ToolType::Shape => "Shape Tool (Y)".into(),
|
||||||
|
ToolType::Text => "Text Tool (T)".into(),
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
ToolType::Brush => "Coming Soon: Brush Tool (B)".into(),
|
||||||
|
ToolType::Heal => "Coming Soon: Heal Tool (J)".into(),
|
||||||
|
ToolType::Clone => "Coming Soon: Clone Tool (C)".into(),
|
||||||
|
ToolType::Patch => "Coming Soon: Patch Tool".into(),
|
||||||
|
ToolType::Detail => "Coming Soon: Detail Tool (D)".into(),
|
||||||
|
ToolType::Relight => "Coming Soon: Relight Tool (O)".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ToolType {
|
impl fmt::Display for ToolType {
|
||||||
@@ -154,19 +296,15 @@ impl fmt::Display for ToolType {
|
|||||||
use ToolType::*;
|
use ToolType::*;
|
||||||
|
|
||||||
let name = match_variant_name!(match (self) {
|
let name = match_variant_name!(match (self) {
|
||||||
|
// General tool group
|
||||||
Select,
|
Select,
|
||||||
Artboard,
|
Artboard,
|
||||||
Navigate,
|
Navigate,
|
||||||
Eyedropper,
|
Eyedropper,
|
||||||
Text,
|
|
||||||
Fill,
|
Fill,
|
||||||
Gradient,
|
Gradient,
|
||||||
Brush,
|
|
||||||
Heal,
|
// Vector tool group
|
||||||
Clone,
|
|
||||||
Patch,
|
|
||||||
Detail,
|
|
||||||
Relight,
|
|
||||||
Path,
|
Path,
|
||||||
Pen,
|
Pen,
|
||||||
Freehand,
|
Freehand,
|
||||||
@@ -174,7 +312,16 @@ impl fmt::Display for ToolType {
|
|||||||
Line,
|
Line,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
Shape
|
Shape,
|
||||||
|
Text,
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
Brush,
|
||||||
|
Heal,
|
||||||
|
Clone,
|
||||||
|
Patch,
|
||||||
|
Detail,
|
||||||
|
Relight,
|
||||||
});
|
});
|
||||||
|
|
||||||
formatter.write_str(name)
|
formatter.write_str(name)
|
||||||
@@ -191,19 +338,15 @@ pub enum StandardToolMessageType {
|
|||||||
pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageType) -> Option<ToolMessage> {
|
pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageType) -> Option<ToolMessage> {
|
||||||
match message_type {
|
match message_type {
|
||||||
StandardToolMessageType::DocumentIsDirty => match tool {
|
StandardToolMessageType::DocumentIsDirty => match tool {
|
||||||
|
// General tool group
|
||||||
ToolType::Select => Some(SelectToolMessage::DocumentIsDirty.into()),
|
ToolType::Select => Some(SelectToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Artboard => Some(ArtboardToolMessage::DocumentIsDirty.into()),
|
ToolType::Artboard => Some(ArtboardToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Navigate => None, // Some(NavigateToolMessage::DocumentIsDirty.into()),
|
ToolType::Navigate => None, // Some(NavigateToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Eyedropper => None, // Some(EyedropperToolMessage::DocumentIsDirty.into()),
|
ToolType::Eyedropper => None, // Some(EyedropperToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Text => Some(TextMessage::DocumentIsDirty.into()),
|
ToolType::Fill => None, // Some(FillToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Fill => None, // Some(FillToolMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Gradient => Some(GradientToolMessage::DocumentIsDirty.into()),
|
ToolType::Gradient => Some(GradientToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Brush => None, // Some(BrushMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Heal => None, // Some(HealMessage::DocumentIsDirty.into()),
|
// Vector tool group
|
||||||
ToolType::Clone => None, // Some(CloneMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Patch => None, // Some(PatchMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Detail => None, // Some(DetailToolMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Relight => None, // Some(RelightMessage::DocumentIsDirty.into()),
|
|
||||||
ToolType::Path => Some(PathToolMessage::DocumentIsDirty.into()),
|
ToolType::Path => Some(PathToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Pen => Some(PenToolMessage::DocumentIsDirty.into()),
|
ToolType::Pen => Some(PenToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Freehand => None, // Some(FreehandToolMessage::DocumentIsDirty.into()),
|
ToolType::Freehand => None, // Some(FreehandToolMessage::DocumentIsDirty.into()),
|
||||||
@@ -212,21 +355,26 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
|
|||||||
ToolType::Rectangle => None, // Some(RectangleToolMessage::DocumentIsDirty.into()),
|
ToolType::Rectangle => None, // Some(RectangleToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Ellipse => None, // Some(EllipseToolMessage::DocumentIsDirty.into()),
|
ToolType::Ellipse => None, // Some(EllipseToolMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Shape => None, // Some(ShapeToolMessage::DocumentIsDirty.into()),
|
ToolType::Shape => None, // Some(ShapeToolMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Text => Some(TextMessage::DocumentIsDirty.into()),
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
ToolType::Brush => None, // Some(BrushMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Heal => None, // Some(HealMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Clone => None, // Some(CloneMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Patch => None, // Some(PatchMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Detail => None, // Some(DetailToolMessage::DocumentIsDirty.into()),
|
||||||
|
ToolType::Relight => None, // Some(RelightMessage::DocumentIsDirty.into()),
|
||||||
},
|
},
|
||||||
StandardToolMessageType::Abort => match tool {
|
StandardToolMessageType::Abort => match tool {
|
||||||
|
// General tool group
|
||||||
ToolType::Select => Some(SelectToolMessage::Abort.into()),
|
ToolType::Select => Some(SelectToolMessage::Abort.into()),
|
||||||
ToolType::Artboard => Some(ArtboardToolMessage::Abort.into()),
|
ToolType::Artboard => Some(ArtboardToolMessage::Abort.into()),
|
||||||
ToolType::Navigate => Some(NavigateToolMessage::Abort.into()),
|
ToolType::Navigate => Some(NavigateToolMessage::Abort.into()),
|
||||||
ToolType::Eyedropper => Some(EyedropperToolMessage::Abort.into()),
|
ToolType::Eyedropper => Some(EyedropperToolMessage::Abort.into()),
|
||||||
ToolType::Text => Some(TextMessage::Abort.into()),
|
|
||||||
ToolType::Fill => Some(FillToolMessage::Abort.into()),
|
ToolType::Fill => Some(FillToolMessage::Abort.into()),
|
||||||
ToolType::Gradient => Some(GradientToolMessage::Abort.into()),
|
ToolType::Gradient => Some(GradientToolMessage::Abort.into()),
|
||||||
// ToolType::Brush => Some(BrushMessage::Abort.into()),
|
|
||||||
// ToolType::Heal => Some(HealMessage::Abort.into()),
|
// Vector tool group
|
||||||
// ToolType::Clone => Some(CloneMessage::Abort.into()),
|
|
||||||
// ToolType::Patch => Some(PatchMessage::Abort.into()),
|
|
||||||
// ToolType::Detail => Some(DetailToolMessage::Abort.into()),
|
|
||||||
// ToolType::Relight => Some(RelightMessage::Abort.into()),
|
|
||||||
ToolType::Path => Some(PathToolMessage::Abort.into()),
|
ToolType::Path => Some(PathToolMessage::Abort.into()),
|
||||||
ToolType::Pen => Some(PenToolMessage::Abort.into()),
|
ToolType::Pen => Some(PenToolMessage::Abort.into()),
|
||||||
ToolType::Freehand => Some(FreehandToolMessage::Abort.into()),
|
ToolType::Freehand => Some(FreehandToolMessage::Abort.into()),
|
||||||
@@ -235,7 +383,15 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
|
|||||||
ToolType::Rectangle => Some(RectangleToolMessage::Abort.into()),
|
ToolType::Rectangle => Some(RectangleToolMessage::Abort.into()),
|
||||||
ToolType::Ellipse => Some(EllipseToolMessage::Abort.into()),
|
ToolType::Ellipse => Some(EllipseToolMessage::Abort.into()),
|
||||||
ToolType::Shape => Some(ShapeToolMessage::Abort.into()),
|
ToolType::Shape => Some(ShapeToolMessage::Abort.into()),
|
||||||
_ => None,
|
ToolType::Text => Some(TextMessage::Abort.into()),
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
ToolType::Brush => None, // Some(BrushMessage::Abort.into()),
|
||||||
|
ToolType::Heal => None, // Some(HealMessage::Abort.into()),
|
||||||
|
ToolType::Clone => None, // Some(CloneMessage::Abort.into()),
|
||||||
|
ToolType::Patch => None, // Some(PatchMessage::Abort.into()),
|
||||||
|
ToolType::Detail => None, // Some(DetailToolMessage::Abort.into()),
|
||||||
|
ToolType::Relight => None, // Some(RelightMessage::Abort.into()),
|
||||||
},
|
},
|
||||||
StandardToolMessageType::SelectionChanged => match tool {
|
StandardToolMessageType::SelectionChanged => match tool {
|
||||||
ToolType::Path => Some(PathToolMessage::SelectionChanged.into()),
|
ToolType::Path => Some(PathToolMessage::SelectionChanged.into()),
|
||||||
@@ -248,19 +404,15 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType {
|
|||||||
use ToolMessage::*;
|
use ToolMessage::*;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
// General tool group
|
||||||
Select(_) => ToolType::Select,
|
Select(_) => ToolType::Select,
|
||||||
Artboard(_) => ToolType::Artboard,
|
Artboard(_) => ToolType::Artboard,
|
||||||
Navigate(_) => ToolType::Navigate,
|
Navigate(_) => ToolType::Navigate,
|
||||||
Eyedropper(_) => ToolType::Eyedropper,
|
Eyedropper(_) => ToolType::Eyedropper,
|
||||||
Text(_) => ToolType::Text,
|
|
||||||
Fill(_) => ToolType::Fill,
|
Fill(_) => ToolType::Fill,
|
||||||
Gradient(_) => ToolType::Gradient,
|
Gradient(_) => ToolType::Gradient,
|
||||||
// Brush(_) => ToolType::Brush,
|
|
||||||
// Heal(_) => ToolType::Heal,
|
// Vector tool group
|
||||||
// Clone(_) => ToolType::Clone,
|
|
||||||
// Patch(_) => ToolType::Patch,
|
|
||||||
// Detail(_) => ToolType::Detail,
|
|
||||||
// Relight(_) => ToolType::Relight,
|
|
||||||
Path(_) => ToolType::Path,
|
Path(_) => ToolType::Path,
|
||||||
Pen(_) => ToolType::Pen,
|
Pen(_) => ToolType::Pen,
|
||||||
Freehand(_) => ToolType::Freehand,
|
Freehand(_) => ToolType::Freehand,
|
||||||
@@ -269,6 +421,15 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType {
|
|||||||
Rectangle(_) => ToolType::Rectangle,
|
Rectangle(_) => ToolType::Rectangle,
|
||||||
Ellipse(_) => ToolType::Ellipse,
|
Ellipse(_) => ToolType::Ellipse,
|
||||||
Shape(_) => ToolType::Shape,
|
Shape(_) => ToolType::Shape,
|
||||||
|
Text(_) => ToolType::Text,
|
||||||
|
|
||||||
|
// Raster tool group
|
||||||
|
// Brush(_) => ToolType::Brush,
|
||||||
|
// Heal(_) => ToolType::Heal,
|
||||||
|
// Clone(_) => ToolType::Clone,
|
||||||
|
// Patch(_) => ToolType::Patch,
|
||||||
|
// Detail(_) => ToolType::Detail,
|
||||||
|
// Relight(_) => ToolType::Relight,
|
||||||
_ => panic!("Conversion from message to tool type impossible because the given ToolMessage does not belong to a tool"),
|
_ => panic!("Conversion from message to tool type impossible because the given ToolMessage does not belong to a tool"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pub enum ToolMessage {
|
|||||||
tool_type: ToolType,
|
tool_type: ToolType,
|
||||||
},
|
},
|
||||||
DocumentIsDirty,
|
DocumentIsDirty,
|
||||||
|
InitTools,
|
||||||
ResetColors,
|
ResetColors,
|
||||||
SelectionChanged,
|
SelectionChanged,
|
||||||
SelectPrimaryColor {
|
SelectPrimaryColor {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::tool::{message_to_tool_type, standard_tool_message, update_working_co
|
|||||||
use crate::document::DocumentMessageHandler;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::input::InputPreprocessorMessageHandler;
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
use crate::layout::layout_message::LayoutTarget;
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
|
use crate::layout::widgets::PropertyHolder;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
use graphene::color::Color;
|
use graphene::color::Color;
|
||||||
@@ -69,12 +70,11 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
|||||||
// Store the new active tool
|
// Store the new active tool
|
||||||
tool_data.active_tool_type = tool_type;
|
tool_data.active_tool_type = tool_type;
|
||||||
|
|
||||||
// Notify the frontend about the new active tool to be displayed
|
|
||||||
let tool_name = tool_type.to_string();
|
|
||||||
responses.push_back(FrontendMessage::UpdateActiveTool { tool_name }.into());
|
|
||||||
|
|
||||||
// Send Properties to the frontend
|
// Send Properties to the frontend
|
||||||
tool_data.tools.get(&tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
|
tool_data.tools.get(&tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
|
||||||
|
|
||||||
|
// Notify the frontend about the new active tool to be displayed
|
||||||
|
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||||
}
|
}
|
||||||
DocumentIsDirty => {
|
DocumentIsDirty => {
|
||||||
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
|
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
|
||||||
@@ -83,6 +83,21 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
|||||||
responses.push_back(message.into());
|
responses.push_back(message.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
InitTools => {
|
||||||
|
let tool_data = &mut self.tool_state.tool_data;
|
||||||
|
let document_data = &self.tool_state.document_tool_data;
|
||||||
|
let active_tool = &tool_data.active_tool_type;
|
||||||
|
|
||||||
|
// Register initial properties
|
||||||
|
tool_data.tools.get(active_tool).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
|
||||||
|
|
||||||
|
// Notify the frontend about the initial active tool
|
||||||
|
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||||
|
|
||||||
|
// Set initial hints and cursor
|
||||||
|
tool_data.active_tool_mut().process_action(ToolMessage::UpdateHints, (document, document_data, input), responses);
|
||||||
|
tool_data.active_tool_mut().process_action(ToolMessage::UpdateCursor, (document, document_data, input), responses);
|
||||||
|
}
|
||||||
ResetColors => {
|
ResetColors => {
|
||||||
let document_data = &mut self.tool_state.document_tool_data;
|
let document_data = &mut self.tool_state.document_tool_data;
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ impl PropertyHolder for FreehandTool {
|
|||||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
label: "Weight".into(),
|
label: "Weight".into(),
|
||||||
value: self.options.line_weight as f64,
|
value: Some(self.options.line_weight as f64),
|
||||||
is_integer: false,
|
is_integer: false,
|
||||||
min: Some(1.),
|
min: Some(1.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ impl PropertyHolder for LineTool {
|
|||||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
label: "Weight".into(),
|
label: "Weight".into(),
|
||||||
value: self.options.line_weight as f64,
|
value: Some(self.options.line_weight as f64),
|
||||||
is_integer: false,
|
is_integer: false,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ impl PropertyHolder for PenTool {
|
|||||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
label: "Weight".into(),
|
label: "Weight".into(),
|
||||||
value: self.options.line_weight,
|
value: Some(self.options.line_weight),
|
||||||
is_integer: false,
|
is_integer: false,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ impl PropertyHolder for ShapeTool {
|
|||||||
WidgetLayout::new(vec![LayoutRow::Row {
|
WidgetLayout::new(vec![LayoutRow::Row {
|
||||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
label: "Sides".into(),
|
label: "Sides".into(),
|
||||||
value: self.options.vertices as f64,
|
value: Some(self.options.vertices as f64),
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(3.),
|
min: Some(3.),
|
||||||
max: Some(256.),
|
max: Some(256.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u8)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ impl PropertyHolder for SplineTool {
|
|||||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
label: "Weight".into(),
|
label: "Weight".into(),
|
||||||
value: self.options.line_weight,
|
value: Some(self.options.line_weight),
|
||||||
is_integer: false,
|
is_integer: false,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ impl PropertyHolder for TextTool {
|
|||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
label: "Size".into(),
|
label: "Size".into(),
|
||||||
value: self.options.font_size as f64,
|
value: Some(self.options.font_size as f64),
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(1.),
|
min: Some(1.),
|
||||||
on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ img {
|
|||||||
.layout-col {
|
.layout-col {
|
||||||
.scrollable-x,
|
.scrollable-x,
|
||||||
.scrollable-y {
|
.scrollable-y {
|
||||||
// Standard
|
// Firefox (standardized in CSS, but less capable)
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-width: 6px;
|
scrollbar-width: 6px;
|
||||||
scrollbar-gutter: 6px;
|
scrollbar-gutter: 6px;
|
||||||
@@ -130,7 +130,7 @@ img {
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebKit
|
// WebKit (only in Chromium/Safari but more capable)
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: calc(2px + 6px + 2px);
|
width: calc(2px + 6px + 2px);
|
||||||
height: calc(2px + 6px + 2px);
|
height: calc(2px + 6px + 2px);
|
||||||
@@ -177,14 +177,14 @@ img {
|
|||||||
|
|
||||||
.scrollable-x:not(.scrollable-y) {
|
.scrollable-x:not(.scrollable-y) {
|
||||||
// Standard
|
// Standard
|
||||||
overflow-x: auto;
|
overflow: auto hidden;
|
||||||
// WebKit
|
// WebKit
|
||||||
overflow-x: overlay;
|
overflow-x: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-y:not(.scrollable-x) {
|
.scrollable-y:not(.scrollable-x) {
|
||||||
// Standard
|
// Standard
|
||||||
overflow-y: auto;
|
overflow: hidden auto;
|
||||||
// WebKit
|
// WebKit
|
||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
}
|
}
|
||||||
@@ -325,6 +325,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen);
|
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen);
|
||||||
|
|
||||||
|
this.editor.instance.init_app();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.inputManager?.removeListeners();
|
this.inputManager?.removeListeners();
|
||||||
|
|||||||
@@ -1,58 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutCol class="document">
|
<LayoutCol class="document">
|
||||||
<LayoutRow class="options-bar" :scrollableX="true">
|
<LayoutRow class="options-bar" :scrollableX="true">
|
||||||
<LayoutRow class="left side">
|
<WidgetLayout :layout="documentModeLayout" />
|
||||||
<DropdownInput :menuEntries="documentModeEntries" v-model:selectedIndex="documentModeSelectionIndex" :drawIcon="true" />
|
<Separator :type="'Section'" />
|
||||||
|
<WidgetLayout :layout="toolOptionsLayout" />
|
||||||
<Separator :type="'Section'" />
|
|
||||||
|
|
||||||
<WidgetLayout :layout="toolOptionsLayout" class="tool-options" />
|
|
||||||
</LayoutRow>
|
|
||||||
|
|
||||||
<LayoutRow class="spacer"></LayoutRow>
|
<LayoutRow class="spacer"></LayoutRow>
|
||||||
|
|
||||||
<WidgetLayout :layout="documentBarLayout" class="right side document-bar" />
|
<WidgetLayout :layout="documentBarLayout" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="shelf-and-viewport">
|
<LayoutRow class="shelf-and-viewport">
|
||||||
<LayoutCol class="shelf">
|
<LayoutCol class="shelf">
|
||||||
<LayoutCol class="tools" :scrollableY="true">
|
<LayoutCol class="tools" :scrollableY="true">
|
||||||
<ShelfItemInput icon="GeneralSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
|
<WidgetLayout :layout="toolShelfLayout" />
|
||||||
<ShelfItemInput icon="GeneralArtboardTool" title="Artboard Tool" :active="activeTool === 'Artboard'" :action="() => selectTool('Artboard')" />
|
|
||||||
<ShelfItemInput icon="GeneralNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" />
|
|
||||||
<ShelfItemInput icon="GeneralEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
|
|
||||||
<ShelfItemInput icon="GeneralFillTool" title="Fill Tool (F)" :active="activeTool === 'Fill'" :action="() => selectTool('Fill')" />
|
|
||||||
<ShelfItemInput icon="GeneralGradientTool" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" :action="() => selectTool('Gradient')" />
|
|
||||||
|
|
||||||
<Separator :type="'Section'" :direction="'Vertical'" />
|
|
||||||
|
|
||||||
<ShelfItemInput icon="VectorPathTool" title="Path Tool (A)" :active="activeTool === 'Path'" :action="() => selectTool('Path')" />
|
|
||||||
<ShelfItemInput icon="VectorPenTool" title="Pen Tool (P)" :active="activeTool === 'Pen'" :action="() => selectTool('Pen')" />
|
|
||||||
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => selectTool('Freehand')" />
|
|
||||||
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => selectTool('Spline')" />
|
|
||||||
<ShelfItemInput icon="VectorLineTool" title="Line Tool (L)" :active="activeTool === 'Line'" :action="() => selectTool('Line')" />
|
|
||||||
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
|
|
||||||
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
|
|
||||||
<ShelfItemInput icon="VectorShapeTool" title="Shape Tool (Y)" :active="activeTool === 'Shape'" :action="() => selectTool('Shape')" />
|
|
||||||
<ShelfItemInput icon="VectorTextTool" title="Text Tool (T)" :active="activeTool === 'Text'" :action="() => selectTool('Text')" />
|
|
||||||
|
|
||||||
<Separator :type="'Section'" :direction="'Vertical'" />
|
|
||||||
|
|
||||||
<ShelfItemInput icon="RasterBrushTool" title="Coming Soon: Brush Tool (B)" :active="activeTool === 'Brush'" :action="() => (dialog.comingSoon(), false) && selectTool('Brush')" />
|
|
||||||
<ShelfItemInput icon="RasterHealTool" title="Coming Soon: Heal Tool (J)" :active="activeTool === 'Heal'" :action="() => (dialog.comingSoon(), false) && selectTool('Heal')" />
|
|
||||||
<ShelfItemInput icon="RasterCloneTool" title="Coming Soon: Clone Tool (C)" :active="activeTool === 'Clone'" :action="() => (dialog.comingSoon(), false) && selectTool('Clone')" />
|
|
||||||
<ShelfItemInput icon="RasterPatchTool" title="Coming Soon: Patch Tool" :active="activeTool === 'Patch'" :action="() => (dialog.comingSoon(), false) && selectTool('Patch')" />
|
|
||||||
<ShelfItemInput
|
|
||||||
icon="RasterDetailTool"
|
|
||||||
title="Coming Soon: Detail Tool (D)"
|
|
||||||
:active="activeTool === 'Detail'"
|
|
||||||
:action="() => (dialog.comingSoon(), false) && selectTool('Detail')"
|
|
||||||
/>
|
|
||||||
<ShelfItemInput
|
|
||||||
icon="RasterRelightTool"
|
|
||||||
title="Coming Soon: Relight Tool (O)"
|
|
||||||
:active="activeTool === 'Relight'"
|
|
||||||
:action="() => (dialog.comingSoon(), false) && selectTool('Relight')"
|
|
||||||
/>
|
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|
||||||
<LayoutCol class="spacer"></LayoutCol>
|
<LayoutCol class="spacer"></LayoutCol>
|
||||||
@@ -128,13 +88,7 @@
|
|||||||
.options-bar {
|
.options-bar {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
margin: 0 4px;
|
||||||
.side {
|
|
||||||
height: 100%;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
@@ -148,7 +102,7 @@
|
|||||||
.tools {
|
.tools {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
|
|
||||||
.shelf-item-input[title^="Coming Soon"] {
|
.icon-button[title^="Coming Soon"] {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
transition: opacity 0.25s;
|
transition: opacity 0.25s;
|
||||||
|
|
||||||
@@ -272,13 +226,11 @@ import {
|
|||||||
UpdateDocumentOverlays,
|
UpdateDocumentOverlays,
|
||||||
UpdateDocumentScrollbars,
|
UpdateDocumentScrollbars,
|
||||||
UpdateDocumentRulers,
|
UpdateDocumentRulers,
|
||||||
UpdateActiveTool,
|
|
||||||
UpdateCanvasZoom,
|
|
||||||
UpdateCanvasRotation,
|
|
||||||
ToolName,
|
|
||||||
UpdateDocumentArtboards,
|
UpdateDocumentArtboards,
|
||||||
UpdateMouseCursor,
|
UpdateMouseCursor,
|
||||||
|
UpdateDocumentModeLayout,
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
|
UpdateToolShelfLayout,
|
||||||
defaultWidgetLayout,
|
defaultWidgetLayout,
|
||||||
UpdateDocumentBarLayout,
|
UpdateDocumentBarLayout,
|
||||||
UpdateImageData,
|
UpdateImageData,
|
||||||
@@ -299,10 +251,6 @@ import { loadDefaultFont, setLoadDefaultFontCallback } from "@/utilities/fonts";
|
|||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
|
||||||
import DropdownInput from "@/components/widgets/inputs/DropdownInput.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 SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
||||||
import CanvasRuler from "@/components/widgets/rulers/CanvasRuler.vue";
|
import CanvasRuler from "@/components/widgets/rulers/CanvasRuler.vue";
|
||||||
import PersistentScrollbar from "@/components/widgets/scrollbars/PersistentScrollbar.vue";
|
import PersistentScrollbar from "@/components/widgets/scrollbars/PersistentScrollbar.vue";
|
||||||
@@ -366,9 +314,6 @@ export default defineComponent({
|
|||||||
const move = delta < 0 ? 1 : -1;
|
const move = delta < 0 ? 1 : -1;
|
||||||
this.editor.instance.translate_canvas_by_fraction(0, move);
|
this.editor.instance.translate_canvas_by_fraction(0, move);
|
||||||
},
|
},
|
||||||
selectTool(toolName: string) {
|
|
||||||
this.editor.instance.select_tool(toolName);
|
|
||||||
},
|
|
||||||
swapWorkingColors() {
|
swapWorkingColors() {
|
||||||
this.editor.instance.swap_colors();
|
this.editor.instance.swap_colors();
|
||||||
},
|
},
|
||||||
@@ -440,19 +385,6 @@ export default defineComponent({
|
|||||||
this.rulerInterval = updateDocumentRulers.interval;
|
this.rulerInterval = updateDocumentRulers.interval;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateActiveTool, (updateActiveTool) => {
|
|
||||||
this.activeTool = updateActiveTool.tool_name;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateCanvasZoom, (updateCanvasZoom) => {
|
|
||||||
this.documentZoom = updateCanvasZoom.factor * 100;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateCanvasRotation, (updateCanvasRotation) => {
|
|
||||||
const newRotation = updateCanvasRotation.angle_radians * (180 / Math.PI);
|
|
||||||
this.documentRotation = (360 + (newRotation % 360)) % 360;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateMouseCursor, (updateMouseCursor) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateMouseCursor, (updateMouseCursor) => {
|
||||||
this.canvasCursor = updateMouseCursor.cursor;
|
this.canvasCursor = updateMouseCursor.cursor;
|
||||||
});
|
});
|
||||||
@@ -502,6 +434,10 @@ export default defineComponent({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentModeLayout, (updateDocumentModeLayout) => {
|
||||||
|
this.documentModeLayout = updateDocumentModeLayout;
|
||||||
|
});
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => {
|
||||||
this.toolOptionsLayout = updateToolOptionsLayout;
|
this.toolOptionsLayout = updateToolOptionsLayout;
|
||||||
});
|
});
|
||||||
@@ -509,6 +445,11 @@ export default defineComponent({
|
|||||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => {
|
||||||
this.documentBarLayout = updateDocumentBarLayout;
|
this.documentBarLayout = updateDocumentBarLayout;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateToolShelfLayout, (updateToolShelfLayout) => {
|
||||||
|
this.toolShelfLayout = updateToolShelfLayout;
|
||||||
|
});
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||||
@@ -524,7 +465,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gets metadat populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
// Gets metadata populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
||||||
const loadBuildMetadata = (): void => {
|
const loadBuildMetadata = (): void => {
|
||||||
const release = process.env.VUE_APP_RELEASE_SERIES;
|
const release = process.env.VUE_APP_RELEASE_SERIES;
|
||||||
let timestamp = "";
|
let timestamp = "";
|
||||||
@@ -544,64 +485,50 @@ export default defineComponent({
|
|||||||
this.editor.instance.populate_build_metadata(release || "", timestamp, hash, branch || "");
|
this.editor.instance.populate_build_metadata(release || "", timestamp, hash, branch || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
|
||||||
// Get initial Document Bar
|
|
||||||
this.editor.instance.init_document_bar();
|
|
||||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||||
|
|
||||||
loadBuildMetadata();
|
loadBuildMetadata();
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
|
||||||
[
|
|
||||||
{ label: "Design Mode", icon: "ViewportDesignMode" },
|
|
||||||
{ label: "Select Mode", icon: "ViewportSelectMode", action: (): void => this.dialog.comingSoon(330) },
|
|
||||||
{ label: "Guide Mode", icon: "ViewportGuideMode", action: (): void => this.dialog.comingSoon(331) },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
const viewModeEntries: RadioEntries = [
|
|
||||||
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal", action: (): void => this.setViewMode("Normal") },
|
|
||||||
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: (): void => this.setViewMode("Outline") },
|
|
||||||
{ value: "pixels", icon: "ViewModePixels", tooltip: "View Mode: Pixels", action: (): void => this.dialog.comingSoon(320) },
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artworkSvg: "",
|
// Interactive text editing
|
||||||
artboardSvg: "",
|
textInput: undefined as undefined | HTMLDivElement,
|
||||||
overlaysSvg: "",
|
|
||||||
|
// CSS properties
|
||||||
canvasSvgWidth: "100%",
|
canvasSvgWidth: "100%",
|
||||||
canvasSvgHeight: "100%",
|
canvasSvgHeight: "100%",
|
||||||
canvasCursor: "default",
|
canvasCursor: "default",
|
||||||
activeTool: "Select" as ToolName,
|
|
||||||
toolOptionsLayout: defaultWidgetLayout(),
|
// Scrollbars
|
||||||
documentBarLayout: defaultWidgetLayout(),
|
|
||||||
documentModeEntries,
|
|
||||||
viewModeEntries,
|
|
||||||
documentModeSelectionIndex: 0,
|
|
||||||
viewModeIndex: 0,
|
|
||||||
snappingEnabled: true,
|
|
||||||
gridEnabled: true,
|
|
||||||
overlaysEnabled: true,
|
|
||||||
documentRotation: 0,
|
|
||||||
documentZoom: 100,
|
|
||||||
scrollbarPos: { x: 0.5, y: 0.5 },
|
scrollbarPos: { x: 0.5, y: 0.5 },
|
||||||
scrollbarSize: { x: 0.5, y: 0.5 },
|
scrollbarSize: { x: 0.5, y: 0.5 },
|
||||||
scrollbarMultiplier: { x: 0, y: 0 },
|
scrollbarMultiplier: { x: 0, y: 0 },
|
||||||
|
|
||||||
|
// Rulers
|
||||||
rulerOrigin: { x: 0, y: 0 },
|
rulerOrigin: { x: 0, y: 0 },
|
||||||
rulerSpacing: 100,
|
rulerSpacing: 100,
|
||||||
rulerInterval: 100,
|
rulerInterval: 100,
|
||||||
textInput: undefined as undefined | HTMLDivElement,
|
|
||||||
|
// Rendered SVG viewport data
|
||||||
|
artworkSvg: "",
|
||||||
|
artboardSvg: "",
|
||||||
|
overlaysSvg: "",
|
||||||
|
|
||||||
|
// Layouts
|
||||||
|
documentModeLayout: defaultWidgetLayout(),
|
||||||
|
toolOptionsLayout: defaultWidgetLayout(),
|
||||||
|
documentBarLayout: defaultWidgetLayout(),
|
||||||
|
toolShelfLayout: defaultWidgetLayout(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
LayoutRow,
|
LayoutRow,
|
||||||
LayoutCol,
|
LayoutCol,
|
||||||
SwatchPairInput,
|
SwatchPairInput,
|
||||||
ShelfItemInput,
|
|
||||||
Separator,
|
Separator,
|
||||||
PersistentScrollbar,
|
PersistentScrollbar,
|
||||||
CanvasRuler,
|
CanvasRuler,
|
||||||
IconButton,
|
IconButton,
|
||||||
DropdownInput,
|
|
||||||
WidgetLayout,
|
WidgetLayout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutCol class="layer-tree">
|
<LayoutCol class="layer-tree">
|
||||||
<LayoutRow class="options-bar">
|
<LayoutRow class="options-bar" :scrollableX="true">
|
||||||
<DropdownInput
|
<WidgetLayout :layout="layerTreeOptionsLayout" />
|
||||||
v-model:selectedIndex="blendModeSelectedIndex"
|
|
||||||
@update:selectedIndex="(newSelectedIndex: number) => setLayerBlendMode(newSelectedIndex)"
|
|
||||||
:menuEntries="blendModeEntries"
|
|
||||||
:disabled="blendModeDropdownDisabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator :type="'Related'" />
|
|
||||||
|
|
||||||
<NumberInput
|
|
||||||
v-model:value="opacity"
|
|
||||||
@update:value="(newOpacity: number) => setLayerOpacity(newOpacity)"
|
|
||||||
:min="0"
|
|
||||||
:max="100"
|
|
||||||
:unit="'%'"
|
|
||||||
:displayDecimalPlaces="2"
|
|
||||||
:label="'Opacity'"
|
|
||||||
:disabled="opacityNumberInputDisabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- <PopoverButton>
|
|
||||||
<h3>Compositing Options</h3>
|
|
||||||
<p>The contents of this popover menu are coming soon</p>
|
|
||||||
</PopoverButton> -->
|
|
||||||
|
|
||||||
<Separator :type="'Section'" />
|
|
||||||
|
|
||||||
<!-- TODO: Remember to make these tooltip input hints customized to macOS also -->
|
|
||||||
<IconButton :action="createEmptyFolder" :icon="'NodeFolder'" title="New Folder (Ctrl+Shift+N)" :size="24" />
|
|
||||||
<IconButton :action="deleteSelectedLayers" :icon="'Trash'" title="Delete Selected (Del)" :size="24" />
|
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="layer-tree-rows" :scrollableY="true">
|
<LayoutRow class="layer-tree-rows" :scrollableY="true">
|
||||||
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
|
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
|
||||||
@@ -101,22 +72,31 @@
|
|||||||
.layer-tree {
|
.layer-tree {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
|
// Options bar
|
||||||
.options-bar {
|
.options-bar {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.widget-layout {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blend mode selector
|
||||||
.dropdown-input {
|
.dropdown-input {
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blend mode selector and opacity slider
|
||||||
.dropdown-input,
|
.dropdown-input,
|
||||||
.number-input {
|
.number-input {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Layer tree
|
||||||
.layer-tree-rows {
|
.layer-tree-rows {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
// Crop away the 1px border below the bottom layer entry when it uses the full space of this panel
|
// Crop away the 1px border below the bottom layer entry when it uses the full space of this panel
|
||||||
@@ -283,58 +263,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import { BlendMode, UpdateDocumentLayerTreeStructure, UpdateDocumentLayer, LayerPanelEntry } from "@/dispatcher/js-messages";
|
import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
import IconButton from "@/components/widgets/buttons/IconButton.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 IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||||
|
|
||||||
type LayerListingInfo = { folderIndex: number; bottomLayer: boolean; editingName: boolean; entry: LayerPanelEntry };
|
type LayerListingInfo = { folderIndex: number; bottomLayer: boolean; editingName: boolean; entry: LayerPanelEntry };
|
||||||
|
|
||||||
const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
|
||||||
[{ label: "Normal", value: "Normal" }],
|
|
||||||
[
|
|
||||||
{ label: "Multiply", value: "Multiply" },
|
|
||||||
{ label: "Darken", value: "Darken" },
|
|
||||||
{ label: "Color Burn", value: "ColorBurn" },
|
|
||||||
// { label: "Linear Burn", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Darker Color", value: "" }, // Not supported by SVG
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ label: "Screen", value: "Screen" },
|
|
||||||
{ label: "Lighten", value: "Lighten" },
|
|
||||||
{ label: "Color Dodge", value: "ColorDodge" },
|
|
||||||
// { label: "Linear Dodge (Add)", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Lighter Color", value: "" }, // Not supported by SVG
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ label: "Overlay", value: "Overlay" },
|
|
||||||
{ label: "Soft Light", value: "SoftLight" },
|
|
||||||
{ label: "Hard Light", value: "HardLight" },
|
|
||||||
// { label: "Vivid Light", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Linear Light", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Pin Light", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Hard Mix", value: "" }, // Not supported by SVG
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ label: "Difference", value: "Difference" },
|
|
||||||
{ label: "Exclusion", value: "Exclusion" },
|
|
||||||
// { label: "Subtract", value: "" }, // Not supported by SVG
|
|
||||||
// { label: "Divide", value: "" }, // Not supported by SVG
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ label: "Hue", value: "Hue" },
|
|
||||||
{ label: "Saturation", value: "Saturation" },
|
|
||||||
{ label: "Color", value: "Color" },
|
|
||||||
{ label: "Luminosity", value: "Luminosity" },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20;
|
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20;
|
||||||
const LAYER_INDENT = 16;
|
const LAYER_INDENT = 16;
|
||||||
const INSERT_MARK_MARGIN_LEFT = 4 + 32 + LAYER_INDENT;
|
const INSERT_MARK_MARGIN_LEFT = 4 + 32 + LAYER_INDENT;
|
||||||
@@ -346,20 +284,17 @@ export default defineComponent({
|
|||||||
inject: ["editor"],
|
inject: ["editor"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
blendModeEntries,
|
// Layer data
|
||||||
blendModeSelectedIndex: 0,
|
layerCache: new Map() as Map<string, LayerPanelEntry>, // TODO: replace with BigUint64Array as index
|
||||||
blendModeDropdownDisabled: true,
|
|
||||||
opacityNumberInputDisabled: true,
|
|
||||||
// TODO: replace with BigUint64Array as index
|
|
||||||
layerCache: new Map() as Map<string, LayerPanelEntry>,
|
|
||||||
layers: [] as LayerListingInfo[],
|
layers: [] as LayerListingInfo[],
|
||||||
layerDepths: [] as number[],
|
devMode: process.env.NODE_ENV === "development",
|
||||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
|
||||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
// Interactive dragging
|
||||||
opacity: 100,
|
|
||||||
draggable: true,
|
draggable: true,
|
||||||
draggingData: undefined as undefined | DraggingData,
|
draggingData: undefined as undefined | DraggingData,
|
||||||
devMode: process.env.NODE_ENV === "development",
|
|
||||||
|
// Layouts
|
||||||
|
layerTreeOptionsLayout: defaultWidgetLayout(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -372,12 +307,6 @@ export default defineComponent({
|
|||||||
markTopOffset(height: number): string {
|
markTopOffset(height: number): string {
|
||||||
return `${height}px`;
|
return `${height}px`;
|
||||||
},
|
},
|
||||||
createEmptyFolder() {
|
|
||||||
this.editor.instance.create_empty_folder();
|
|
||||||
},
|
|
||||||
deleteSelectedLayers() {
|
|
||||||
this.editor.instance.delete_selected_layers();
|
|
||||||
},
|
|
||||||
toggleLayerVisibility(path: BigUint64Array) {
|
toggleLayerVisibility(path: BigUint64Array) {
|
||||||
this.editor.instance.toggle_layer_visibility(path);
|
this.editor.instance.toggle_layer_visibility(path);
|
||||||
},
|
},
|
||||||
@@ -413,27 +342,12 @@ export default defineComponent({
|
|||||||
window.getSelection()?.removeAllRanges();
|
window.getSelection()?.removeAllRanges();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async setLayerBlendMode(newSelectedIndex: number) {
|
|
||||||
const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value;
|
|
||||||
if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
|
|
||||||
},
|
|
||||||
async setLayerOpacity(newOpacity: number) {
|
|
||||||
this.editor.instance.set_opacity_for_selected_layers(newOpacity);
|
|
||||||
},
|
|
||||||
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
|
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
|
||||||
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
|
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
|
||||||
},
|
},
|
||||||
async deselectAllLayers() {
|
async deselectAllLayers() {
|
||||||
this.selectionRangeStartLayer = undefined;
|
|
||||||
this.selectionRangeEndLayer = undefined;
|
|
||||||
|
|
||||||
this.editor.instance.deselect_all_layers();
|
this.editor.instance.deselect_all_layers();
|
||||||
},
|
},
|
||||||
async clearSelection() {
|
|
||||||
this.layers.forEach((layer) => {
|
|
||||||
layer.entry.layer_metadata.selected = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
calculateDragIndex(tree: HTMLElement, clientY: number): DraggingData {
|
calculateDragIndex(tree: HTMLElement, clientY: number): DraggingData {
|
||||||
const treeChildren = tree.children;
|
const treeChildren = tree.children;
|
||||||
const treeOffset = tree.getBoundingClientRect().top;
|
const treeOffset = tree.getBoundingClientRect().top;
|
||||||
@@ -523,51 +437,7 @@ export default defineComponent({
|
|||||||
this.draggingData = undefined;
|
this.draggingData = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: Move blend mode setting logic to backend based on the layers it knows are selected
|
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructure) {
|
||||||
setBlendModeForSelectedLayers() {
|
|
||||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
|
||||||
|
|
||||||
if (selected.length < 1) {
|
|
||||||
this.blendModeSelectedIndex = 0;
|
|
||||||
this.blendModeDropdownDisabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.blendModeDropdownDisabled = false;
|
|
||||||
|
|
||||||
const firstEncounteredBlendMode = selected[0].entry.blend_mode;
|
|
||||||
const allBlendModesAlike = !selected.find((layer) => layer.entry.blend_mode !== firstEncounteredBlendMode);
|
|
||||||
|
|
||||||
if (allBlendModesAlike) {
|
|
||||||
this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode);
|
|
||||||
} else {
|
|
||||||
// Display a dash when they are not all the same value
|
|
||||||
this.blendModeSelectedIndex = NaN;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// TODO: Move opacity setting logic to backend based on the layers it knows are selected
|
|
||||||
setOpacityForSelectedLayers() {
|
|
||||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
|
||||||
|
|
||||||
if (selected.length < 1) {
|
|
||||||
this.opacity = 100;
|
|
||||||
this.opacityNumberInputDisabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.opacityNumberInputDisabled = false;
|
|
||||||
|
|
||||||
const firstEncounteredOpacity = selected[0].entry.opacity;
|
|
||||||
const allOpacitiesAlike = !selected.find((layer) => layer.entry.opacity !== firstEncounteredOpacity);
|
|
||||||
|
|
||||||
if (allOpacitiesAlike) {
|
|
||||||
this.opacity = firstEncounteredOpacity;
|
|
||||||
} else {
|
|
||||||
// Display a dash when they are not all the same value
|
|
||||||
this.opacity = NaN;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
|
||||||
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
|
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
|
||||||
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
|
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
|
||||||
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
|
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
|
||||||
@@ -598,31 +468,35 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
|
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
||||||
|
this.rebuildLayerTree(updateDocumentLayerTreeStructure);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayer, (updateDocumentLayer) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
|
||||||
const targetPath = updateDocumentLayer.data.path;
|
this.layerTreeOptionsLayout = updateLayerTreeOptionsLayout;
|
||||||
const targetLayer = updateDocumentLayer.data;
|
});
|
||||||
|
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
|
||||||
|
const targetPath = updateDocumentLayerDetails.data.path;
|
||||||
|
const targetLayer = updateDocumentLayerDetails.data;
|
||||||
|
|
||||||
const layer = this.layerCache.get(targetPath.toString());
|
const layer = this.layerCache.get(targetPath.toString());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
Object.assign(this.layerCache.get(targetPath.toString()), targetLayer);
|
Object.assign(layer, targetLayer);
|
||||||
} else {
|
} else {
|
||||||
this.layerCache.set(targetPath.toString(), targetLayer);
|
this.layerCache.set(targetPath.toString(), targetLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setBlendModeForSelectedLayers();
|
|
||||||
this.setOpacityForSelectedLayers();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
LayoutRow,
|
LayoutRow,
|
||||||
LayoutCol,
|
LayoutCol,
|
||||||
Separator,
|
WidgetLayout,
|
||||||
NumberInput,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
IconLabel,
|
IconLabel,
|
||||||
DropdownInput,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="widget-layout">
|
<div class="widget-layout">
|
||||||
<template v-for="(layoutRow, index) in layout.layout" :key="index">
|
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target" v-for="(layoutRow, index) in layout.layout" :key="index" />
|
||||||
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target"></component>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,7 +16,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/dispatcher/js-messages";
|
import { isWidgetColumn, isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
||||||
import WidgetSection from "@/components/widgets/WidgetSection.vue";
|
import WidgetSection from "@/components/widgets/WidgetSection.vue";
|
||||||
@@ -29,6 +27,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
layoutRowType(layoutRow: LayoutRow): unknown {
|
layoutRowType(layoutRow: LayoutRow): unknown {
|
||||||
|
if (isWidgetColumn(layoutRow)) return WidgetRow;
|
||||||
if (isWidgetRow(layoutRow)) return WidgetRow;
|
if (isWidgetRow(layoutRow)) return WidgetRow;
|
||||||
if (isWidgetSection(layoutRow)) return WidgetSection;
|
if (isWidgetSection(layoutRow)) return WidgetSection;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="widget-row">
|
<div :class="`widget-${direction}`">
|
||||||
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
<template v-for="(component, index) in widgets" :key="index">
|
||||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||||
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
@@ -35,6 +35,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.widget-column {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.widget-row {
|
.widget-row {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -63,7 +69,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { WidgetRow } from "@/dispatcher/js-messages";
|
import { WidgetColumn, WidgetRow, isWidgetColumn, isWidgetRow } from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||||
@@ -84,9 +90,21 @@ import Separator from "@/components/widgets/separators/Separator.vue";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inject: ["editor"],
|
inject: ["editor"],
|
||||||
props: {
|
props: {
|
||||||
widgetData: { type: Object as PropType<WidgetRow>, required: true },
|
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
|
||||||
layoutTarget: { required: true },
|
layoutTarget: { required: true },
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
direction() {
|
||||||
|
if (isWidgetColumn(this.widgetData)) return "column";
|
||||||
|
if (isWidgetRow(this.widgetData)) return "row";
|
||||||
|
return "ERROR";
|
||||||
|
},
|
||||||
|
widgets() {
|
||||||
|
if (isWidgetColumn(this.widgetData)) return this.widgetData.columnWidgets;
|
||||||
|
if (isWidgetRow(this.widgetData)) return this.widgetData.rowWidgets;
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateLayout(widgetId: BigInt, value: unknown) {
|
updateLayout(widgetId: BigInt, value: unknown) {
|
||||||
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-button" :class="`size-${size}`" @click="(e: MouseEvent) => action(e)">
|
<button :class="['icon-button', `size-${size}`, active && 'active']" @click="(e: MouseEvent) => action(e)">
|
||||||
<IconLabel :icon="icon" />
|
<IconLabel :icon="icon" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,7 +25,11 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&.active {
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
background: var(--color-6-lowergray);
|
background: var(--color-6-lowergray);
|
||||||
color: var(--color-f-white);
|
color: var(--color-f-white);
|
||||||
|
|
||||||
@@ -68,6 +72,7 @@ export default defineComponent({
|
|||||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||||
icon: { type: String as PropType<IconName>, required: true },
|
icon: { type: String as PropType<IconName>, required: true },
|
||||||
size: { type: Number as PropType<IconSize>, required: true },
|
size: { type: Number as PropType<IconSize>, required: true },
|
||||||
|
active: { type: Boolean as PropType<boolean>, default: false },
|
||||||
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
||||||
},
|
},
|
||||||
components: { IconLabel },
|
components: { IconLabel },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="main-column">
|
<LayoutCol class="main-column">
|
||||||
<WidgetLayout v-if="dialog.state.widgets.layout.length > 0" :layout="dialog.state.widgets" class="details" />
|
<WidgetLayout v-if="dialog.state.widgets.layout.length > 0" :layout="dialog.state.widgets" class="details" />
|
||||||
<LayoutRow v-if="dialog.state.jsCallbackBasedButtons?.length > 0" class="panic-buttons-row">
|
<LayoutRow v-if="(dialog.state.jsCallbackBasedButtons?.length || NaN) > 0" class="panic-buttons-row">
|
||||||
<TextButton v-for="(button, index) in dialog.state.jsCallbackBasedButtons" :key="index" :action="() => button.callback?.()" v-bind="button.props" />
|
<TextButton v-for="(button, index) in dialog.state.jsCallbackBasedButtons" :key="index" :action="() => button.callback?.()" v-bind="button.props" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<FloatingMenu class="menu-list" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollableY="scrollableY" data-hover-menu-keep-open>
|
<FloatingMenu class="menu-list" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollableY="scrollableY" data-hover-menu-keep-open>
|
||||||
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
|
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
||||||
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
v-for="(entry, entryIndex) in section"
|
v-for="(entry, entryIndex) in section"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<MenuList
|
<MenuList
|
||||||
v-if="entry.children"
|
v-if="entry.children"
|
||||||
:direction="'TopRight'"
|
:direction="'TopRight'"
|
||||||
:menuEntries="entry.children"
|
:entries="entry.children"
|
||||||
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
||||||
:ref="(ref: any) => setEntryRefs(entry, ref)"
|
:ref="(ref: any) => setEntryRefs(entry, ref)"
|
||||||
/>
|
/>
|
||||||
@@ -167,7 +167,7 @@ const MenuList = defineComponent({
|
|||||||
inject: ["fullscreen"],
|
inject: ["fullscreen"],
|
||||||
props: {
|
props: {
|
||||||
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
|
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
|
||||||
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
||||||
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
|
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
|
||||||
defaultAction: { type: Function as PropType<() => void>, required: false },
|
defaultAction: { type: Function as PropType<() => void>, required: false },
|
||||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||||
@@ -243,9 +243,9 @@ const MenuList = defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
menuEntriesWithoutRefs(): MenuListEntryData[][] {
|
entriesWithoutRefs(): MenuListEntryData[][] {
|
||||||
return this.menuEntries.map((entries) =>
|
return this.entries.map((menuListEntries) =>
|
||||||
entries.map((entry) => {
|
menuListEntries.map((entry) => {
|
||||||
const { ref, ...entryWithoutRef } = entry;
|
const { ref, ...entryWithoutRef } = entry;
|
||||||
return entryWithoutRef;
|
return entryWithoutRef;
|
||||||
})
|
})
|
||||||
@@ -259,7 +259,7 @@ const MenuList = defineComponent({
|
|||||||
this.measureAndReportWidth();
|
this.measureAndReportWidth();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
menuEntriesWithoutRefs: {
|
entriesWithoutRefs: {
|
||||||
handler() {
|
handler() {
|
||||||
this.measureAndReportWidth();
|
this.measureAndReportWidth();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
v-model:activeEntry="activeEntry"
|
v-model:activeEntry="activeEntry"
|
||||||
@update:activeEntry="(newActiveEntry: typeof MENU_LIST_ENTRY) => activeEntryChanged(newActiveEntry)"
|
@update:activeEntry="(newActiveEntry: typeof MENU_LIST_ENTRY) => activeEntryChanged(newActiveEntry)"
|
||||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
||||||
:menuEntries="menuEntries"
|
:entries="entries"
|
||||||
:direction="'Bottom'"
|
:direction="'Bottom'"
|
||||||
:drawIcon="drawIcon"
|
:drawIcon="drawIcon"
|
||||||
:scrollableY="true"
|
:scrollableY="true"
|
||||||
@@ -100,23 +100,23 @@ declare global {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ["update:selectedIndex"],
|
emits: ["update:selectedIndex"],
|
||||||
props: {
|
props: {
|
||||||
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
||||||
selectedIndex: { type: Number as PropType<number>, required: true },
|
selectedIndex: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeEntry: this.menuEntries.flat()[this.selectedIndex],
|
activeEntry: this.selectedIndex !== undefined ? this.entries.flat()[this.selectedIndex] : { label: "-" },
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
||||||
selectedIndex(newSelectedIndex: number) {
|
selectedIndex(newSelectedIndex: number | undefined) {
|
||||||
const entries = this.menuEntries.flat();
|
const entries = this.entries.flat();
|
||||||
|
|
||||||
if (!Number.isNaN(newSelectedIndex) && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
|
if (newSelectedIndex !== undefined && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
|
||||||
this.activeEntry = entries[newSelectedIndex];
|
this.activeEntry = entries[newSelectedIndex];
|
||||||
} else {
|
} else {
|
||||||
this.activeEntry = { label: "-" };
|
this.activeEntry = { label: "-" };
|
||||||
@@ -126,7 +126,7 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
// Called only when `activeEntry` is changed from the child MenuList component via user input
|
// Called only when `activeEntry` is changed from the child MenuList component via user input
|
||||||
activeEntryChanged(newActiveEntry: MenuListEntry) {
|
activeEntryChanged(newActiveEntry: MenuListEntry) {
|
||||||
this.$emit("update:selectedIndex", this.menuEntries.flat().indexOf(newActiveEntry));
|
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
||||||
},
|
},
|
||||||
clickDropdownBox() {
|
clickDropdownBox() {
|
||||||
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
|
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
|
||||||
|
|||||||
@@ -4,14 +4,7 @@
|
|||||||
<span>{{ activeEntry.label }}</span>
|
<span>{{ activeEntry.label }}</span>
|
||||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<MenuList
|
<MenuList v-model:activeEntry="activeEntry" @widthChanged="(newWidth: number) => onWidthChanged(newWidth)" :entries="entries" :direction="'Bottom'" :scrollableY="true" ref="menuList" />
|
||||||
v-model:activeEntry="activeEntry"
|
|
||||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
|
||||||
:menuEntries="menuEntries"
|
|
||||||
:direction="'Bottom'"
|
|
||||||
:scrollableY="true"
|
|
||||||
ref="menuList"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -100,9 +93,9 @@ export default defineComponent({
|
|||||||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const { menuEntries, activeEntry } = this.updateEntries();
|
const { entries, activeEntry } = this.updateEntries();
|
||||||
return {
|
return {
|
||||||
menuEntries,
|
entries,
|
||||||
activeEntry,
|
activeEntry,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
};
|
};
|
||||||
@@ -133,12 +126,12 @@ export default defineComponent({
|
|||||||
onWidthChanged(newWidth: number) {
|
onWidthChanged(newWidth: number) {
|
||||||
this.minWidth = newWidth;
|
this.minWidth = newWidth;
|
||||||
},
|
},
|
||||||
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
|
updateEntries(): { entries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
|
||||||
const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
|
const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
|
||||||
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
|
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
|
||||||
|
|
||||||
let selectedEntry: MenuListEntry | undefined;
|
let selectedEntry: MenuListEntry | undefined;
|
||||||
const entries = choices.map((name) => {
|
const menuListEntries = choices.map((name) => {
|
||||||
const result: MenuListEntry = {
|
const result: MenuListEntry = {
|
||||||
label: name,
|
label: name,
|
||||||
action: (): void => this.selectFont(name),
|
action: (): void => this.selectFont(name),
|
||||||
@@ -149,21 +142,21 @@ export default defineComponent({
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuEntries: SectionsOfMenuListEntries = [entries];
|
const entries: SectionsOfMenuListEntries = [menuListEntries];
|
||||||
const activeEntry = selectedEntry || { label: "-" };
|
const activeEntry = selectedEntry || { label: "-" };
|
||||||
|
|
||||||
return { menuEntries, activeEntry };
|
return { entries, activeEntry };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
fontFamily() {
|
fontFamily() {
|
||||||
const { menuEntries, activeEntry } = this.updateEntries();
|
const { entries, activeEntry } = this.updateEntries();
|
||||||
this.menuEntries = menuEntries;
|
this.entries = entries;
|
||||||
this.activeEntry = activeEntry;
|
this.activeEntry = activeEntry;
|
||||||
},
|
},
|
||||||
fontStyle() {
|
fontStyle() {
|
||||||
const { menuEntries, activeEntry } = this.updateEntries();
|
const { entries, activeEntry } = this.updateEntries();
|
||||||
this.menuEntries = menuEntries;
|
this.entries = entries;
|
||||||
this.activeEntry = activeEntry;
|
this.activeEntry = activeEntry;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<IconLabel :icon="'GraphiteLogo'" />
|
<IconLabel :icon="'GraphiteLogo'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
|
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
||||||
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
|
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
|
||||||
<IconLabel :icon="entry.icon" v-if="entry.icon" />
|
<IconLabel :icon="entry.icon" v-if="entry.icon" />
|
||||||
<span v-if="entry.label">{{ entry.label }}</span>
|
<span v-if="entry.label">{{ entry.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<MenuList :menuEntries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
|
<MenuList :entries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,7 +58,7 @@ import { EditorState } from "@/state/wasm-loader";
|
|||||||
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
function makeMenuEntries(editor: EditorState): MenuListEntries {
|
function makeEntries(editor: EditorState): MenuListEntries {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: "File",
|
label: "File",
|
||||||
@@ -213,7 +213,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menuEntries: makeMenuEntries(this.editor),
|
entries: makeEntries(this.editor),
|
||||||
comingSoon: (): void => this.dialog.comingSoon(),
|
comingSoon: (): void => this.dialog.comingSoon(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
@cancelTextChange="() => onCancelTextChange()"
|
@cancelTextChange="() => onCancelTextChange()"
|
||||||
ref="fieldInput"
|
ref="fieldInput"
|
||||||
>
|
>
|
||||||
<button v-if="!Number.isNaN(value)" class="arrow left" @click="() => onIncrement('Decrease')"></button>
|
<button v-if="value !== undefined" class="arrow left" @click="() => onIncrement('Decrease')"></button>
|
||||||
<button v-if="!Number.isNaN(value)" class="arrow right" @click="() => onIncrement('Increase')"></button>
|
<button v-if="value !== undefined" class="arrow right" @click="() => onIncrement('Increase')"></button>
|
||||||
</FieldInput>
|
</FieldInput>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ["update:value"],
|
emits: ["update:value"],
|
||||||
props: {
|
props: {
|
||||||
value: { type: Number as PropType<number>, required: true },
|
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||||
min: { type: Number as PropType<number>, required: false },
|
min: { type: Number as PropType<number>, required: false },
|
||||||
max: { type: Number as PropType<number>, required: false },
|
max: { type: Number as PropType<number>, required: false },
|
||||||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||||
@@ -116,7 +116,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onTextFocused() {
|
onTextFocused() {
|
||||||
if (Number.isNaN(this.value)) this.text = "";
|
if (this.value === undefined) this.text = "";
|
||||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||||
else this.text = `${this.value}${this.unit}`;
|
else this.text = `${this.value}${this.unit}`;
|
||||||
|
|
||||||
@@ -131,18 +131,20 @@ export default defineComponent({
|
|||||||
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
|
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
|
||||||
onTextChanged() {
|
onTextChanged() {
|
||||||
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
|
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
|
||||||
if (this.editing) {
|
if (!this.editing) return;
|
||||||
const newValue = parseFloat(this.text);
|
|
||||||
this.updateValue(newValue);
|
|
||||||
|
|
||||||
this.editing = false;
|
const parsed = parseFloat(this.text);
|
||||||
|
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||||
|
|
||||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
this.updateValue(newValue);
|
||||||
inputElement.blur();
|
|
||||||
}
|
this.editing = false;
|
||||||
|
|
||||||
|
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||||
|
inputElement.blur();
|
||||||
},
|
},
|
||||||
onCancelTextChange() {
|
onCancelTextChange() {
|
||||||
this.updateValue(NaN);
|
this.updateValue(undefined);
|
||||||
|
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
|
||||||
@@ -150,16 +152,16 @@ export default defineComponent({
|
|||||||
inputElement.blur();
|
inputElement.blur();
|
||||||
},
|
},
|
||||||
onIncrement(direction: IncrementDirection) {
|
onIncrement(direction: IncrementDirection) {
|
||||||
if (Number.isNaN(this.value)) return;
|
if (this.value === undefined) return;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
Add: (): void => {
|
Add: (): void => {
|
||||||
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
|
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
|
||||||
this.updateValue(this.value + directionAddend);
|
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
||||||
},
|
},
|
||||||
Multiply: (): void => {
|
Multiply: (): void => {
|
||||||
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
|
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
|
||||||
this.updateValue(this.value * directionMultiplier);
|
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
||||||
},
|
},
|
||||||
Callback: (): void => {
|
Callback: (): void => {
|
||||||
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
||||||
@@ -170,20 +172,20 @@ export default defineComponent({
|
|||||||
const action = actions[this.incrementBehavior];
|
const action = actions[this.incrementBehavior];
|
||||||
action();
|
action();
|
||||||
},
|
},
|
||||||
updateValue(newValue: number) {
|
updateValue(newValue: number | undefined) {
|
||||||
const invalid = Number.isNaN(newValue);
|
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
||||||
|
let cleaned = newValue !== undefined ? newValue : nowValid;
|
||||||
|
|
||||||
let sanitized = newValue;
|
if (typeof this.min === "number" && !Number.isNaN(this.min) && cleaned !== undefined) cleaned = Math.max(cleaned, this.min);
|
||||||
if (invalid) sanitized = this.value;
|
if (typeof this.max === "number" && !Number.isNaN(this.max) && cleaned !== undefined) cleaned = Math.min(cleaned, this.max);
|
||||||
if (this.isInteger) sanitized = Math.round(sanitized);
|
|
||||||
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);
|
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
||||||
|
|
||||||
this.text = this.displayText(sanitized);
|
this.text = this.displayText(cleaned);
|
||||||
},
|
},
|
||||||
displayText(value: number): string {
|
displayText(value: number | undefined): string {
|
||||||
|
if (value === undefined) return "-";
|
||||||
|
|
||||||
// Find the amount of digits on the left side of the decimal
|
// Find the amount of digits on the left side of the decimal
|
||||||
// 10.25 == 2
|
// 10.25 == 2
|
||||||
// 1.23 == 1
|
// 1.23 == 1
|
||||||
@@ -199,8 +201,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// Called only when `value` is changed from outside this component (with v-model)
|
// Called only when `value` is changed from outside this component (with v-model)
|
||||||
value(newValue: number) {
|
value(newValue: number | undefined) {
|
||||||
if (Number.isNaN(newValue)) {
|
if (newValue === undefined) {
|
||||||
this.text = "-";
|
this.text = "-";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<LayoutRow class="shelf-item-input" :class="{ active: active }">
|
|
||||||
<IconButton :action="action" :icon="icon" :size="32" />
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.shelf-item-input {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
background: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
import { IconName } from "@/utilities/icons";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
icon: { type: String as PropType<IconName>, required: true },
|
|
||||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
|
||||||
active: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -7,12 +7,14 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.separator {
|
.separator {
|
||||||
&.vertical {
|
&.vertical {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&.related {
|
&.related {
|
||||||
margin-top: 4px;
|
height: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unrelated {
|
&.unrelated {
|
||||||
margin-top: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.section,
|
&.section,
|
||||||
@@ -37,12 +39,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&.related {
|
&.related {
|
||||||
margin-left: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unrelated {
|
&.unrelated {
|
||||||
margin-left: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.section,
|
&.section,
|
||||||
|
|||||||
@@ -61,10 +61,6 @@ export default defineComponent({
|
|||||||
this.editor.dispatcher.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
this.editor.dispatcher.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
||||||
this.hintData = updateInputHints.hint_data;
|
this.hintData = updateInputHints.hint_data;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Switch away from, and back to, the Select Tool to make it display the correct hints in the status bar
|
|
||||||
this.editor.instance.select_tool("Path");
|
|
||||||
this.editor.instance.select_tool("Select");
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
UserInputLabel,
|
UserInputLabel,
|
||||||
|
|||||||
@@ -117,33 +117,6 @@ export class UpdateWorkingColors extends JsMessage {
|
|||||||
readonly secondary!: Color;
|
readonly secondary!: Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolName =
|
|
||||||
| "Select"
|
|
||||||
| "Artboard"
|
|
||||||
| "Navigate"
|
|
||||||
| "Eyedropper"
|
|
||||||
| "Text"
|
|
||||||
| "Fill"
|
|
||||||
| "Gradient"
|
|
||||||
| "Brush"
|
|
||||||
| "Heal"
|
|
||||||
| "Clone"
|
|
||||||
| "Patch"
|
|
||||||
| "Detail"
|
|
||||||
| "Relight"
|
|
||||||
| "Path"
|
|
||||||
| "Pen"
|
|
||||||
| "Freehand"
|
|
||||||
| "Spline"
|
|
||||||
| "Line"
|
|
||||||
| "Rectangle"
|
|
||||||
| "Ellipse"
|
|
||||||
| "Shape";
|
|
||||||
|
|
||||||
export class UpdateActiveTool extends JsMessage {
|
|
||||||
readonly tool_name!: ToolName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateActiveDocument extends JsMessage {
|
export class UpdateActiveDocument extends JsMessage {
|
||||||
readonly document_id!: BigInt;
|
readonly document_id!: BigInt;
|
||||||
}
|
}
|
||||||
@@ -320,48 +293,16 @@ export class UpdateImageData extends JsMessage {
|
|||||||
|
|
||||||
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||||
|
|
||||||
export class UpdateDocumentLayer extends JsMessage {
|
export class UpdateDocumentLayerDetails extends JsMessage {
|
||||||
@Type(() => LayerPanelEntry)
|
@Type(() => LayerPanelEntry)
|
||||||
readonly data!: LayerPanelEntry;
|
readonly data!: LayerPanelEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCanvasZoom extends JsMessage {
|
|
||||||
readonly factor!: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateCanvasRotation extends JsMessage {
|
|
||||||
readonly angle_radians!: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BlendMode =
|
|
||||||
| "Normal"
|
|
||||||
| "Multiply"
|
|
||||||
| "Darken"
|
|
||||||
| "ColorBurn"
|
|
||||||
| "Screen"
|
|
||||||
| "Lighten"
|
|
||||||
| "ColorDodge"
|
|
||||||
| "Overlay"
|
|
||||||
| "SoftLight"
|
|
||||||
| "HardLight"
|
|
||||||
| "Difference"
|
|
||||||
| "Exclusion"
|
|
||||||
| "Hue"
|
|
||||||
| "Saturation"
|
|
||||||
| "Color"
|
|
||||||
| "Luminosity";
|
|
||||||
|
|
||||||
export class LayerPanelEntry {
|
export class LayerPanelEntry {
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
visible!: boolean;
|
visible!: boolean;
|
||||||
|
|
||||||
blend_mode!: BlendMode;
|
|
||||||
|
|
||||||
// On the rust side opacity is out of 1 rather than 100
|
|
||||||
@Transform(({ value }) => value * 100)
|
|
||||||
opacity!: number;
|
|
||||||
|
|
||||||
layer_type!: LayerType;
|
layer_type!: LayerType;
|
||||||
|
|
||||||
@Transform(({ value }) => new BigUint64Array(value))
|
@Transform(({ value }) => new BigUint64Array(value))
|
||||||
@@ -433,15 +374,21 @@ export function defaultWidgetLayout(): WidgetLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LayoutRow = WidgetRow | WidgetSection;
|
// TODO: Rename LayoutRow to something more generic
|
||||||
|
export type LayoutRow = WidgetRow | WidgetColumn | WidgetSection;
|
||||||
|
|
||||||
export type WidgetRow = { widgets: Widget[] };
|
export type WidgetColumn = { columnWidgets: Widget[] };
|
||||||
export function isWidgetRow(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetRow {
|
export function isWidgetColumn(layoutColumn: LayoutRow): layoutColumn is WidgetColumn {
|
||||||
return Boolean((layoutRow as WidgetRow).widgets);
|
return Boolean((layoutColumn as WidgetColumn).columnWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WidgetRow = { rowWidgets: Widget[] };
|
||||||
|
export function isWidgetRow(layoutRow: LayoutRow): layoutRow is WidgetRow {
|
||||||
|
return Boolean((layoutRow as WidgetRow).rowWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetSection = { name: string; layout: LayoutRow[] };
|
export type WidgetSection = { name: string; layout: LayoutRow[] };
|
||||||
export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetSection {
|
export function isWidgetSection(layoutRow: LayoutRow): layoutRow is WidgetSection {
|
||||||
return Boolean((layoutRow as WidgetSection).layout);
|
return Boolean((layoutRow as WidgetSection).layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +423,13 @@ export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
|||||||
layout!: LayoutRow[];
|
layout!: LayoutRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
layout_target!: unknown;
|
layout_target!: unknown;
|
||||||
|
|
||||||
@@ -490,6 +444,13 @@ export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
|
|||||||
layout!: LayoutRow[];
|
layout!: LayoutRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
layout_target!: unknown;
|
layout_target!: unknown;
|
||||||
|
|
||||||
@@ -504,13 +465,20 @@ export class UpdatePropertyPanelSectionsLayout extends JsMessage implements Widg
|
|||||||
layout!: LayoutRow[];
|
layout!: LayoutRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
// Unpacking rust types to more usable type in the frontend
|
// Unpacking rust types to more usable type in the frontend
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
||||||
return widgetLayout.map((rowOrSection): LayoutRow => {
|
return widgetLayout.map((layoutType): LayoutRow => {
|
||||||
if (rowOrSection.Row) {
|
if (layoutType.Column) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
|
const columnWidgets = layoutType.Column.columnWidgets.map((widgetHolder: any) => {
|
||||||
const { widget_id } = widgetHolder;
|
const { widget_id } = widgetHolder;
|
||||||
const kind = Object.keys(widgetHolder.widget)[0];
|
const kind = Object.keys(widgetHolder.widget)[0];
|
||||||
const props = widgetHolder.widget[kind];
|
const props = widgetHolder.widget[kind];
|
||||||
@@ -518,13 +486,27 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
|||||||
return { widget_id, kind, props };
|
return { widget_id, kind, props };
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: WidgetRow = { widgets };
|
const result: WidgetColumn = { columnWidgets };
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowOrSection.Section) {
|
if (layoutType.Row) {
|
||||||
const { name } = rowOrSection.Section;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const layout = createWidgetLayout(rowOrSection.Section.layout);
|
const rowWidgets = layoutType.Row.rowWidgets.map((widgetHolder: any) => {
|
||||||
|
const { widget_id } = widgetHolder;
|
||||||
|
const kind = Object.keys(widgetHolder.widget)[0];
|
||||||
|
const props = widgetHolder.widget[kind];
|
||||||
|
|
||||||
|
return { widget_id, kind, props };
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: WidgetRow = { rowWidgets };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType.Section) {
|
||||||
|
const { name } = layoutType.Section;
|
||||||
|
const layout = createWidgetLayout(layoutType.Section.layout);
|
||||||
|
|
||||||
const result: WidgetSection = { name, layout };
|
const result: WidgetSection = { name, layout };
|
||||||
return result;
|
return result;
|
||||||
@@ -567,14 +549,12 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
TriggerVisitLink,
|
TriggerVisitLink,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateActiveTool,
|
|
||||||
UpdateCanvasRotation,
|
|
||||||
UpdateCanvasZoom,
|
|
||||||
UpdateDialogDetails,
|
UpdateDialogDetails,
|
||||||
UpdateDocumentArtboards,
|
UpdateDocumentArtboards,
|
||||||
UpdateDocumentArtwork,
|
UpdateDocumentArtwork,
|
||||||
UpdateDocumentBarLayout,
|
UpdateDocumentBarLayout,
|
||||||
UpdateDocumentLayer,
|
UpdateToolShelfLayout,
|
||||||
|
UpdateDocumentLayerDetails,
|
||||||
UpdateDocumentOverlays,
|
UpdateDocumentOverlays,
|
||||||
UpdateDocumentRulers,
|
UpdateDocumentRulers,
|
||||||
UpdateDocumentScrollbars,
|
UpdateDocumentScrollbars,
|
||||||
@@ -584,6 +564,8 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
UpdatePropertyPanelOptionsLayout,
|
UpdatePropertyPanelOptionsLayout,
|
||||||
UpdatePropertyPanelSectionsLayout,
|
UpdatePropertyPanelSectionsLayout,
|
||||||
|
UpdateLayerTreeOptionsLayout,
|
||||||
|
UpdateDocumentModeLayout,
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
UpdateWorkingColors,
|
UpdateWorkingColors,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function preparePanicDialog(dialogState: DialogState, title: string, details: st
|
|||||||
const widgets: WidgetLayout = {
|
const widgets: WidgetLayout = {
|
||||||
layout: [
|
layout: [
|
||||||
{
|
{
|
||||||
widgets: [
|
rowWidgets: [
|
||||||
{
|
{
|
||||||
kind: "TextLabel",
|
kind: "TextLabel",
|
||||||
props: { value: title, bold: true },
|
props: { value: title, bold: true },
|
||||||
@@ -34,7 +34,7 @@ function preparePanicDialog(dialogState: DialogState, title: string, details: st
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
widgets: [
|
rowWidgets: [
|
||||||
{
|
{
|
||||||
kind: "TextLabel",
|
kind: "TextLabel",
|
||||||
props: { value: details, multiline: true },
|
props: { value: details, multiline: true },
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function initWasm(): Promise<void> {
|
|||||||
// Skip if the wasm module is already initialized
|
// Skip if the wasm module is already initialized
|
||||||
if (wasmImport !== null) return;
|
if (wasmImport !== null) return;
|
||||||
|
|
||||||
// Separating in two lines satisfies typescript when used below
|
// Separating in two lines satisfies TypeScript
|
||||||
const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
|
const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
|
||||||
wasmImport = importedWasm;
|
wasmImport = importedWasm;
|
||||||
|
|
||||||
|
|||||||
+18
-102
@@ -2,17 +2,13 @@
|
|||||||
// It serves as a thin wrapper over the editor backend API that relies
|
// It serves as a thin wrapper over the editor backend API that relies
|
||||||
// on the dispatcher messaging system and more complex Rust data types.
|
// on the dispatcher messaging system and more complex Rust data types.
|
||||||
|
|
||||||
use crate::helpers::Error;
|
use crate::helpers::{translate_key, Error};
|
||||||
use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type};
|
|
||||||
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
|
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
|
||||||
|
|
||||||
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
||||||
use editor::input::input_preprocessor::ModifierKeys;
|
use editor::input::input_preprocessor::ModifierKeys;
|
||||||
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
||||||
use editor::message_prelude::*;
|
use editor::message_prelude::*;
|
||||||
use editor::misc::EditorError;
|
|
||||||
use editor::viewport_tools::tool::ToolType;
|
|
||||||
use editor::viewport_tools::tools;
|
|
||||||
use editor::Color;
|
use editor::Color;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use editor::LayerId;
|
use editor::LayerId;
|
||||||
@@ -96,19 +92,6 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(WorkspaceMessage::NodeGraphToggleVisibility);
|
self.dispatch(WorkspaceMessage::NodeGraphToggleVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modify the currently selected tool in the document state store
|
|
||||||
pub fn select_tool(&self, tool: String) -> Result<(), JsValue> {
|
|
||||||
match translate_tool_type(&tool) {
|
|
||||||
Some(tool_type) => {
|
|
||||||
let message = ToolMessage::ActivateTool { tool_type };
|
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
None => Err(Error::new(&format!("Couldn't select {} because it was not recognized as a valid tool", tool)).into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update layout of a given UI
|
/// Update layout of a given UI
|
||||||
pub fn update_layout(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
pub fn update_layout(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||||
match (from_value(layout_target), from_value(value)) {
|
match (from_value(layout_target), from_value(value)) {
|
||||||
@@ -121,29 +104,6 @@ impl JsEditorHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a message to a given tool
|
|
||||||
pub fn send_tool_message(&self, tool: String, message: &JsValue) -> Result<(), JsValue> {
|
|
||||||
let tool_message = match translate_tool_type(&tool) {
|
|
||||||
Some(tool) => match tool {
|
|
||||||
ToolType::Select => match serde_wasm_bindgen::from_value::<tools::select_tool::SelectToolMessage>(message.clone()) {
|
|
||||||
Ok(select_message) => Ok(ToolMessage::Select(select_message)),
|
|
||||||
Err(err) => Err(Error::new(&format!("Invalid message for {}: {}", tool, err)).into()),
|
|
||||||
},
|
|
||||||
_ => Err(Error::new(&format!("Tool message sending not implemented for {}", tool)).into()),
|
|
||||||
},
|
|
||||||
None => Err(Error::new(&format!("Couldn't send message for {} because it was not recognized as a valid tool", tool)).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match tool_message {
|
|
||||||
Ok(message) => {
|
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_document(&self, document_id: u64) {
|
pub fn select_document(&self, document_id: u64) {
|
||||||
let message = PortfolioMessage::SelectDocument { document_id };
|
let message = PortfolioMessage::SelectDocument { document_id };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
@@ -442,12 +402,6 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all selected layers
|
|
||||||
pub fn delete_selected_layers(&self) {
|
|
||||||
let message = DocumentMessage::DeleteSelectedLayers;
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reorder selected layer
|
/// Reorder selected layer
|
||||||
pub fn reorder_selected_layers(&self, relative_index_offset: isize) {
|
pub fn reorder_selected_layers(&self, relative_index_offset: isize) {
|
||||||
let message = DocumentMessage::ReorderSelectedLayers { relative_index_offset };
|
let message = DocumentMessage::ReorderSelectedLayers { relative_index_offset };
|
||||||
@@ -470,25 +424,6 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the blend mode for the selected layers
|
|
||||||
pub fn set_blend_mode_for_selected_layers(&self, blend_mode_svg_style_name: String) -> Result<(), JsValue> {
|
|
||||||
if let Some(blend_mode) = translate_blend_mode(blend_mode_svg_style_name.as_str()) {
|
|
||||||
let message = DocumentMessage::SetBlendModeForSelectedLayers { blend_mode };
|
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::new(&EditorError::Misc("UnknownBlendMode".to_string()).to_string()).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the opacity for the selected layers
|
|
||||||
pub fn set_opacity_for_selected_layers(&self, opacity_percent: f64) {
|
|
||||||
let opacity = opacity_percent / 100.;
|
|
||||||
let message = DocumentMessage::SetOpacityForSelectedLayers { opacity };
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Export the document
|
/// Export the document
|
||||||
pub fn export_document(&self) {
|
pub fn export_document(&self) {
|
||||||
let message = DialogMessage::RequestExportDialog;
|
let message = DialogMessage::RequestExportDialog;
|
||||||
@@ -507,13 +442,6 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the list of selected layers. The layer paths have to be stored in one array and are separated by LayerId::MAX
|
|
||||||
pub fn select_layers(&self, paths: Vec<LayerId>) {
|
|
||||||
let replacement_selected_layers = paths.split(|id| *id == LayerId::MAX).map(|path| path.to_vec()).collect();
|
|
||||||
let message = DocumentMessage::SetSelectedLayers { replacement_selected_layers };
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends the blob url generated by js
|
/// Sends the blob url generated by js
|
||||||
pub fn set_image_blob_url(&self, path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
pub fn set_image_blob_url(&self, path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
||||||
let dimensions = (width, height);
|
let dimensions = (width, height);
|
||||||
@@ -540,29 +468,14 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames a layer from the layer list
|
// TODO: Replace with initialization system, issue #524
|
||||||
pub fn rename_layer(&self, layer_path: Vec<LayerId>, new_name: String) {
|
pub fn init_app(&self) {
|
||||||
let message = DocumentMessage::RenameLayer { layer_path, new_name };
|
let message = PortfolioMessage::UpdateDocumentWidgets;
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes a layer from the layer list
|
let message = ToolMessage::InitTools;
|
||||||
pub fn delete_layer(&self, layer_path: Vec<LayerId>) {
|
|
||||||
let message = DocumentMessage::DeleteLayer { layer_path };
|
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an empty folder at the document root
|
|
||||||
pub fn create_empty_folder(&self) {
|
|
||||||
let message = DocumentMessage::CreateEmptyFolder { container_path: vec![] };
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
|
||||||
pub fn init_document_bar(&self) {
|
|
||||||
let message = PortfolioMessage::UpdateDocumentBar;
|
|
||||||
self.dispatch(message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needed to make JsEditorHandle functions pub to rust. Do not fully
|
// Needed to make JsEditorHandle functions pub to rust. Do not fully
|
||||||
@@ -579,10 +492,11 @@ impl Drop for JsEditorHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access a handle to WASM memory
|
/// Set the random seed used by the editor by calling this from JS upon initialization.
|
||||||
|
/// This is necessary because WASM doesn't have a random number generator.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn wasm_memory() -> JsValue {
|
pub fn set_random_seed(seed: u64) {
|
||||||
wasm_bindgen::memory()
|
editor::communication::set_uuid_seed(seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intentionally panic for debugging purposes
|
/// Intentionally panic for debugging purposes
|
||||||
@@ -591,17 +505,24 @@ pub fn intentional_panic() {
|
|||||||
panic!();
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the constant FILE_SAVE_SUFFIX
|
/// Access a handle to WASM memory
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn wasm_memory() -> JsValue {
|
||||||
|
wasm_bindgen::memory()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the constant `FILE_SAVE_SUFFIX`
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn file_save_suffix() -> String {
|
pub fn file_save_suffix() -> String {
|
||||||
FILE_SAVE_SUFFIX.into()
|
FILE_SAVE_SUFFIX.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the constant FILE_SAVE_SUFFIX
|
/// Get the constant `GRAPHITE_DOCUMENT_VERSION`
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn graphite_version() -> String {
|
pub fn graphite_version() -> String {
|
||||||
GRAPHITE_DOCUMENT_VERSION.to_string()
|
GRAPHITE_DOCUMENT_VERSION.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the constant `i32::MAX`
|
/// Get the constant `i32::MAX`
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn i32_max() -> i32 {
|
pub fn i32_max() -> i32 {
|
||||||
@@ -613,8 +534,3 @@ pub fn i32_max() -> i32 {
|
|||||||
pub fn i32_min() -> i32 {
|
pub fn i32_min() -> i32 {
|
||||||
i32::MIN
|
i32::MIN
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn set_random_seed(seed: u64) {
|
|
||||||
editor::communication::set_uuid_seed(seed)
|
|
||||||
}
|
|
||||||
|
|||||||
+129
-11
@@ -1,5 +1,27 @@
|
|||||||
|
use crate::JS_EDITOR_HANDLES;
|
||||||
|
|
||||||
|
use editor::{input::keyboard::Key, message_prelude::FrontendMessage};
|
||||||
|
|
||||||
|
use std::panic;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
|
||||||
|
pub fn panic_hook(info: &panic::PanicInfo) {
|
||||||
|
let panic_info = info.to_string();
|
||||||
|
let title = "The editor crashed — sorry about that".to_string();
|
||||||
|
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.".to_string();
|
||||||
|
log::error!("{}", info);
|
||||||
|
JS_EDITOR_HANDLES.with(|instances| {
|
||||||
|
instances.borrow_mut().values_mut().for_each(|instance| {
|
||||||
|
instance.handle_response_rust_proxy(FrontendMessage::DisplayDialogPanic {
|
||||||
|
panic_info: panic_info.clone(),
|
||||||
|
title: title.clone(),
|
||||||
|
description: description.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The JavaScript `Error` type
|
/// The JavaScript `Error` type
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -10,15 +32,111 @@ extern "C" {
|
|||||||
pub fn new(msg: &str) -> Error;
|
pub fn new(msg: &str) -> Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a string and matches it to its equivalently-named enum variant (useful for simple type translations)
|
/// Logging to the JS console
|
||||||
macro_rules! match_string_to_enum {
|
#[wasm_bindgen]
|
||||||
(match ($e:expr) {$($var:ident),* $(,)?}) => {
|
extern "C" {
|
||||||
match $e {
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
$(
|
fn log(msg: &str, format: &str);
|
||||||
stringify!($var) => Some($var),
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
)*
|
fn info(msg: &str, format: &str);
|
||||||
_ => None
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
}
|
fn warn(msg: &str, format: &str);
|
||||||
};
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
|
fn error(msg: &str, format: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WasmLog;
|
||||||
|
|
||||||
|
impl log::Log for WasmLog {
|
||||||
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
|
metadata.level() <= log::Level::Info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &log::Record) {
|
||||||
|
let (log, name, color): (fn(&str, &str), &str, &str) = match record.level() {
|
||||||
|
log::Level::Trace => (log, "trace", "color:plum"),
|
||||||
|
log::Level::Debug => (log, "debug", "color:cyan"),
|
||||||
|
log::Level::Warn => (warn, "warn", "color:goldenrod"),
|
||||||
|
log::Level::Info => (info, "info", "color:mediumseagreen"),
|
||||||
|
log::Level::Error => (error, "error", "color:red"),
|
||||||
|
};
|
||||||
|
let msg = &format!("%c{}\t{}", name, record.args());
|
||||||
|
log(msg, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate a keyboard key from its JS name to its Rust `Key` enum
|
||||||
|
pub fn translate_key(name: &str) -> Key {
|
||||||
|
use Key::*;
|
||||||
|
|
||||||
|
log::trace!("Key event received: {}", name);
|
||||||
|
|
||||||
|
match name.to_lowercase().as_str() {
|
||||||
|
"a" => KeyA,
|
||||||
|
"b" => KeyB,
|
||||||
|
"c" => KeyC,
|
||||||
|
"d" => KeyD,
|
||||||
|
"e" => KeyE,
|
||||||
|
"f" => KeyF,
|
||||||
|
"g" => KeyG,
|
||||||
|
"h" => KeyH,
|
||||||
|
"i" => KeyI,
|
||||||
|
"j" => KeyJ,
|
||||||
|
"k" => KeyK,
|
||||||
|
"l" => KeyL,
|
||||||
|
"m" => KeyM,
|
||||||
|
"n" => KeyN,
|
||||||
|
"o" => KeyO,
|
||||||
|
"p" => KeyP,
|
||||||
|
"q" => KeyQ,
|
||||||
|
"r" => KeyR,
|
||||||
|
"s" => KeyS,
|
||||||
|
"t" => KeyT,
|
||||||
|
"u" => KeyU,
|
||||||
|
"v" => KeyV,
|
||||||
|
"w" => KeyW,
|
||||||
|
"x" => KeyX,
|
||||||
|
"y" => KeyY,
|
||||||
|
"z" => KeyZ,
|
||||||
|
"0" => Key0,
|
||||||
|
"1" => Key1,
|
||||||
|
"2" => Key2,
|
||||||
|
"3" => Key3,
|
||||||
|
"4" => Key4,
|
||||||
|
"5" => Key5,
|
||||||
|
"6" => Key6,
|
||||||
|
"7" => Key7,
|
||||||
|
"8" => Key8,
|
||||||
|
"9" => Key9,
|
||||||
|
"enter" => KeyEnter,
|
||||||
|
"=" => KeyEquals,
|
||||||
|
"+" => KeyPlus,
|
||||||
|
"-" => KeyMinus,
|
||||||
|
"shift" => KeyShift,
|
||||||
|
// When using linux + chrome + the neo keyboard layout, the shift key is recognized as caps
|
||||||
|
"capslock" => KeyShift,
|
||||||
|
" " => KeySpace,
|
||||||
|
"control" => KeyControl,
|
||||||
|
"delete" => KeyDelete,
|
||||||
|
"backspace" => KeyBackspace,
|
||||||
|
"alt" => KeyAlt,
|
||||||
|
"escape" => KeyEscape,
|
||||||
|
"tab" => KeyTab,
|
||||||
|
"arrowup" => KeyArrowUp,
|
||||||
|
"arrowdown" => KeyArrowDown,
|
||||||
|
"arrowleft" => KeyArrowLeft,
|
||||||
|
"arrowright" => KeyArrowRight,
|
||||||
|
"[" => KeyLeftBracket,
|
||||||
|
"]" => KeyRightBracket,
|
||||||
|
"{" => KeyLeftCurlyBracket,
|
||||||
|
"}" => KeyRightCurlyBracket,
|
||||||
|
"pageup" => KeyPageUp,
|
||||||
|
"pagedown" => KeyPageDown,
|
||||||
|
"," => KeyComma,
|
||||||
|
"." => KeyPeriod,
|
||||||
|
_ => UnknownKey,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub(crate) use match_string_to_enum;
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod logging;
|
|
||||||
pub mod type_translators;
|
|
||||||
|
|
||||||
use editor::message_prelude::*;
|
use helpers::{panic_hook, WasmLog};
|
||||||
|
|
||||||
use logging::WasmLog;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
@@ -20,7 +16,7 @@ thread_local! {
|
|||||||
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, api::JsEditorHandle>> = RefCell::new(HashMap::new());
|
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, api::JsEditorHandle>> = RefCell::new(HashMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the backend
|
/// Initialize the backend
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
panic::set_hook(Box::new(panic_hook));
|
panic::set_hook(Box::new(panic_hook));
|
||||||
@@ -28,20 +24,3 @@ pub fn init() {
|
|||||||
log::set_logger(&LOGGER).expect("Failed to set logger");
|
log::set_logger(&LOGGER).expect("Failed to set logger");
|
||||||
log::set_max_level(log::LevelFilter::Debug);
|
log::set_max_level(log::LevelFilter::Debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a panic occurs, close up shop before the backend dies
|
|
||||||
fn panic_hook(info: &panic::PanicInfo) {
|
|
||||||
let panic_info = info.to_string();
|
|
||||||
let title = "The editor crashed — sorry about that".to_string();
|
|
||||||
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.".to_string();
|
|
||||||
log::error!("{}", info);
|
|
||||||
JS_EDITOR_HANDLES.with(|instances| {
|
|
||||||
instances.borrow_mut().values_mut().for_each(|instance| {
|
|
||||||
instance.handle_response_rust_proxy(FrontendMessage::DisplayDialogPanic {
|
|
||||||
panic_info: panic_info.clone(),
|
|
||||||
title: title.clone(),
|
|
||||||
description: description.clone(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
extern "C" {
|
|
||||||
#[wasm_bindgen(js_namespace = console)]
|
|
||||||
fn log(msg: &str, format: &str);
|
|
||||||
#[wasm_bindgen(js_namespace = console)]
|
|
||||||
fn info(msg: &str, format: &str);
|
|
||||||
#[wasm_bindgen(js_namespace = console)]
|
|
||||||
fn warn(msg: &str, format: &str);
|
|
||||||
#[wasm_bindgen(js_namespace = console)]
|
|
||||||
fn error(msg: &str, format: &str);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct WasmLog;
|
|
||||||
|
|
||||||
impl log::Log for WasmLog {
|
|
||||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
|
||||||
metadata.level() <= log::Level::Info
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(&self, record: &log::Record) {
|
|
||||||
let (log, name, color): (fn(&str, &str), &str, &str) = match record.level() {
|
|
||||||
log::Level::Trace => (log, "trace", "color:plum"),
|
|
||||||
log::Level::Debug => (log, "debug", "color:cyan"),
|
|
||||||
log::Level::Warn => (warn, "warn", "color:goldenrod"),
|
|
||||||
log::Level::Info => (info, "info", "color:mediumseagreen"),
|
|
||||||
log::Level::Error => (error, "error", "color:red"),
|
|
||||||
};
|
|
||||||
let msg = &format!("%c{}\t{}", name, record.args());
|
|
||||||
log(msg, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use crate::helpers::match_string_to_enum;
|
|
||||||
|
|
||||||
use editor::input::keyboard::Key;
|
|
||||||
use editor::viewport_tools::tool::ToolType;
|
|
||||||
use graphene::layers::blend_mode::BlendMode;
|
|
||||||
|
|
||||||
pub fn translate_tool_type(name: &str) -> Option<ToolType> {
|
|
||||||
use ToolType::*;
|
|
||||||
|
|
||||||
match_string_to_enum!(match (name) {
|
|
||||||
Select,
|
|
||||||
Artboard,
|
|
||||||
Navigate,
|
|
||||||
Eyedropper,
|
|
||||||
Text,
|
|
||||||
Fill,
|
|
||||||
Gradient,
|
|
||||||
Brush,
|
|
||||||
Heal,
|
|
||||||
Clone,
|
|
||||||
Patch,
|
|
||||||
Detail,
|
|
||||||
Relight,
|
|
||||||
Path,
|
|
||||||
Pen,
|
|
||||||
Freehand,
|
|
||||||
Spline,
|
|
||||||
Line,
|
|
||||||
Rectangle,
|
|
||||||
Ellipse,
|
|
||||||
Shape
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate_blend_mode(blend_mode_svg_style_name: &str) -> Option<BlendMode> {
|
|
||||||
use BlendMode::*;
|
|
||||||
|
|
||||||
let blend_mode = match blend_mode_svg_style_name {
|
|
||||||
"Normal" => Normal,
|
|
||||||
"Multiply" => Multiply,
|
|
||||||
"Darken" => Darken,
|
|
||||||
"ColorBurn" => ColorBurn,
|
|
||||||
"Screen" => Screen,
|
|
||||||
"Lighten" => Lighten,
|
|
||||||
"ColorDodge" => ColorDodge,
|
|
||||||
"Overlay" => Overlay,
|
|
||||||
"SoftLight" => SoftLight,
|
|
||||||
"HardLight" => HardLight,
|
|
||||||
"Difference" => Difference,
|
|
||||||
"Exclusion" => Exclusion,
|
|
||||||
"Hue" => Hue,
|
|
||||||
"Saturation" => Saturation,
|
|
||||||
"Color" => Color,
|
|
||||||
"Luminosity" => Luminosity,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(blend_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate_key(name: &str) -> Key {
|
|
||||||
use Key::*;
|
|
||||||
|
|
||||||
log::trace!("Key event received: {}", name);
|
|
||||||
|
|
||||||
match name.to_lowercase().as_str() {
|
|
||||||
"a" => KeyA,
|
|
||||||
"b" => KeyB,
|
|
||||||
"c" => KeyC,
|
|
||||||
"d" => KeyD,
|
|
||||||
"e" => KeyE,
|
|
||||||
"f" => KeyF,
|
|
||||||
"g" => KeyG,
|
|
||||||
"h" => KeyH,
|
|
||||||
"i" => KeyI,
|
|
||||||
"j" => KeyJ,
|
|
||||||
"k" => KeyK,
|
|
||||||
"l" => KeyL,
|
|
||||||
"m" => KeyM,
|
|
||||||
"n" => KeyN,
|
|
||||||
"o" => KeyO,
|
|
||||||
"p" => KeyP,
|
|
||||||
"q" => KeyQ,
|
|
||||||
"r" => KeyR,
|
|
||||||
"s" => KeyS,
|
|
||||||
"t" => KeyT,
|
|
||||||
"u" => KeyU,
|
|
||||||
"v" => KeyV,
|
|
||||||
"w" => KeyW,
|
|
||||||
"x" => KeyX,
|
|
||||||
"y" => KeyY,
|
|
||||||
"z" => KeyZ,
|
|
||||||
"0" => Key0,
|
|
||||||
"1" => Key1,
|
|
||||||
"2" => Key2,
|
|
||||||
"3" => Key3,
|
|
||||||
"4" => Key4,
|
|
||||||
"5" => Key5,
|
|
||||||
"6" => Key6,
|
|
||||||
"7" => Key7,
|
|
||||||
"8" => Key8,
|
|
||||||
"9" => Key9,
|
|
||||||
"enter" => KeyEnter,
|
|
||||||
"=" => KeyEquals,
|
|
||||||
"+" => KeyPlus,
|
|
||||||
"-" => KeyMinus,
|
|
||||||
"shift" => KeyShift,
|
|
||||||
// When using linux + chrome + the neo keyboard layout, the shift key is recognized as caps
|
|
||||||
"capslock" => KeyShift,
|
|
||||||
" " => KeySpace,
|
|
||||||
"control" => KeyControl,
|
|
||||||
"delete" => KeyDelete,
|
|
||||||
"backspace" => KeyBackspace,
|
|
||||||
"alt" => KeyAlt,
|
|
||||||
"escape" => KeyEscape,
|
|
||||||
"tab" => KeyTab,
|
|
||||||
"arrowup" => KeyArrowUp,
|
|
||||||
"arrowdown" => KeyArrowDown,
|
|
||||||
"arrowleft" => KeyArrowLeft,
|
|
||||||
"arrowright" => KeyArrowRight,
|
|
||||||
"[" => KeyLeftBracket,
|
|
||||||
"]" => KeyRightBracket,
|
|
||||||
"{" => KeyLeftCurlyBracket,
|
|
||||||
"}" => KeyRightCurlyBracket,
|
|
||||||
"pageup" => KeyPageUp,
|
|
||||||
"pagedown" => KeyPageDown,
|
|
||||||
"," => KeyComma,
|
|
||||||
"." => KeyPeriod,
|
|
||||||
_ => UnknownKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,73 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// Describes how overlapping SVG elements should be blended together.
|
/// Describes how overlapping SVG elements should be blended together.
|
||||||
/// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples.
|
/// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples.
|
||||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum BlendMode {
|
pub enum BlendMode {
|
||||||
|
// Basic group
|
||||||
Normal,
|
Normal,
|
||||||
|
// Not supported by SVG, but we should someday support: Dissolve
|
||||||
|
|
||||||
|
// Darken group
|
||||||
Multiply,
|
Multiply,
|
||||||
Darken,
|
Darken,
|
||||||
ColorBurn,
|
ColorBurn,
|
||||||
|
// Not supported by SVG, but we should someday support: Linear Burn, Darker Color
|
||||||
|
|
||||||
|
// Lighten group
|
||||||
Screen,
|
Screen,
|
||||||
Lighten,
|
Lighten,
|
||||||
ColorDodge,
|
ColorDodge,
|
||||||
|
// Not supported by SVG, but we should someday support: Linear Dodge (Add), Lighter Color
|
||||||
|
|
||||||
|
// Contrast group
|
||||||
Overlay,
|
Overlay,
|
||||||
SoftLight,
|
SoftLight,
|
||||||
HardLight,
|
HardLight,
|
||||||
|
// Not supported by SVG, but we should someday support: Vivid Light, Linear Light, Pin Light, Hard Mix
|
||||||
|
|
||||||
|
// Inversion group
|
||||||
Difference,
|
Difference,
|
||||||
Exclusion,
|
Exclusion,
|
||||||
|
// Not supported by SVG, but we should someday support: Subtract, Divide
|
||||||
|
|
||||||
|
// Component group
|
||||||
Hue,
|
Hue,
|
||||||
Saturation,
|
Saturation,
|
||||||
Color,
|
Color,
|
||||||
Luminosity,
|
Luminosity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BlendMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
BlendMode::Normal => "Normal".to_string(),
|
||||||
|
|
||||||
|
BlendMode::Multiply => "Multiply".to_string(),
|
||||||
|
BlendMode::Darken => "Darken".to_string(),
|
||||||
|
BlendMode::ColorBurn => "Color Burn".to_string(),
|
||||||
|
|
||||||
|
BlendMode::Screen => "Screen".to_string(),
|
||||||
|
BlendMode::Lighten => "Lighten".to_string(),
|
||||||
|
BlendMode::ColorDodge => "Color Dodge".to_string(),
|
||||||
|
|
||||||
|
BlendMode::Overlay => "Overlay".to_string(),
|
||||||
|
BlendMode::SoftLight => "Soft Light".to_string(),
|
||||||
|
BlendMode::HardLight => "Hard Light".to_string(),
|
||||||
|
|
||||||
|
BlendMode::Difference => "Difference".to_string(),
|
||||||
|
BlendMode::Exclusion => "Exclusion".to_string(),
|
||||||
|
|
||||||
|
BlendMode::Hue => "Hue".to_string(),
|
||||||
|
BlendMode::Saturation => "Saturation".to_string(),
|
||||||
|
BlendMode::Color => "Color".to_string(),
|
||||||
|
BlendMode::Luminosity => "Luminosity".to_string(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BlendMode {
|
impl BlendMode {
|
||||||
/// Convert the enum to the CSS string for the blend mode.
|
/// Convert the enum to the CSS string for the blend mode.
|
||||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||||
@@ -45,4 +91,16 @@ impl BlendMode {
|
|||||||
BlendMode::Luminosity => "luminosity",
|
BlendMode::Luminosity => "luminosity",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List of all the blend modes in their conventional ordering and grouping.
|
||||||
|
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
|
||||||
|
[
|
||||||
|
&[BlendMode::Normal],
|
||||||
|
&[BlendMode::Multiply, BlendMode::Darken, BlendMode::ColorBurn],
|
||||||
|
&[BlendMode::Screen, BlendMode::Lighten, BlendMode::ColorDodge],
|
||||||
|
&[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight],
|
||||||
|
&[BlendMode::Difference, BlendMode::Exclusion],
|
||||||
|
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user