Implement the Properties panel with a transform section for layers (#527)
* initial layout system with tool options * cargo fmt * cargo fmt again * document bar defined on the backend * cargo fmt * removed RC<RefCell> * cargo fmt * - fix increment behavior - removed hashmap from layout message handler - removed no op message from layoutMessage * cargo fmt * only send documentBar when zoom or rotation is updated * ctrl-0 changes zoom properly * unfinished layer hook in * fix layerData name * layer panel options bar * basic x/y movment * working transform section * changed messages from tuples to structs * hook up text input * - fixed number input to be more clear - fixed actions for properties message handler * Add styling Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
d084775d81
commit
91e4201cb1
|
|
@ -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.);
|
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.2";
|
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.3";
|
||||||
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ pub enum DocumentMessage {
|
||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
#[child]
|
#[child]
|
||||||
TransformLayers(TransformLayerMessage),
|
TransformLayers(TransformLayerMessage),
|
||||||
|
#[remain::unsorted]
|
||||||
|
#[child]
|
||||||
|
PropertiesPanel(PropertiesPanelMessage),
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
AbortTransaction,
|
AbortTransaction,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
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 super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
|
||||||
use crate::consts::{
|
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,
|
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,
|
pub artboard_message_handler: ArtboardMessageHandler,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
transform_layer_handler: TransformLayerMessageHandler,
|
transform_layer_handler: TransformLayerMessageHandler,
|
||||||
|
properties_panel_message_handler: PropertiesPanelMessageHandler,
|
||||||
pub overlays_visible: bool,
|
pub overlays_visible: bool,
|
||||||
pub snapping_enabled: bool,
|
pub snapping_enabled: bool,
|
||||||
pub view_mode: ViewMode,
|
pub view_mode: ViewMode,
|
||||||
|
|
@ -65,6 +66,7 @@ impl Default for DocumentMessageHandler {
|
||||||
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(),
|
||||||
snapping_enabled: true,
|
snapping_enabled: true,
|
||||||
overlays_visible: true,
|
overlays_visible: true,
|
||||||
view_mode: ViewMode::default(),
|
view_mode: ViewMode::default(),
|
||||||
|
|
@ -676,6 +678,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
self.transform_layer_handler
|
self.transform_layer_handler
|
||||||
.process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses);
|
.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
|
// Messages
|
||||||
AbortTransaction => {
|
AbortTransaction => {
|
||||||
|
|
@ -683,9 +689,17 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||||
}
|
}
|
||||||
AddSelectedLayers { additional_layers } => {
|
AddSelectedLayers { additional_layers } => {
|
||||||
for layer_path in additional_layers {
|
for layer_path in &additional_layers {
|
||||||
responses.extend(self.select_layer(&layer_path));
|
responses.extend(self.select_layer(layer_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selected_paths: Vec<Vec<u64>> = 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
|
// 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());
|
||||||
|
|
@ -743,12 +757,15 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
DebugPrintDocument => {
|
DebugPrintDocument => {
|
||||||
log::debug!("{:#?}\n{:#?}", self.graphene_document, self.layer_metadata);
|
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 => {
|
DeleteSelectedLayers => {
|
||||||
self.backup(responses);
|
self.backup(responses);
|
||||||
|
|
||||||
for path in self.selected_layers_without_children() {
|
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());
|
responses.push_front(DocumentMessage::SelectionChanged.into());
|
||||||
|
|
@ -861,9 +878,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) {
|
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::UpdateDocumentLayer { data: layer_entry }.into());
|
||||||
}
|
}
|
||||||
|
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
|
||||||
}
|
}
|
||||||
MoveSelectedLayersTo {
|
MoveSelectedLayersTo {
|
||||||
folder_path,
|
folder_path,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ mod overlays_message;
|
||||||
mod overlays_message_handler;
|
mod overlays_message_handler;
|
||||||
mod portfolio_message;
|
mod portfolio_message;
|
||||||
mod portfolio_message_handler;
|
mod portfolio_message_handler;
|
||||||
|
mod properties_panel_message;
|
||||||
|
mod properties_panel_message_handler;
|
||||||
mod transform_layer_message;
|
mod transform_layer_message;
|
||||||
mod transform_layer_message_handler;
|
mod transform_layer_message_handler;
|
||||||
|
|
||||||
|
|
@ -42,6 +44,11 @@ pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use portfolio_message_handler::PortfolioMessageHandler;
|
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)]
|
#[doc(inline)]
|
||||||
pub use transform_layer_message::{TransformLayerMessage, TransformLayerMessageDiscriminant};
|
pub use transform_layer_message::{TransformLayerMessage, TransformLayerMessageDiscriminant};
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
|
|
@ -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<LayerId> },
|
||||||
|
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||||
|
ClearSelection,
|
||||||
|
ModifyName { name: String },
|
||||||
|
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||||
|
SetActiveLayers { paths: Vec<Vec<LayerId>> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum TransformOp {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Width,
|
||||||
|
Height,
|
||||||
|
Rotation,
|
||||||
|
}
|
||||||
|
|
@ -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<Vec<LayerId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PropertiesPanelMessage, &GrapheneDocument> for PropertiesPanelMessageHandler {
|
||||||
|
#[remain::check]
|
||||||
|
fn process_action(&mut self, message: PropertiesPanelMessage, data: &GrapheneDocument, responses: &mut VecDeque<Message>) {
|
||||||
|
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<Message>) {
|
||||||
|
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()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,8 @@ pub enum FrontendMessage {
|
||||||
UpdateInputHints { hint_data: HintData },
|
UpdateInputHints { hint_data: HintData },
|
||||||
UpdateMouseCursor { cursor: MouseCursorIcon },
|
UpdateMouseCursor { cursor: MouseCursorIcon },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
||||||
|
UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
|
UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ pub enum LayoutMessage {
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LayoutTarget {
|
pub enum LayoutTarget {
|
||||||
DocumentBar,
|
DocumentBar,
|
||||||
|
PropertiesOptionsPanel,
|
||||||
|
PropertiesSectionsPanel,
|
||||||
ToolOptions,
|
ToolOptions,
|
||||||
|
|
||||||
// KEEP THIS ENUM LAST
|
// KEEP THIS ENUM LAST
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ impl LayoutMessageHandler {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
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"),
|
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());
|
||||||
|
|
@ -63,6 +71,7 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||||
let callback_message = (icon_button.on_update.callback)(icon_button);
|
let callback_message = (icon_button.on_update.callback)(icon_button);
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
|
Widget::IconLabel(_) => {}
|
||||||
Widget::PopoverButton(_) => {}
|
Widget::PopoverButton(_) => {}
|
||||||
Widget::OptionalInput(optional_input) => {
|
Widget::OptionalInput(optional_input) => {
|
||||||
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
||||||
|
|
@ -76,6 +85,13 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||||
responses.push_back(callback_message);
|
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);
|
self.send_layout(layout_target, responses);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,15 +54,6 @@ pub enum LayoutRow {
|
||||||
Section { name: String, layout: SubLayout },
|
Section { name: String, layout: SubLayout },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutRow {
|
|
||||||
pub fn widgets(&self) -> Vec<WidgetHolder> {
|
|
||||||
match &self {
|
|
||||||
Self::Row { name: _, widgets } => widgets.to_vec(),
|
|
||||||
Self::Section { name: _, layout } => layout.iter().flat_map(|row| row.widgets()).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct WidgetIter<'a> {
|
pub struct WidgetIter<'a> {
|
||||||
pub stack: Vec<&'a LayoutRow>,
|
pub stack: Vec<&'a LayoutRow>,
|
||||||
|
|
@ -158,11 +149,14 @@ impl<T> Default for WidgetCallback<T> {
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Widget {
|
pub enum Widget {
|
||||||
IconButton(IconButton),
|
IconButton(IconButton),
|
||||||
|
IconLabel(IconLabel),
|
||||||
NumberInput(NumberInput),
|
NumberInput(NumberInput),
|
||||||
OptionalInput(OptionalInput),
|
OptionalInput(OptionalInput),
|
||||||
PopoverButton(PopoverButton),
|
PopoverButton(PopoverButton),
|
||||||
RadioInput(RadioInput),
|
RadioInput(RadioInput),
|
||||||
Separator(Separator),
|
Separator(Separator),
|
||||||
|
TextInput(TextInput),
|
||||||
|
TextLabel(TextLabel),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||||
|
|
@ -189,6 +183,18 @@ pub struct NumberInput {
|
||||||
pub increment_callback_decrease: WidgetCallback<NumberInput>,
|
pub increment_callback_decrease: WidgetCallback<NumberInput>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub unit: 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<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
|
@ -281,3 +287,17 @@ pub struct RadioEntryData {
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_update: WidgetCallback<()>,
|
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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ pub mod message_prelude {
|
||||||
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
||||||
pub use crate::document::{OverlaysMessage, OverlaysMessageDiscriminant};
|
pub use crate::document::{OverlaysMessage, OverlaysMessageDiscriminant};
|
||||||
pub use crate::document::{PortfolioMessage, PortfolioMessageDiscriminant};
|
pub use crate::document::{PortfolioMessage, PortfolioMessageDiscriminant};
|
||||||
|
pub use crate::document::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant};
|
||||||
pub use crate::document::{TransformLayerMessage, TransformLayerMessageDiscriminant};
|
pub use crate::document::{TransformLayerMessage, TransformLayerMessageDiscriminant};
|
||||||
pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant};
|
pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant};
|
||||||
pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant};
|
pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant};
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@
|
||||||
|
|
||||||
<Separator :type="'Section'" />
|
<Separator :type="'Section'" />
|
||||||
|
|
||||||
<WidgetLayout :layout="toolOptionsLayout" />
|
<WidgetLayout :layout="toolOptionsLayout" class="tool-options" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
|
|
||||||
<LayoutRow class="spacer"></LayoutRow>
|
<LayoutRow class="spacer"></LayoutRow>
|
||||||
|
|
||||||
<WidgetLayout :layout="documentBarLayout" class="right side" />
|
<WidgetLayout :layout="documentBarLayout" class="right side document-bar" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="shelf-and-viewport">
|
<LayoutRow class="shelf-and-viewport">
|
||||||
<LayoutCol class="shelf">
|
<LayoutCol class="shelf">
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutCol class="properties-panel"></LayoutCol>
|
<LayoutCol class="properties">
|
||||||
|
<LayoutRow class="options-bar">
|
||||||
|
<WidgetLayout :layout="propertiesOptionsLayout"></WidgetLayout>
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="sections" :scrollableY="true">
|
||||||
|
<WidgetLayout :layout="propertiesSectionsLayout"></WidgetLayout>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.properties {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.widget-layout {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-bar {
|
||||||
|
height: 32px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sections {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import { defaultWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } 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 WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { LayoutCol },
|
inject: ["editor", "dialog"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
propertiesOptionsLayout: defaultWidgetLayout(),
|
||||||
|
propertiesSectionsLayout: defaultWidgetLayout(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
|
||||||
|
this.propertiesOptionsLayout = updatePropertyPanelOptionsLayout;
|
||||||
|
});
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||||
|
this.propertiesSectionsLayout = updatePropertyPanelSectionsLayout;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
WidgetLayout,
|
||||||
|
LayoutRow,
|
||||||
|
LayoutCol,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div>{{ widgetData.name }}</div>
|
||||||
<div class="widget-row">
|
<div class="widget-row">
|
||||||
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
||||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||||
|
|
@ -13,18 +14,20 @@
|
||||||
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
||||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||||
/>
|
/>
|
||||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||||
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
||||||
|
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="component.props">{{ component.props.value }}</TextLabel>
|
||||||
|
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.widget-row {
|
.widget-row {
|
||||||
height: 100%;
|
height: 32px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -42,6 +45,8 @@ import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -63,6 +68,8 @@ export default defineComponent({
|
||||||
IconButton,
|
IconButton,
|
||||||
OptionalInput,
|
OptionalInput,
|
||||||
RadioInput,
|
RadioInput,
|
||||||
|
TextLabel,
|
||||||
|
IconLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,78 @@
|
||||||
<!-- TODO: Implement collapsable sections with properties system -->
|
<!-- TODO: Implement collapsable sections with properties system -->
|
||||||
<template>
|
<template>
|
||||||
<div class="widget-section">
|
<LayoutCol class="widget-section">
|
||||||
<template v-for="(layoutRow, index) in widgetData.layout" :key="index">
|
<LayoutRow class="header" @click.stop="() => (expanded = !expanded)">
|
||||||
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget"></component>
|
<div class="expand-arrow" :class="{ expanded }"></div>
|
||||||
</template>
|
<Separator :type="'Related'" />
|
||||||
</div>
|
<TextLabel :bold="true">{{ widgetData.name }}</TextLabel>
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutCol class="body" v-if="expanded">
|
||||||
|
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget" v-for="(layoutRow, index) in widgetData.layout" :key="index"></component>
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutCol>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.widget-section {
|
.widget-section {
|
||||||
height: 100%;
|
.header {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 24px;
|
||||||
display: flex;
|
background: var(--color-4-dimgray);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0 -4px;
|
||||||
|
|
||||||
|
.expand-arrow {
|
||||||
|
width: 6px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 3px 0 3px 6px;
|
||||||
|
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded::after {
|
||||||
|
border-width: 6px 3px 0 3px;
|
||||||
|
border-color: var(--color-e-nearwhite) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
flex: 0 0 30%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetSection as WidgetSectionFromJsMessages } from "@/dispatcher/js-messages";
|
import { isWidgetRow, isWidgetSection, LayoutRow as LayoutSystemRow, WidgetSection as WidgetSectionFromJsMessages } from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||||
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
||||||
|
|
||||||
const WidgetSection = defineComponent({
|
const WidgetSection = defineComponent({
|
||||||
|
|
@ -34,21 +86,27 @@ const WidgetSection = defineComponent({
|
||||||
return {
|
return {
|
||||||
isWidgetRow,
|
isWidgetRow,
|
||||||
isWidgetSection,
|
isWidgetSection,
|
||||||
|
expanded: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
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);
|
||||||
},
|
},
|
||||||
layoutRowType(layoutRow: LayoutRow): unknown {
|
layoutRowType(layoutRow: LayoutSystemRow): unknown {
|
||||||
if (isWidgetRow(layoutRow)) return WidgetRow;
|
if (isWidgetRow(layoutRow)) return WidgetRow;
|
||||||
if (isWidgetSection(layoutRow)) return WidgetSection;
|
if (isWidgetSection(layoutRow)) return WidgetSection;
|
||||||
|
|
||||||
throw new Error("Layout row type does not exist");
|
throw new Error("Layout row type does not exist");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: { WidgetRow },
|
components: {
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
Separator,
|
||||||
|
WidgetRow,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export default WidgetSection;
|
export default WidgetSection;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,8 +185,9 @@ export default defineComponent({
|
||||||
// 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
|
||||||
// 0.23 == 0 (reason for the slightly more complicated code)
|
// 0.23 == 0 (Reason for the slightly more complicated code)
|
||||||
const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value);
|
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 roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
|
||||||
|
|
||||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@
|
||||||
></FieldInput>
|
></FieldInput>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.text-input {
|
||||||
|
input {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
@ -20,7 +26,7 @@ import { defineComponent, PropType } from "vue";
|
||||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ["update:value"],
|
emits: ["update:value", "commitText"],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String as PropType<string>, required: true },
|
value: { type: String as PropType<string>, required: true },
|
||||||
label: { type: String as PropType<string>, required: false },
|
label: { type: String as PropType<string>, required: false },
|
||||||
|
|
@ -54,7 +60,13 @@ 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 in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
|
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
|
||||||
if (this.editing) this.onCancelTextChange();
|
if (!this.editing) return;
|
||||||
|
|
||||||
|
this.onCancelTextChange();
|
||||||
|
|
||||||
|
// TODO: Find a less hacky way to do this
|
||||||
|
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||||
|
this.$emit("commitText", inputElement.value);
|
||||||
},
|
},
|
||||||
onCancelTextChange() {
|
onCancelTextChange() {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
||||||
return Boolean((layoutRow as WidgetSection).layout);
|
return Boolean((layoutRow as WidgetSection).layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput";
|
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel";
|
||||||
|
|
||||||
export interface Widget {
|
export interface Widget {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
|
|
@ -428,7 +428,21 @@ export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
layout!: LayoutRow[];
|
layout!: LayoutRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateDocumentBarLayout extends JsMessage {
|
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
|
||||||
layout_target!: unknown;
|
layout_target!: unknown;
|
||||||
|
|
||||||
@Transform(({ value }) => createWidgetLayout(value))
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
|
@ -457,7 +471,7 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
||||||
if (rowOrSection.Section) {
|
if (rowOrSection.Section) {
|
||||||
return {
|
return {
|
||||||
name: rowOrSection.Section.name,
|
name: rowOrSection.Section.name,
|
||||||
layout: createWidgetLayout(rowOrSection.Section),
|
layout: createWidgetLayout(rowOrSection.Section.layout),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,6 +522,8 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
||||||
UpdateInputHints,
|
UpdateInputHints,
|
||||||
UpdateMouseCursor,
|
UpdateMouseCursor,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
|
UpdatePropertyPanelOptionsLayout,
|
||||||
|
UpdatePropertyPanelSectionsLayout,
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
UpdateWorkingColors,
|
UpdateWorkingColors,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue