diff --git a/.vscode/settings.json b/.vscode/settings.json index 88480d0b6..dc3effd8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "[rust]": { "editor.formatOnSave": true, "editor.formatOnPaste": true, - "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.defaultFormatter": "matklad.rust-analyzer", }, // Web: save on format "[typescript][javascript][vue]": { diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 185ebf488..e2f2af126 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -41,7 +41,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ ArtboardMessageDiscriminant::RenderArtboards, ))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), - MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer), + MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerDetails), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList), MessageDiscriminant::Tool(ToolMessageDiscriminant::DocumentIsDirty), @@ -142,12 +142,7 @@ impl Dispatcher { fn log_message(&self, message: &Message) { use Message::*; - if log::max_level() == log::LevelFilter::Trace - && !(matches!( - message, - InputPreprocessor(_) | Frontend(FrontendMessage::UpdateCanvasZoom { .. }) | Frontend(FrontendMessage::UpdateCanvasRotation { .. }) - ) || MessageDiscriminant::from(message).local_name().ends_with("PointerMove")) - { + if log::max_level() == log::LevelFilter::Trace && !(matches!(message, InputPreprocessor(_)) || MessageDiscriminant::from(message).local_name().ends_with("PointerMove")) { log::trace!("Message: {:?}", message); // log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions())); } @@ -447,6 +442,8 @@ mod test { #[test] fn check_if_graphite_file_version_upgrade_is_needed() { + use crate::layout::widgets::{LayoutRow, TextLabel, Widget}; + init_logger(); set_uuid_seed(0); let mut editor = Editor::new(); @@ -458,8 +455,8 @@ mod test { for response in responses { if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response { - if let crate::layout::widgets::LayoutRow::Row { widgets } = &layout[0] { - if let crate::layout::widgets::Widget::TextLabel(crate::layout::widgets::TextLabel { value, .. }) = &widgets[0].widget { + if let LayoutRow::Row { widgets } = &layout[0] { + if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget { println!(); println!("-------------------------------------------------"); println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!"); diff --git a/editor/src/communication/graphite-test-document.graphite b/editor/src/communication/graphite-test-document.graphite index e0c9f6d9c..b4fb917d4 100644 --- a/editor/src/communication/graphite-test-document.graphite +++ b/editor/src/communication/graphite-test-document.graphite @@ -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}} \ No newline at end of file diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 0aa201fee..b04c69ad8 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -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.); // 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; diff --git a/editor/src/dialog/dialogs/export_dialog.rs b/editor/src/dialog/dialogs/export_dialog.rs index 776b1f54e..1a4e50582 100644 --- a/editor/src/dialog/dialogs/export_dialog.rs +++ b/editor/src/dialog/dialogs/export_dialog.rs @@ -64,7 +64,7 @@ impl PropertyHolder for Export { let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())]; export_area_options.extend(artboards); 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() .map(|(val, name)| DropdownEntryData { label: name, @@ -84,8 +84,8 @@ impl PropertyHolder for Export { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::DropdownInput(DropdownInput { - selected_index: index as u32, - menu_entries, + selected_index: Some(index as u32), + entries, ..Default::default() })), ]; @@ -101,19 +101,19 @@ impl PropertyHolder for Export { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: self.scale_factor, + value: Some(self.scale_factor), label: "".into(), unit: " ".into(), - disabled: self.file_type == FileType::Svg, 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() })), ]; let button_widgets = vec![ WidgetHolder::new(Widget::TextButton(TextButton { - label: "OK".to_string(), + label: "Export".to_string(), min_width: 96, emphasized: true, on_update: WidgetCallback::new(|_| { diff --git a/editor/src/dialog/dialogs/new_document_dialog.rs b/editor/src/dialog/dialogs/new_document_dialog.rs index 3353291d8..985c6deb5 100644 --- a/editor/src/dialog/dialogs/new_document_dialog.rs +++ b/editor/src/dialog/dialogs/new_document_dialog.rs @@ -66,13 +66,13 @@ impl PropertyHolder for NewDocument { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: self.dimensions.x as f64, + value: Some(self.dimensions.x as f64), label: "W".into(), unit: " px".into(), disabled: self.infinite, is_integer: true, 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() })), WidgetHolder::new(Widget::Separator(Separator { @@ -80,13 +80,13 @@ impl PropertyHolder for NewDocument { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: self.dimensions.y as f64, + value: Some(self.dimensions.y as f64), label: "H".into(), unit: " px".into(), disabled: self.infinite, is_integer: true, 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() })), ]; diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 0d21bfb24..3501a9ffd 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,16 +1,17 @@ use super::clipboards::Clipboard; use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData; -use super::utility_types::TargetDocument; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; +use super::utility_types::{DocumentMode, TargetDocument}; use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler}; 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::frontend::utility_types::{FileType, FrontendImageData}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::{ - IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget, - WidgetCallback, WidgetHolder, WidgetLayout, + DropdownEntryData, DropdownInput, IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, + SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout, }; use crate::message_prelude::*; use crate::viewport_tools::vector_editor::vector_shape::VectorShape; @@ -18,6 +19,7 @@ use crate::EditorError; use graphene::color::Color; use graphene::document::Document as GrapheneDocument; +use graphene::layers::blend_mode::BlendMode; use graphene::layers::folder_layer::FolderLayer; use graphene::layers::layer_info::LayerDataType; use graphene::layers::style::{Fill, ViewMode}; @@ -32,15 +34,24 @@ use std::collections::VecDeque; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocumentMessageHandler { 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)] pub document_undo_history: Vec, #[serde(skip)] pub document_redo_history: Vec, - pub saved_document_identifier: u64, - pub name: String, + #[serde(with = "vectorize_layer_metadata")] pub layer_metadata: HashMap, LayerMetadata>, layer_range_selection_reference: Vec, + movement_handler: MovementMessageHandler, #[serde(skip)] overlays_message_handler: OverlaysMessageHandler, @@ -48,31 +59,32 @@ pub struct DocumentMessageHandler { #[serde(skip)] transform_layer_handler: TransformLayerMessageHandler, properties_panel_message_handler: PropertiesPanelMessageHandler, - pub overlays_visible: bool, - pub snapping_enabled: bool, - pub view_mode: ViewMode, - pub version: String, } impl Default for DocumentMessageHandler { fn default() -> Self { Self { graphene_document: GrapheneDocument::default(), - document_undo_history: Vec::new(), - document_redo_history: Vec::new(), saved_document_identifier: 0, 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_range_selection_reference: Vec::new(), + movement_handler: MovementMessageHandler::default(), overlays_message_handler: OverlaysMessageHandler::default(), artboard_message_handler: ArtboardMessageHandler::default(), transform_layer_handler: TransformLayerMessageHandler::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) { layer.selected = true; 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 { log::warn!("Tried to select non existing layer {:?}", path); None @@ -516,11 +528,9 @@ impl DocumentMessageHandler { responses.push_back(FrontendMessage::TriggerFontLoadDefault.into()) } } -} -impl PropertyHolder for DocumentMessageHandler { - fn properties(&self) -> WidgetLayout { - WidgetLayout::new(vec![LayoutRow::Row { + pub fn update_document_widgets(&self, responses: &mut VecDeque) { + let document_bar_layout = WidgetLayout::new(vec![LayoutRow::Row { widgets: vec![ WidgetHolder::new(Widget::OptionalInput(OptionalInput { checked: self.snapping_enabled, @@ -600,11 +610,11 @@ impl PropertyHolder for DocumentMessageHandler { })), WidgetHolder::new(Widget::NumberInput(NumberInput { 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., on_update: WidgetCallback::new(|number_input: &NumberInput| { MovementMessage::SetCanvasRotation { - angle_radians: number_input.value * (std::f64::consts::PI / 180.), + angle_radians: number_input.value.unwrap() * (std::f64::consts::PI / 180.), } .into() }), @@ -641,12 +651,12 @@ impl PropertyHolder for DocumentMessageHandler { })), WidgetHolder::new(Widget::NumberInput(NumberInput { unit: "%".into(), - value: self.movement_handler.zoom * 100., + value: Some(self.movement_handler.zoom * 100.), min: Some(0.000001), max: Some(1000000.), on_update: WidgetCallback::new(|number_input: &NumberInput| { MovementMessage::SetCanvasZoom { - zoom_factor: number_input.value / 100., + zoom_factor: number_input.value.unwrap() / 100., } .into() }), @@ -656,7 +666,163 @@ impl PropertyHolder for DocumentMessageHandler { ..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) { + 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 for Docum // TODO: Correctly update layer panel in clear_selection instead of here responses.push_back(FolderChanged { affected_folder_path: vec![] }.into()); responses.push_back(DocumentMessage::SelectionChanged.into()); + + self.update_layer_tree_options_bar_widgets(responses); } AlignSelectedLayers { axis, aggregate } => { self.backup(responses); @@ -796,7 +964,7 @@ impl MessageHandler for Docum } } BooleanOperation(op) => { - // convert Vec<&[LayerId]> to Vec> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum + // Convert Vec<&[LayerId]> to Vec> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum responses.push_back(StartTransaction.into()); responses.push_back( DocumentOperation::BooleanOperation { @@ -974,9 +1142,10 @@ impl MessageHandler for Docum } LayerChanged { affected_layer_path } => { 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()); + self.update_layer_tree_options_bar_widgets(responses); } LoadFont { font } => { if !self.graphene_document.font_cache.loaded_font(&font) { diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index 6c4eac344..81075a700 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -1,5 +1,4 @@ use graphene::document::FontCache; -use graphene::layers::blend_mode::BlendMode; use graphene::layers::layer_info::{Layer, LayerData, LayerDataType}; use graphene::layers::style::ViewMode; use graphene::LayerId; @@ -48,8 +47,6 @@ pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, la LayerPanelEntry { name, visible: layer.visible, - blend_mode: layer.blend_mode, - opacity: layer.opacity, layer_type: (&layer.data).into(), layer_metadata: *layer_metadata, path, @@ -88,8 +85,6 @@ impl Serialize for RawBuffer { pub struct LayerPanelEntry { pub name: String, pub visible: bool, - pub blend_mode: BlendMode, - pub opacity: f64, pub layer_type: LayerDataTypeDiscriminant, pub layer_metadata: LayerMetadata, pub path: Vec, diff --git a/editor/src/document/movement_message_handler.rs b/editor/src/document/movement_message_handler.rs index e3f34c417..bf60e46a7 100644 --- a/editor/src/document/movement_message_handler.rs +++ b/editor/src/document/movement_message_handler.rs @@ -153,10 +153,9 @@ impl MessageHandler { @@ -245,15 +244,13 @@ impl MessageHandler { self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); - responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - responses.push_back(PortfolioMessage::UpdateDocumentBar.into()); + responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } TransformCanvasEnd => { diff --git a/editor/src/document/portfolio_message.rs b/editor/src/document/portfolio_message.rs index d196d2a2b..9312b8266 100644 --- a/editor/src/document/portfolio_message.rs +++ b/editor/src/document/portfolio_message.rs @@ -67,6 +67,6 @@ pub enum PortfolioMessage { SetActiveDocument { document_id: u64, }, - UpdateDocumentBar, + UpdateDocumentWidgets, UpdateOpenDocumentsList, } diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index c5579692a..3b42db133 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -73,9 +73,10 @@ impl PortfolioMessageHandler { .layer_metadata .keys() .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::>(), ); + 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_default_font(responses); @@ -432,14 +433,14 @@ impl MessageHandler for Port responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into()); } responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(PortfolioMessage::UpdateDocumentBar.into()); + responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); } SetActiveDocument { document_id } => { self.active_document_id = document_id; } - UpdateDocumentBar => { + UpdateDocumentWidgets => { let active_document = self.active_document(); - active_document.register_properties(responses, LayoutTarget::DocumentBar) + active_document.update_document_widgets(responses); } UpdateOpenDocumentsList => { // Send the list of document tab names diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index 1ac21986e..aaa84b7fc 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -137,14 +137,14 @@ impl<'a> MessageHandler MessageHandler LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.transform.x(), + value: Some(layer.transform.x()), label: "X".into(), unit: " px".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::X, } .into() @@ -549,12 +549,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.transform.y(), + value: Some(layer.transform.y()), label: "Y".into(), unit: " px".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::Y, } .into() @@ -574,12 +574,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.transform.rotation() * 180. / PI, + value: Some(layer.transform.rotation() * 180. / PI), label: "".into(), unit: "°".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value / 180. * PI, + value: number_input.value.unwrap() / 180. * PI, transform_op: TransformOp::Rotation, } .into() @@ -599,12 +599,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.transform.scale_x(), + value: Some(layer.transform.scale_x()), label: "X".into(), unit: "".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::ScaleX, } .into() @@ -616,12 +616,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.transform.scale_y(), + value: Some(layer.transform.scale_y()), label: "Y".into(), unit: "".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::ScaleY, } .into() @@ -641,12 +641,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), 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(), unit: " px".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::Width, } .into() @@ -658,12 +658,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), 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(), unit: " px".into(), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { - value: number_input.value, + value: number_input.value.unwrap(), transform_op: TransformOp::Height, } .into() @@ -765,7 +765,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: layer.size, + value: Some(layer.size), min: Some(1.), unit: " px".into(), 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_style: font_style.clone(), font_file: font_file.clone(), - size: number_input.value, + size: number_input.value.unwrap(), } .into() }), @@ -954,13 +954,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: stroke.weight() as f64, + value: Some(stroke.weight() as f64), is_integer: false, min: Some(0.), unit: " px".into(), on_update: WidgetCallback::new(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke2.clone().with_weight(number_input.value), + stroke: internal_stroke2.clone().with_weight(number_input.value.unwrap()), } .into() }), @@ -1000,13 +1000,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: stroke.dash_offset() as f64, + value: Some(stroke.dash_offset() as f64), is_integer: true, min: Some(0.), unit: " px".into(), on_update: WidgetCallback::new(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke4.clone().with_dash_offset(number_input.value), + stroke: internal_stroke4.clone().with_dash_offset(number_input.value.unwrap()), } .into() }), @@ -1120,13 +1120,13 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { direction: SeparatorDirection::Horizontal, })), 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, min: Some(0.), unit: "".into(), on_update: WidgetCallback::new(move |number_input: &NumberInput| { 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() }), diff --git a/editor/src/document/utility_types.rs b/editor/src/document/utility_types.rs index c9d9991e2..70fb43583 100644 --- a/editor/src/document/utility_types.rs +++ b/editor/src/document/utility_types.rs @@ -4,6 +4,7 @@ use graphene::LayerId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; pub type DocumentSave = (GrapheneDocument, HashMap, LayerMetadata>); @@ -32,3 +33,31 @@ pub enum TargetDocument { Artboard, 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(), + } + } +} diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index b50502e46..8fea2ff73 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -34,25 +34,25 @@ pub enum FrontendMessage { // Update prefix: give the frontend a new value or state for it to use UpdateActiveDocument { document_id: u64 }, - UpdateActiveTool { tool_name: String }, - UpdateCanvasRotation { angle_radians: f64 }, - UpdateCanvasZoom { factor: f64 }, UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout }, UpdateDocumentArtboards { svg: String }, UpdateDocumentArtwork { svg: String }, UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout }, - UpdateDocumentLayer { data: LayerPanelEntry }, + UpdateDocumentLayerDetails { data: LayerPanelEntry }, UpdateDocumentLayerTreeStructure { data_buffer: RawBuffer }, + UpdateDocumentModeLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateDocumentOverlays { svg: String }, UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 }, UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) }, UpdateImageData { image_data: Vec }, UpdateInputHints { hint_data: HintData }, + UpdateLayerTreeOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateMouseCursor { cursor: MouseCursorIcon }, UpdateNodeGraphVisibility { visible: bool }, UpdateOpenDocumentsList { open_documents: Vec }, UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, + UpdateToolShelfLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateWorkingColors { primary: Color, secondary: Color }, } diff --git a/editor/src/layout/layout_message.rs b/editor/src/layout/layout_message.rs index 9489d1e3a..e465ec523 100644 --- a/editor/src/layout/layout_message.rs +++ b/editor/src/layout/layout_message.rs @@ -17,9 +17,12 @@ pub enum LayoutMessage { pub enum LayoutTarget { DialogDetails, DocumentBar, - PropertiesOptionsPanel, - PropertiesSectionsPanel, + DocumentMode, + LayerTreeOptions, + PropertiesOptions, + PropertiesSections, ToolOptions, + ToolShelf, // KEEP THIS ENUM LAST // This is a marker that is used to define an array that is used to hold widgets diff --git a/editor/src/layout/layout_message_handler.rs b/editor/src/layout/layout_message_handler.rs index 6fe6e0f76..9d46bb58f 100644 --- a/editor/src/layout/layout_message_handler.rs +++ b/editor/src/layout/layout_message_handler.rs @@ -12,30 +12,45 @@ pub struct LayoutMessageHandler { } impl LayoutMessageHandler { + #[remain::check] fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque) { let widget_layout = &self.layouts[layout_target as usize]; + #[remain::sorted] let message = match layout_target { LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { layout_target, layout: widget_layout.layout.clone(), }, - LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { - layout_target, - layout: widget_layout.layout.clone(), - }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, layout: widget_layout.layout.clone(), }, - LayoutTarget::PropertiesOptionsPanel => FrontendMessage::UpdatePropertyPanelOptionsLayout { + LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, 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: widget_layout.layout.clone(), }, + #[remain::unsorted] LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), }; responses.push_back(message.into()); @@ -72,8 +87,8 @@ impl MessageHandler for LayoutMessageHandler { } Widget::DropdownInput(dropdown_input) => { let update_value = value.as_u64().expect("DropdownInput update was not of type: u64"); - dropdown_input.selected_index = update_value as u32; - let callback_message = (dropdown_input.menu_entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&()); + dropdown_input.selected_index = Some(update_value as u32); + let callback_message = (dropdown_input.entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&()); responses.push_back(callback_message); } Widget::FontInput(font_input) => { @@ -102,7 +117,7 @@ impl MessageHandler for LayoutMessageHandler { Widget::NumberInput(number_input) => match value { Value::Number(num) => { 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); responses.push_back(callback_message); } diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index 3f28cd6e6..e83fe2a23 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -52,8 +52,18 @@ pub type SubLayout = Vec; #[remain::sorted] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum LayoutRow { - Row { widgets: Vec }, - Section { name: String, layout: SubLayout }, + Column { + #[serde(rename = "columnWidgets")] + widgets: Vec, + }, + Row { + #[serde(rename = "rowWidgets")] + widgets: Vec, + }, + Section { + name: String, + layout: SubLayout, + }, } #[derive(Debug, Default)] @@ -72,6 +82,10 @@ impl<'a> Iterator for WidgetIter<'a> { } match self.stack.pop() { + Some(LayoutRow::Column { widgets }) => { + self.current_slice = Some(widgets); + self.next() + } Some(LayoutRow::Row { widgets }) => { self.current_slice = Some(widgets); self.next() @@ -103,6 +117,10 @@ impl<'a> Iterator for WidgetIterMut<'a> { }; match self.stack.pop() { + Some(LayoutRow::Column { widgets }) => { + self.current_slice = Some(widgets); + self.next() + } Some(LayoutRow::Row { widgets }) => { self.current_slice = Some(widgets); self.next() @@ -170,7 +188,7 @@ pub enum Widget { #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { - pub value: f64, + pub value: Option, #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback, @@ -285,6 +303,7 @@ pub struct IconButton { #[serde(rename = "title")] pub tooltip: String, pub size: u32, + pub active: bool, #[serde(rename = "gapAfter")] pub gap_after: bool, #[serde(skip)] @@ -298,12 +317,12 @@ pub struct IconButton { pub struct TextButton { pub label: String, pub emphasized: bool, - pub disabled: bool, pub min_width: u32, pub gap_after: bool, #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback, + pub disabled: bool, } #[derive(Clone, Serialize, Deserialize, Derivative, Default)] @@ -342,16 +361,14 @@ pub struct PopoverButton { #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] pub struct DropdownInput { - #[serde(rename = "menuEntries")] - pub menu_entries: Vec>, - - // 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 + pub entries: Vec>, + // 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) #[serde(rename = "selectedIndex")] - pub selected_index: u32, - + pub selected_index: Option, #[serde(rename = "drawIcon")] pub draw_icon: bool, + // `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput` + pub disabled: bool, } #[derive(Clone, Serialize, Deserialize, Derivative, Default)] diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 4557e43e4..5bbcdd7a0 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -2,7 +2,7 @@ use super::tools::*; use crate::communication::message_handler::MessageHandler; use crate::document::DocumentMessageHandler; 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 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)] pub struct ToolFsmState { pub document_tool_data: DocumentToolData, @@ -126,19 +163,15 @@ impl ToolFsmState { #[repr(usize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ToolType { + // General tool group Select, Artboard, Navigate, Eyedropper, - Text, Fill, Gradient, - Brush, - Heal, - Clone, - Patch, - Detail, - Relight, + + // Vector tool group Path, Pen, Freehand, @@ -147,6 +180,115 @@ pub enum ToolType { Rectangle, Ellipse, 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 { @@ -154,19 +296,15 @@ impl fmt::Display for ToolType { use ToolType::*; let name = match_variant_name!(match (self) { + // General tool group Select, Artboard, Navigate, Eyedropper, - Text, Fill, Gradient, - Brush, - Heal, - Clone, - Patch, - Detail, - Relight, + + // Vector tool group Path, Pen, Freehand, @@ -174,7 +312,16 @@ impl fmt::Display for ToolType { Line, Rectangle, Ellipse, - Shape + Shape, + Text, + + // Raster tool group + Brush, + Heal, + Clone, + Patch, + Detail, + Relight, }); formatter.write_str(name) @@ -191,19 +338,15 @@ pub enum StandardToolMessageType { pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageType) -> Option { match message_type { StandardToolMessageType::DocumentIsDirty => match tool { + // General tool group ToolType::Select => Some(SelectToolMessage::DocumentIsDirty.into()), ToolType::Artboard => Some(ArtboardToolMessage::DocumentIsDirty.into()), ToolType::Navigate => None, // Some(NavigateToolMessage::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::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()), + + // Vector tool group ToolType::Path => Some(PathToolMessage::DocumentIsDirty.into()), ToolType::Pen => Some(PenToolMessage::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::Ellipse => None, // Some(EllipseToolMessage::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 { + // General tool group ToolType::Select => Some(SelectToolMessage::Abort.into()), ToolType::Artboard => Some(ArtboardToolMessage::Abort.into()), ToolType::Navigate => Some(NavigateToolMessage::Abort.into()), ToolType::Eyedropper => Some(EyedropperToolMessage::Abort.into()), - ToolType::Text => Some(TextMessage::Abort.into()), ToolType::Fill => Some(FillToolMessage::Abort.into()), ToolType::Gradient => Some(GradientToolMessage::Abort.into()), - // ToolType::Brush => Some(BrushMessage::Abort.into()), - // ToolType::Heal => Some(HealMessage::Abort.into()), - // ToolType::Clone => Some(CloneMessage::Abort.into()), - // ToolType::Patch => Some(PatchMessage::Abort.into()), - // ToolType::Detail => Some(DetailToolMessage::Abort.into()), - // ToolType::Relight => Some(RelightMessage::Abort.into()), + + // Vector tool group ToolType::Path => Some(PathToolMessage::Abort.into()), ToolType::Pen => Some(PenToolMessage::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::Ellipse => Some(EllipseToolMessage::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 { ToolType::Path => Some(PathToolMessage::SelectionChanged.into()), @@ -248,19 +404,15 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { use ToolMessage::*; match message { + // General tool group Select(_) => ToolType::Select, Artboard(_) => ToolType::Artboard, Navigate(_) => ToolType::Navigate, Eyedropper(_) => ToolType::Eyedropper, - Text(_) => ToolType::Text, Fill(_) => ToolType::Fill, Gradient(_) => ToolType::Gradient, - // Brush(_) => ToolType::Brush, - // Heal(_) => ToolType::Heal, - // Clone(_) => ToolType::Clone, - // Patch(_) => ToolType::Patch, - // Detail(_) => ToolType::Detail, - // Relight(_) => ToolType::Relight, + + // Vector tool group Path(_) => ToolType::Path, Pen(_) => ToolType::Pen, Freehand(_) => ToolType::Freehand, @@ -269,6 +421,15 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { Rectangle(_) => ToolType::Rectangle, Ellipse(_) => ToolType::Ellipse, 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"), } } diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index af9767a7d..31e027cb0 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -82,6 +82,7 @@ pub enum ToolMessage { tool_type: ToolType, }, DocumentIsDirty, + InitTools, ResetColors, SelectionChanged, SelectPrimaryColor { diff --git a/editor/src/viewport_tools/tool_message_handler.rs b/editor/src/viewport_tools/tool_message_handler.rs index 61144ed70..21c8ddb43 100644 --- a/editor/src/viewport_tools/tool_message_handler.rs +++ b/editor/src/viewport_tools/tool_message_handler.rs @@ -2,6 +2,7 @@ use super::tool::{message_to_tool_type, standard_tool_message, update_working_co use crate::document::DocumentMessageHandler; use crate::input::InputPreprocessorMessageHandler; use crate::layout::layout_message::LayoutTarget; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use graphene::color::Color; @@ -69,12 +70,11 @@ impl MessageHandler { // Send the DocumentIsDirty message to the active tool's sub-tool message handler @@ -83,6 +83,21 @@ impl MessageHandler { + 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 => { let document_data = &mut self.tool_state.document_tool_data; diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 1a96fe1f5..a6a94448e 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -63,10 +63,10 @@ impl PropertyHolder for FreehandTool { widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), - value: self.options.line_weight as f64, + value: Some(self.options.line_weight as f64), is_integer: false, 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() }))], }]) diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 4abdcec0c..8fb0b962b 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -64,10 +64,10 @@ impl PropertyHolder for LineTool { widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), - value: self.options.line_weight as f64, + value: Some(self.options.line_weight as f64), is_integer: false, 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() }))], }]) diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index 457e52600..b6b1f50ef 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -73,10 +73,10 @@ impl PropertyHolder for PenTool { widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), - value: self.options.line_weight, + value: Some(self.options.line_weight), is_integer: false, 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() }))], }]) diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 5a6029ea4..117e97398 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -61,11 +61,11 @@ impl PropertyHolder for ShapeTool { WidgetLayout::new(vec![LayoutRow::Row { widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { label: "Sides".into(), - value: self.options.vertices as f64, + value: Some(self.options.vertices as f64), is_integer: true, min: Some(3.), 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() }))], }]) diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index fe38aa0dd..bf53a2247 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -67,10 +67,10 @@ impl PropertyHolder for SplineTool { widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), - value: self.options.line_weight, + value: Some(self.options.line_weight), is_integer: false, 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() }))], }]) diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index 0f1955c12..db2507622 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -115,10 +115,10 @@ impl PropertyHolder for TextTool { WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Size".into(), - value: self.options.font_size as f64, + value: Some(self.options.font_size as f64), is_integer: true, 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() })), ], diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fd3d7eb40..676e32de6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -120,7 +120,7 @@ img { .layout-col { .scrollable-x, .scrollable-y { - // Standard + // Firefox (standardized in CSS, but less capable) scrollbar-width: thin; scrollbar-width: 6px; scrollbar-gutter: 6px; @@ -130,7 +130,7 @@ img { scrollbar-width: none; } - // WebKit + // WebKit (only in Chromium/Safari but more capable) &::-webkit-scrollbar { width: calc(2px + 6px + 2px); height: calc(2px + 6px + 2px); @@ -177,14 +177,14 @@ img { .scrollable-x:not(.scrollable-y) { // Standard - overflow-x: auto; + overflow: auto hidden; // WebKit overflow-x: overlay; } .scrollable-y:not(.scrollable-x) { // Standard - overflow-y: auto; + overflow: hidden auto; // WebKit overflow-y: overlay; } @@ -325,6 +325,8 @@ export default defineComponent({ }, mounted() { this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen); + + this.editor.instance.init_app(); }, beforeUnmount() { this.inputManager?.removeListeners(); diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 65ed5b251..367643f65 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -1,58 +1,18 @@