diff --git a/editor/src/consts.rs b/editor/src/consts.rs index e3b88147d8..e8e4fff339 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -54,5 +54,5 @@ pub const FILE_EXPORT_SUFFIX: &str = ".svg"; 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.2"; +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.3"; pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05; diff --git a/editor/src/document/document_message.rs b/editor/src/document/document_message.rs index 6942bea218..9143841f54 100644 --- a/editor/src/document/document_message.rs +++ b/editor/src/document/document_message.rs @@ -28,6 +28,9 @@ pub enum DocumentMessage { #[remain::unsorted] #[child] TransformLayers(TransformLayerMessage), + #[remain::unsorted] + #[child] + PropertiesPanel(PropertiesPanelMessage), // Messages AbortTransaction, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index b8b3520c87..dcef8595c3 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,7 +1,7 @@ use super::clipboards::Clipboard; use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; -use super::vectorize_layer_metadata; +use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler}; use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler}; use crate::consts::{ ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR, @@ -45,6 +45,7 @@ pub struct DocumentMessageHandler { pub artboard_message_handler: ArtboardMessageHandler, #[serde(skip)] transform_layer_handler: TransformLayerMessageHandler, + properties_panel_message_handler: PropertiesPanelMessageHandler, pub overlays_visible: bool, pub snapping_enabled: bool, pub view_mode: ViewMode, @@ -65,6 +66,7 @@ impl Default for DocumentMessageHandler { 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(), @@ -676,6 +678,10 @@ impl MessageHandler for Docum self.transform_layer_handler .process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses); } + #[remain::unsorted] + PropertiesPanel(message) => { + self.properties_panel_message_handler.process_action(message, &self.graphene_document, responses); + } // Messages AbortTransaction => { @@ -683,9 +689,17 @@ impl MessageHandler for Docum responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); } AddSelectedLayers { additional_layers } => { - for layer_path in additional_layers { - responses.extend(self.select_layer(&layer_path)); + for layer_path in &additional_layers { + responses.extend(self.select_layer(layer_path)); } + + let selected_paths: Vec> = self.selected_layers().map(|path| path.to_vec()).collect(); + if selected_paths.is_empty() { + responses.push_back(PropertiesPanelMessage::ClearSelection.into()) + } else { + responses.push_back(PropertiesPanelMessage::SetActiveLayers { paths: selected_paths }.into()) + } + // 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()); @@ -743,12 +757,15 @@ impl MessageHandler for Docum DebugPrintDocument => { log::debug!("{:#?}\n{:#?}", self.graphene_document, self.layer_metadata); } - DeleteLayer { layer_path } => responses.push_front(DocumentOperation::DeleteLayer { path: layer_path }.into()), + DeleteLayer { layer_path } => { + responses.push_front(DocumentOperation::DeleteLayer { path: layer_path.clone() }.into()); + responses.push_back(PropertiesPanelMessage::CheckSelectedWasDeleted { path: layer_path }.into()); + } DeleteSelectedLayers => { self.backup(responses); for path in self.selected_layers_without_children() { - responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into()); + responses.push_front(DocumentMessage::DeleteLayer { layer_path: path.to_vec() }.into()); } responses.push_front(DocumentMessage::SelectionChanged.into()); @@ -861,9 +878,10 @@ impl MessageHandler for Docum ); } LayerChanged { affected_layer_path } => { - if let Ok(layer_entry) = self.layer_panel_entry(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(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into()); } MoveSelectedLayersTo { folder_path, diff --git a/editor/src/document/mod.rs b/editor/src/document/mod.rs index 18e858003c..2e7717c9f4 100644 --- a/editor/src/document/mod.rs +++ b/editor/src/document/mod.rs @@ -14,6 +14,8 @@ mod overlays_message; mod overlays_message_handler; mod portfolio_message; mod portfolio_message_handler; +mod properties_panel_message; +mod properties_panel_message_handler; mod transform_layer_message; mod transform_layer_message_handler; @@ -42,6 +44,11 @@ pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant}; #[doc(inline)] pub use portfolio_message_handler::PortfolioMessageHandler; +#[doc(inline)] +pub use properties_panel_message::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant}; +#[doc(inline)] +pub use properties_panel_message_handler::PropertiesPanelMessageHandler; + #[doc(inline)] pub use transform_layer_message::{TransformLayerMessage, TransformLayerMessageDiscriminant}; #[doc(inline)] diff --git a/editor/src/document/properties_panel_message.rs b/editor/src/document/properties_panel_message.rs new file mode 100644 index 0000000000..182da8f2bc --- /dev/null +++ b/editor/src/document/properties_panel_message.rs @@ -0,0 +1,24 @@ +use crate::message_prelude::*; + +use serde::{Deserialize, Serialize}; + +#[remain::sorted] +#[impl_message(Message, DocumentMessage, PropertiesPanel)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum PropertiesPanelMessage { + CheckSelectedWasDeleted { path: Vec }, + CheckSelectedWasUpdated { path: Vec }, + ClearSelection, + ModifyName { name: String }, + ModifyTransform { value: f64, transform_op: TransformOp }, + SetActiveLayers { paths: Vec> }, +} + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum TransformOp { + X, + Y, + Width, + Height, + Rotation, +} diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs new file mode 100644 index 0000000000..f64fc590c4 --- /dev/null +++ b/editor/src/document/properties_panel_message_handler.rs @@ -0,0 +1,379 @@ +use super::layer_panel::LayerDataTypeDiscriminant; +use crate::document::properties_panel_message::TransformOp; +use crate::layout::layout_message::LayoutTarget; +use crate::layout::widgets::{ + IconLabel, LayoutRow, NumberInput, PopoverButton, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout, +}; +use crate::message_prelude::*; + +use graphene::document::Document as GrapheneDocument; +use graphene::layers::layer_info::{Layer, LayerDataType}; +use graphene::{LayerId, Operation}; + +use glam::{DAffine2, DVec2}; +use serde::{Deserialize, Serialize}; +use std::f64::consts::PI; + +trait DAffine2Utils { + fn width(&self) -> f64; + fn update_width(self, new_width: f64) -> Self; + fn height(&self) -> f64; + fn update_height(self, new_height: f64) -> Self; + fn x(&self) -> f64; + fn update_x(self, new_x: f64) -> Self; + fn y(&self) -> f64; + fn update_y(self, new_y: f64) -> Self; + fn rotation(&self) -> f64; + fn update_rotation(self, new_rotation: f64) -> Self; +} + +impl DAffine2Utils for DAffine2 { + fn width(&self) -> f64 { + self.transform_vector2((1., 0.).into()).length() + } + + fn update_width(self, new_width: f64) -> Self { + self * DAffine2::from_scale((new_width / self.width(), 1.).into()) + } + + fn height(&self) -> f64 { + self.transform_vector2((0., 1.).into()).length() + } + + fn update_height(self, new_height: f64) -> Self { + self * DAffine2::from_scale((1., new_height / self.height()).into()) + } + + fn x(&self) -> f64 { + self.translation.x + } + + fn update_x(mut self, new_x: f64) -> Self { + self.translation.x = new_x; + self + } + + fn y(&self) -> f64 { + self.translation.y + } + + fn update_y(mut self, new_y: f64) -> Self { + self.translation.y = new_y; + self + } + + fn rotation(&self) -> f64 { + let cos = self.matrix2.col(0).x / self.width(); + let sin = self.matrix2.col(0).y / self.width(); + sin.atan2(cos) + } + + fn update_rotation(self, new_rotation: f64) -> Self { + let width = self.width(); + let height = self.height(); + let half_width = width / 2.; + let half_height = height / 2.; + + let angle_translation_offset = |angle: f64| DVec2::new(-half_width * angle.cos() + half_height * angle.sin(), -half_width * angle.sin() - half_height * angle.cos()); + let angle_translation_adjustment = angle_translation_offset(new_rotation) - angle_translation_offset(self.rotation()); + + DAffine2::from_scale_angle_translation((width, height).into(), new_rotation, self.translation + angle_translation_adjustment) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PropertiesPanelMessageHandler { + active_path: Option>, +} + +impl PropertiesPanelMessageHandler { + fn matches_selected(&self, path: &[LayerId]) -> bool { + let last_active_path = self.active_path.as_ref().map(|v| v.last().copied()).flatten(); + let last_modified = path.last().copied(); + matches!((last_active_path, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last) + } +} + +impl MessageHandler for PropertiesPanelMessageHandler { + #[remain::check] + fn process_action(&mut self, message: PropertiesPanelMessage, data: &GrapheneDocument, responses: &mut VecDeque) { + let graphene_document = data; + use PropertiesPanelMessage::*; + match message { + SetActiveLayers { paths } => { + if paths.len() > 1 { + // TODO: Allow for multiple selected layers + responses.push_back(PropertiesPanelMessage::ClearSelection.into()) + } else { + let path = paths.into_iter().next().unwrap(); + let layer = graphene_document.layer(&path).unwrap(); + register_layer_properties(layer, responses); + self.active_path = Some(path) + } + } + ClearSelection => { + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(vec![]), + layout_target: LayoutTarget::PropertiesOptionsPanel, + } + .into(), + ); + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(vec![]), + layout_target: LayoutTarget::PropertiesSectionsPanel, + } + .into(), + ); + } + ModifyTransform { value, transform_op } => { + let path = self.active_path.as_ref().expect("Received update for properties panel with no active layer"); + let layer = graphene_document.layer(path).unwrap(); + + use TransformOp::*; + let action = match transform_op { + X => DAffine2::update_x, + Y => DAffine2::update_y, + Width => DAffine2::update_width, + Height => DAffine2::update_height, + Rotation => DAffine2::update_rotation, + }; + + responses.push_back( + Operation::SetLayerTransform { + path: path.clone(), + transform: action(layer.transform, value).to_cols_array(), + } + .into(), + ); + } + ModifyName { name } => { + let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(DocumentMessage::SetLayerName { layer_path: path, name }.into()) + } + CheckSelectedWasUpdated { path } => { + if self.matches_selected(&path) { + let layer = graphene_document.layer(&path).unwrap(); + register_layer_properties(layer, responses); + } + } + CheckSelectedWasDeleted { path } => { + if self.matches_selected(&path) { + self.active_path = None; + responses.push_back( + LayoutMessage::SendLayout { + layout_target: LayoutTarget::PropertiesOptionsPanel, + layout: WidgetLayout::default(), + } + .into(), + ); + responses.push_back( + LayoutMessage::SendLayout { + layout_target: LayoutTarget::PropertiesSectionsPanel, + layout: WidgetLayout::default(), + } + .into(), + ); + } + } + } + } + + fn actions(&self) -> ActionList { + actions!(PropertiesMessageDiscriminant;) + } +} + +fn register_layer_properties(layer: &Layer, responses: &mut VecDeque) { + let options_bar = vec![LayoutRow::Row { + name: "".into(), + widgets: vec![ + match &layer.data { + LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { + icon: "NodeTypeFolder".into(), + gap_after: true, + })), + LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { + icon: "NodeTypePath".into(), + gap_after: true, + })), + LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { + icon: "NodeTypePath".into(), + gap_after: true, + })), + }, + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: LayerDataTypeDiscriminant::from(&layer.data).to_string(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), + on_update: WidgetCallback::new(|text_input| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Options Bar".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + ], + }]; + + let properties_body = match &layer.data { + LayerDataType::Folder(_) => { + vec![node_section_transform(layer)] + } + LayerDataType::Shape(_) => { + vec![node_section_transform(layer)] + } + LayerDataType::Text(_) => { + vec![node_section_transform(layer)] + } + }; + + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(options_bar), + layout_target: LayoutTarget::PropertiesOptionsPanel, + } + .into(), + ); + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(properties_body), + layout_target: LayoutTarget::PropertiesSectionsPanel, + } + .into(), + ); +} + +fn node_section_transform(layer: &Layer) -> LayoutRow { + LayoutRow::Section { + name: "Transform".into(), + layout: vec![ + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Position".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.x(), + label: "X".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::X, + } + .into() + }), + ..NumberInput::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.y(), + label: "Y".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Y, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Dimensions".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.width(), + label: "W".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Width, + } + .into() + }), + ..NumberInput::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.height(), + label: "H".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Height, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Rotation".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.rotation() * 180. / PI, + label: "R".into(), + unit: "°".into(), + on_update: WidgetCallback::new(|number_input| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value / 180. * PI, + transform_op: TransformOp::Rotation, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + ], + } +} diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index 023de49689..4e172d25f7 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -46,6 +46,8 @@ pub enum FrontendMessage { UpdateInputHints { hint_data: HintData }, UpdateMouseCursor { cursor: MouseCursorIcon }, UpdateOpenDocumentsList { open_documents: Vec }, + UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, + UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateToolOptionsLayout { 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 400a5f3692..c32bbb9abe 100644 --- a/editor/src/layout/layout_message.rs +++ b/editor/src/layout/layout_message.rs @@ -16,6 +16,8 @@ pub enum LayoutMessage { #[repr(u8)] pub enum LayoutTarget { DocumentBar, + PropertiesOptionsPanel, + PropertiesSectionsPanel, ToolOptions, // KEEP THIS ENUM LAST diff --git a/editor/src/layout/layout_message_handler.rs b/editor/src/layout/layout_message_handler.rs index 7bdd72ebcb..4bedcf66e0 100644 --- a/editor/src/layout/layout_message_handler.rs +++ b/editor/src/layout/layout_message_handler.rs @@ -23,6 +23,14 @@ impl LayoutMessageHandler { layout_target, layout: widget_layout.layout.clone(), }, + LayoutTarget::PropertiesOptionsPanel => FrontendMessage::UpdatePropertyPanelOptionsLayout { + layout_target, + layout: widget_layout.layout.clone(), + }, + LayoutTarget::PropertiesSectionsPanel => FrontendMessage::UpdatePropertyPanelSectionsLayout { + layout_target, + layout: widget_layout.layout.clone(), + }, LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), }; responses.push_back(message.into()); @@ -63,6 +71,7 @@ impl MessageHandler for LayoutMessageHandler { let callback_message = (icon_button.on_update.callback)(icon_button); responses.push_back(callback_message); } + Widget::IconLabel(_) => {} Widget::PopoverButton(_) => {} Widget::OptionalInput(optional_input) => { let update_value = value.as_bool().expect("OptionalInput update was not of type: bool"); @@ -76,6 +85,13 @@ impl MessageHandler for LayoutMessageHandler { let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&()); responses.push_back(callback_message); } + Widget::TextInput(text_input) => { + let update_value = value.as_str().expect("OptionalInput update was not of type: string"); + text_input.value = update_value.into(); + let callback_message = (text_input.on_update.callback)(text_input); + responses.push_back(callback_message); + } + Widget::TextLabel(_) => {} }; self.send_layout(layout_target, responses); } diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index 95ec267de2..b45c5365c2 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -54,15 +54,6 @@ pub enum LayoutRow { Section { name: String, layout: SubLayout }, } -impl LayoutRow { - pub fn widgets(&self) -> Vec { - match &self { - Self::Row { name: _, widgets } => widgets.to_vec(), - Self::Section { name: _, layout } => layout.iter().flat_map(|row| row.widgets()).collect(), - } - } -} - #[derive(Debug, Default)] pub struct WidgetIter<'a> { pub stack: Vec<&'a LayoutRow>, @@ -158,11 +149,14 @@ impl Default for WidgetCallback { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Widget { IconButton(IconButton), + IconLabel(IconLabel), NumberInput(NumberInput), OptionalInput(OptionalInput), PopoverButton(PopoverButton), RadioInput(RadioInput), Separator(Separator), + TextInput(TextInput), + TextLabel(TextLabel), } #[derive(Clone, Serialize, Deserialize, Derivative)] @@ -189,6 +183,18 @@ pub struct NumberInput { pub increment_callback_decrease: WidgetCallback, pub label: String, pub unit: String, + #[serde(rename = "displayDecimalPlaces")] + #[derivative(Default(value = "3"))] + pub display_decimal_places: u32, +} + +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct TextInput { + pub value: String, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] @@ -281,3 +287,17 @@ pub struct RadioEntryData { #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback<()>, } + +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq)] +pub struct IconLabel { + pub icon: String, + #[serde(rename = "gapAfter")] + pub gap_after: bool, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Default)] +pub struct TextLabel { + pub value: String, + pub bold: bool, + pub italic: bool, +} diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 5af3452c69..951d15e815 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -64,6 +64,7 @@ pub mod message_prelude { pub use crate::document::{MovementMessage, MovementMessageDiscriminant}; pub use crate::document::{OverlaysMessage, OverlaysMessageDiscriminant}; pub use crate::document::{PortfolioMessage, PortfolioMessageDiscriminant}; + pub use crate::document::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant}; pub use crate::document::{TransformLayerMessage, TransformLayerMessageDiscriminant}; pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant}; diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 82b12f7b4e..fc664e5c8c 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -6,12 +6,12 @@ - + - + diff --git a/frontend/src/components/panels/Properties.vue b/frontend/src/components/panels/Properties.vue index 56c3a2de25..07521ba2d7 100644 --- a/frontend/src/components/panels/Properties.vue +++ b/frontend/src/components/panels/Properties.vue @@ -1,15 +1,64 @@ - + diff --git a/frontend/src/components/widgets/WidgetLayout.vue b/frontend/src/components/widgets/WidgetLayout.vue index d3a2c48158..0ee3c2f45e 100644 --- a/frontend/src/components/widgets/WidgetLayout.vue +++ b/frontend/src/components/widgets/WidgetLayout.vue @@ -12,7 +12,6 @@ flex: 0 0 auto; display: flex; flex-direction: column; - align-items: center; } diff --git a/frontend/src/components/widgets/WidgetRow.vue b/frontend/src/components/widgets/WidgetRow.vue index 30b6136fee..98a3eb2704 100644 --- a/frontend/src/components/widgets/WidgetRow.vue +++ b/frontend/src/components/widgets/WidgetRow.vue @@ -1,4 +1,5 @@ - diff --git a/frontend/src/components/widgets/inputs/NumberInput.vue b/frontend/src/components/widgets/inputs/NumberInput.vue index d6a0a7f66b..51cc7cea8d 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.vue +++ b/frontend/src/components/widgets/inputs/NumberInput.vue @@ -185,8 +185,9 @@ export default defineComponent({ // Find the amount of digits on the left side of the decimal // 10.25 == 2 // 1.23 == 1 - // 0.23 == 0 (reason for the slightly more complicated code) - const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value); + // 0.23 == 0 (Reason for the slightly more complicated code) + const absValueInt = Math.floor(Math.abs(value)); + const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length; const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0); const displayValue = Math.round(value * roundingPower) / roundingPower; diff --git a/frontend/src/components/widgets/inputs/TextInput.vue b/frontend/src/components/widgets/inputs/TextInput.vue index 8d9858992f..30b42868e5 100644 --- a/frontend/src/components/widgets/inputs/TextInput.vue +++ b/frontend/src/components/widgets/inputs/TextInput.vue @@ -12,7 +12,13 @@ > - +