Replace the Spreadsheet panel with an improved Data panel (#3037)
* Improve the table data panel * Add the "Window" menu bar section and polish everything
This commit is contained in:
parent
2f4aef34e5
commit
2bb4509647
|
|
@ -2130,6 +2130,7 @@ dependencies = [
|
|||
name = "graphite-editor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bezier-rs",
|
||||
"bitflags 2.9.1",
|
||||
"bytemuck",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ web-sys = { workspace = true }
|
|||
bytemuck = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
spin = "0.9.8"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ pub struct DispatcherMessageHandlers {
|
|||
pub portfolio_message_handler: PortfolioMessageHandler,
|
||||
preferences_message_handler: PreferencesMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
workspace_message_handler: WorkspaceMessageHandler,
|
||||
}
|
||||
|
||||
impl DispatcherMessageHandlers {
|
||||
|
|
@ -231,9 +230,6 @@ impl Dispatcher {
|
|||
|
||||
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
|
||||
}
|
||||
Message::Workspace(message) => {
|
||||
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::NoOp => {}
|
||||
Message::Batched { messages } => {
|
||||
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
|
|||
for (_, message) in elements.rev() {
|
||||
responses.add_front(message);
|
||||
}
|
||||
for (id, messages) in self.after_graph_run.iter() {
|
||||
for (&document_id, messages) in self.after_graph_run.iter() {
|
||||
if !messages.is_empty() {
|
||||
responses.add(PortfolioMessage::SubmitGraphRender { document_id: *id, ignore_hash: false });
|
||||
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,11 +149,16 @@ pub enum FrontendMessage {
|
|||
UpdateGraphViewOverlay {
|
||||
open: bool,
|
||||
},
|
||||
UpdateSpreadsheetState {
|
||||
UpdateDataPanelState {
|
||||
open: bool,
|
||||
node: Option<NodeId>,
|
||||
},
|
||||
UpdateSpreadsheetLayout {
|
||||
UpdatePropertiesPanelState {
|
||||
open: bool,
|
||||
},
|
||||
UpdateLayersPanelState {
|
||||
open: bool,
|
||||
},
|
||||
UpdateDataPanelLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
|
|
@ -296,7 +301,7 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "openDocuments")]
|
||||
open_documents: Vec<FrontendDocumentDetails>,
|
||||
},
|
||||
UpdatePropertyPanelSectionsLayout {
|
||||
UpdatePropertiesPanelLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
||||
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
|
||||
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
|
||||
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
|
||||
//
|
||||
// FrontendMessage
|
||||
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ impl LayoutMessageHandler {
|
|||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::ImageLabel(_) => {}
|
||||
Widget::IconLabel(_) => {}
|
||||
Widget::InvisibleStandinInput(invisible) => {
|
||||
let callback_message = match action {
|
||||
|
|
@ -481,18 +482,18 @@ impl LayoutMessageHandler {
|
|||
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
|
||||
|
||||
let message = match layout_target {
|
||||
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
||||
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
|
||||
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
|
||||
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
|
||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
|
||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
||||
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
|
||||
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
|
||||
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
|
||||
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
|
||||
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
||||
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
|
||||
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
|
||||
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
|
||||
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
|
||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
|
||||
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
|
||||
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ pub enum LayoutTarget {
|
|||
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
|
||||
NodeGraphControlBar,
|
||||
/// The body of the Properties panel containing many collapsable sections.
|
||||
PropertiesSections,
|
||||
PropertiesPanel,
|
||||
/// The spredsheet panel allows for the visualisation of data in the graph.
|
||||
Spreadsheet,
|
||||
DataPanel,
|
||||
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
|
||||
ToolOptions,
|
||||
/// The vertical buttons for all of the tools on the left of the canvas.
|
||||
|
|
@ -369,6 +369,7 @@ impl LayoutGroup {
|
|||
Widget::IconButton(x) => &mut x.tooltip,
|
||||
Widget::IconLabel(x) => &mut x.tooltip,
|
||||
Widget::ImageButton(x) => &mut x.tooltip,
|
||||
Widget::ImageLabel(x) => &mut x.tooltip,
|
||||
Widget::NumberInput(x) => &mut x.tooltip,
|
||||
Widget::ParameterExposeButton(x) => &mut x.tooltip,
|
||||
Widget::PopoverButton(x) => &mut x.tooltip,
|
||||
|
|
@ -546,6 +547,7 @@ pub enum Widget {
|
|||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
ImageButton(ImageButton),
|
||||
ImageLabel(ImageLabel),
|
||||
InvisibleStandinInput(InvisibleStandinInput),
|
||||
NodeCatalog(NodeCatalog),
|
||||
NumberInput(NumberInput),
|
||||
|
|
@ -622,6 +624,7 @@ impl DiffUpdate {
|
|||
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconLabel(_)
|
||||
| Widget::ImageLabel(_)
|
||||
| Widget::CurveInput(_)
|
||||
| Widget::InvisibleStandinInput(_)
|
||||
| Widget::NodeCatalog(_)
|
||||
|
|
|
|||
|
|
@ -168,8 +168,6 @@ pub struct ColorInput {
|
|||
#[widget_builder(constructor)]
|
||||
pub value: FillChoice,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// TODO: Implement
|
||||
// #[serde(rename = "allowTransparency")]
|
||||
// #[derivative(Default(value = "false"))]
|
||||
|
|
@ -179,9 +177,11 @@ pub struct ColorInput {
|
|||
#[derivative(Default(value = "true"))]
|
||||
pub allow_none: bool,
|
||||
|
||||
// TODO: Implement
|
||||
// pub disabled: bool,
|
||||
//
|
||||
pub disabled: bool,
|
||||
|
||||
#[serde(rename = "menuDirection")]
|
||||
pub menu_direction: Option<MenuDirection>,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
|
|
|
|||
|
|
@ -416,6 +416,9 @@ pub struct TextInput {
|
|||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
|||
|
|
@ -65,4 +65,17 @@ pub struct TextLabel {
|
|||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct ImageLabel {
|
||||
#[widget_builder(constructor)]
|
||||
pub url: String,
|
||||
|
||||
pub width: Option<String>,
|
||||
|
||||
pub height: Option<String>,
|
||||
|
||||
pub tooltip: String,
|
||||
}
|
||||
|
||||
// TODO: Add UserInputLabel
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ pub enum Message {
|
|||
Preferences(PreferencesMessage),
|
||||
#[child]
|
||||
Tool(ToolMessage),
|
||||
#[child]
|
||||
Workspace(WorkspaceMessage),
|
||||
|
||||
// Messages
|
||||
NoOp,
|
||||
|
|
|
|||
|
|
@ -16,4 +16,3 @@ pub mod portfolio;
|
|||
pub mod preferences;
|
||||
pub mod prelude;
|
||||
pub mod tool;
|
||||
pub mod workspace;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::InspectResult;
|
||||
|
||||
/// The spreadsheet UI allows for graph data to be previewed.
|
||||
#[impl_message(Message, PortfolioMessage, Spreadsheet)]
|
||||
/// The Data panel UI allows the user to visualize the output data of the selected node.
|
||||
#[impl_message(Message, DocumentMessage, DataPanel)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SpreadsheetMessage {
|
||||
ToggleOpen,
|
||||
|
||||
pub enum DataPanelMessage {
|
||||
UpdateLayout {
|
||||
#[serde(skip)]
|
||||
inspect_result: InspectResult,
|
||||
},
|
||||
ClearLayout,
|
||||
|
||||
PushToElementPath {
|
||||
index: usize,
|
||||
|
|
@ -19,14 +18,15 @@ pub enum SpreadsheetMessage {
|
|||
len: usize,
|
||||
},
|
||||
|
||||
ViewVectorDomain {
|
||||
domain: VectorDomain,
|
||||
ViewVectorTableTab {
|
||||
tab: VectorTableTab,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorDomain {
|
||||
pub enum VectorTableTab {
|
||||
#[default]
|
||||
Properties,
|
||||
Points,
|
||||
Segments,
|
||||
Regions,
|
||||
|
|
@ -0,0 +1,633 @@
|
|||
use super::VectorTableTab;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
|
||||
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Context;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::{Fill, FillChoice};
|
||||
use graphene_std::{Artboard, Graphic};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct DataPanelMessageContext<'a> {
|
||||
pub network_interface: &'a mut NodeNetworkInterface,
|
||||
pub data_panel_open: bool,
|
||||
}
|
||||
|
||||
/// The data panel allows for graph data to be previewed.
|
||||
#[derive(Default, Debug, Clone, ExtractField)]
|
||||
pub struct DataPanelMessageHandler {
|
||||
introspected_node: Option<NodeId>,
|
||||
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
|
||||
element_path: Vec<usize>,
|
||||
active_vector_table_tab: VectorTableTab,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<DataPanelMessage, DataPanelMessageContext<'_>> for DataPanelMessageHandler {
|
||||
fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque<Message>, context: DataPanelMessageContext) {
|
||||
match message {
|
||||
DataPanelMessage::UpdateLayout { mut inspect_result } => {
|
||||
self.introspected_node = Some(inspect_result.inspect_node);
|
||||
self.introspected_data = inspect_result.take_data();
|
||||
self.update_layout(responses, context);
|
||||
}
|
||||
DataPanelMessage::ClearLayout => {
|
||||
self.introspected_node = None;
|
||||
self.introspected_data = None;
|
||||
self.element_path.clear();
|
||||
self.active_vector_table_tab = VectorTableTab::default();
|
||||
self.update_layout(responses, context);
|
||||
}
|
||||
|
||||
DataPanelMessage::PushToElementPath { index } => {
|
||||
self.element_path.push(index);
|
||||
self.update_layout(responses, context);
|
||||
}
|
||||
DataPanelMessage::TruncateElementPath { len } => {
|
||||
self.element_path.truncate(len);
|
||||
self.update_layout(responses, context);
|
||||
}
|
||||
|
||||
DataPanelMessage::ViewVectorTableTab { tab } => {
|
||||
self.active_vector_table_tab = tab;
|
||||
self.update_layout(responses, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(DataPanelMessage;)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataPanelMessageHandler {
|
||||
fn update_layout(&mut self, responses: &mut VecDeque<Message>, context: DataPanelMessageContext<'_>) {
|
||||
let DataPanelMessageContext { network_interface, .. } = context;
|
||||
|
||||
let mut layout_data = LayoutData {
|
||||
current_depth: 0,
|
||||
desired_path: &mut self.element_path,
|
||||
breadcrumbs: Vec::new(),
|
||||
vector_table_tab: self.active_vector_table_tab,
|
||||
};
|
||||
|
||||
// Main data visualization
|
||||
let mut layout = self
|
||||
.introspected_data
|
||||
.as_ref()
|
||||
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data).unwrap_or_else(|| label("Visualization of this data type is not yet supported")))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut widgets = Vec::new();
|
||||
|
||||
// Selected layer/node name
|
||||
if let Some(node_id) = self.introspected_node {
|
||||
let is_layer = network_interface.is_layer(&node_id, &[]);
|
||||
|
||||
widgets.extend([
|
||||
if is_layer {
|
||||
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder()
|
||||
} else {
|
||||
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder()
|
||||
},
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
TextInput::new(network_interface.display_name(&node_id, &[]))
|
||||
.tooltip(if is_layer { "Name of the selected layer" } else { "Name of the selected node" })
|
||||
.on_update(move |text_input| {
|
||||
NodeGraphMessage::SetDisplayName {
|
||||
node_id,
|
||||
alias: text_input.value.clone(),
|
||||
skip_adding_history_step: false,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.max_width(200)
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Element path breadcrumbs
|
||||
if !layout_data.breadcrumbs.is_empty() {
|
||||
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
|
||||
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
|
||||
.widget_holder();
|
||||
widgets.push(breadcrumb);
|
||||
}
|
||||
|
||||
if !widgets.is_empty() {
|
||||
layout.insert(0, LayoutGroup::Row { widgets });
|
||||
}
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout { layout }),
|
||||
layout_target: LayoutTarget::DataPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct LayoutData<'a> {
|
||||
current_depth: usize,
|
||||
desired_path: &'a mut Vec<usize>,
|
||||
breadcrumbs: Vec<String>,
|
||||
vector_table_tab: VectorTableTab,
|
||||
}
|
||||
|
||||
macro_rules! generate_layout_downcast {
|
||||
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
|
||||
if false { None }
|
||||
$(
|
||||
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<Context, $ty>>() {
|
||||
Some(io.output.layout_with_breadcrumb($data))
|
||||
}
|
||||
)*
|
||||
else { None }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We simply try all these types sequentially. Find a better strategy.
|
||||
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
||||
generate_layout_downcast!(introspected_data, data, [
|
||||
Table<Artboard>,
|
||||
Table<Graphic>,
|
||||
Table<Vector>,
|
||||
Table<Raster<CPU>>,
|
||||
Table<Raster<GPU>>,
|
||||
Table<Color>,
|
||||
Color,
|
||||
Option<Color>,
|
||||
f64,
|
||||
u32,
|
||||
u64,
|
||||
bool,
|
||||
String,
|
||||
Option<f64>,
|
||||
DVec2,
|
||||
DAffine2,
|
||||
])
|
||||
}
|
||||
|
||||
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
|
||||
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
|
||||
}
|
||||
|
||||
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
|
||||
let error = vec![TextLabel::new(x).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets: error }]
|
||||
}
|
||||
|
||||
trait TableRowLayout {
|
||||
fn type_name() -> &'static str;
|
||||
fn identifier(&self) -> String;
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
data.breadcrumbs.push(self.identifier());
|
||||
self.element_page(data)
|
||||
}
|
||||
fn element_widget(&self, index: usize) -> WidgetHolder {
|
||||
TextButton::new(self.identifier())
|
||||
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
|
||||
.widget_holder()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TableRowLayout> TableRowLayout for Table<T> {
|
||||
fn type_name() -> &'static str {
|
||||
"Table"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
|
||||
}
|
||||
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
|
||||
if let Some(row) = self.get(index) {
|
||||
data.current_depth += 1;
|
||||
let result = row.element.layout_with_breadcrumb(data);
|
||||
data.current_depth -= 1;
|
||||
return result;
|
||||
} else {
|
||||
warn!("Desired path truncated");
|
||||
data.desired_path.truncate(data.current_depth);
|
||||
}
|
||||
}
|
||||
|
||||
let mut rows = self
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{index}")).widget_holder(),
|
||||
row.element.element_widget(index),
|
||||
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(),
|
||||
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
|
||||
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
|
||||
|
||||
vec![LayoutGroup::Table { rows }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Artboard {
|
||||
fn type_name() -> &'static str {
|
||||
"Artboard"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.label.clone()
|
||||
}
|
||||
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.content.element_page(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Graphic {
|
||||
fn type_name() -> &'static str {
|
||||
"Graphic"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
match self {
|
||||
Self::Graphic(table) => table.identifier(),
|
||||
Self::Vector(table) => table.identifier(),
|
||||
Self::RasterCPU(table) => table.identifier(),
|
||||
Self::RasterGPU(table) => table.identifier(),
|
||||
Self::Color(table) => table.identifier(),
|
||||
}
|
||||
}
|
||||
// Don't put a breadcrumb for Graphic
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.element_page(data)
|
||||
}
|
||||
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
match self {
|
||||
Self::Graphic(table) => table.layout_with_breadcrumb(data),
|
||||
Self::Vector(table) => table.layout_with_breadcrumb(data),
|
||||
Self::RasterCPU(table) => table.layout_with_breadcrumb(data),
|
||||
Self::RasterGPU(table) => table.layout_with_breadcrumb(data),
|
||||
Self::Color(table) => table.layout_with_breadcrumb(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Vector {
|
||||
fn type_name() -> &'static str {
|
||||
"Vector"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!(
|
||||
"Vector ({} point{}, {} segment{})",
|
||||
self.point_domain.ids().len(),
|
||||
if self.point_domain.ids().len() == 1 { "" } else { "s" },
|
||||
self.segment_domain.ids().len(),
|
||||
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
|
||||
)
|
||||
}
|
||||
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
|
||||
.into_iter()
|
||||
.map(|tab| {
|
||||
RadioEntryData::new(format!("{tab:?}"))
|
||||
.label(format!("{tab:?}"))
|
||||
.on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into())
|
||||
})
|
||||
.collect();
|
||||
let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_holder()];
|
||||
|
||||
let mut table_rows = Vec::new();
|
||||
match data.vector_table_tab {
|
||||
VectorTableTab::Properties => {
|
||||
table_rows.push(column_headings(&["property", "value"]));
|
||||
|
||||
match self.style.fill.clone() {
|
||||
Fill::None => table_rows.push(vec![
|
||||
TextLabel::new("Fill").widget_holder(),
|
||||
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
|
||||
]),
|
||||
Fill::Solid(color) => table_rows.push(vec![
|
||||
TextLabel::new("Fill").widget_holder(),
|
||||
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
|
||||
]),
|
||||
Fill::Gradient(gradient) => {
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Fill").widget_holder(),
|
||||
ColorInput::new(FillChoice::Gradient(gradient.stops))
|
||||
.disabled(true)
|
||||
.menu_direction(Some(MenuDirection::Top))
|
||||
.widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Fill Gradient Type").widget_holder(),
|
||||
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Fill Gradient Start").widget_holder(),
|
||||
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Fill Gradient Transform").widget_holder(),
|
||||
TextLabel::new(format_transform_matrix(&gradient.transform)).widget_holder(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(stroke) = self.style.stroke.clone() {
|
||||
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke").widget_holder(),
|
||||
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Lengths").widget_holder(),
|
||||
TextLabel::new(if stroke.dash_lengths.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
|
||||
})
|
||||
.widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Offset").widget_holder(),
|
||||
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
|
||||
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
|
||||
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Transform").widget_holder(),
|
||||
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Non-Scaling").widget_holder(),
|
||||
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Paint Order").widget_holder(),
|
||||
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
|
||||
]);
|
||||
}
|
||||
|
||||
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
|
||||
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
|
||||
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]);
|
||||
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Upstream Nested Layers").widget_holder(),
|
||||
TextLabel::new(if self.upstream_nested_layers.is_some() {
|
||||
"Yes (this preserves references to its upstream nested layers for editing by tools)"
|
||||
} else {
|
||||
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
|
||||
})
|
||||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
VectorTableTab::Points => {
|
||||
table_rows.push(column_headings(&["", "position"]));
|
||||
table_rows.extend(
|
||||
self.point_domain
|
||||
.iter()
|
||||
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]),
|
||||
);
|
||||
}
|
||||
VectorTableTab::Segments => {
|
||||
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
|
||||
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).widget_holder(),
|
||||
TextLabel::new(format!("{start}")).widget_holder(),
|
||||
TextLabel::new(format!("{end}")).widget_holder(),
|
||||
TextLabel::new(format!("{handles:?}")).widget_holder(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorTableTab::Regions => {
|
||||
table_rows.push(column_headings(&["", "segment_range", "fill"]));
|
||||
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).widget_holder(),
|
||||
TextLabel::new(format!("{segment_range:?}")).widget_holder(),
|
||||
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Raster<CPU> {
|
||||
fn type_name() -> &'static str {
|
||||
"Raster"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Raster ({}x{})", self.width, self.height)
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
|
||||
use base64::Engine;
|
||||
|
||||
let output = self.data().to_png();
|
||||
let preamble = "data:image/png;base64,";
|
||||
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
||||
base64_string.push_str(preamble);
|
||||
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
||||
base64_string
|
||||
});
|
||||
|
||||
let widgets = vec![ImageLabel::new(base64_string).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Raster<GPU> {
|
||||
fn type_name() -> &'static str {
|
||||
"Raster"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Raster ({}x{})", self.data().width(), self.data().height())
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Color {
|
||||
fn type_name() -> &'static str {
|
||||
"Color"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
|
||||
}
|
||||
fn element_widget(&self, _index: usize) -> WidgetHolder {
|
||||
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![self.element_widget(0)];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Option<Color> {
|
||||
fn type_name() -> &'static str {
|
||||
"Option<Color>"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!(
|
||||
"Option<Color> (#{})",
|
||||
if let Some(color) = self { color.to_linear_srgb().to_rgba_hex_srgb() } else { "None".to_string() }
|
||||
)
|
||||
}
|
||||
fn element_widget(&self, _index: usize) -> WidgetHolder {
|
||||
ColorInput::new(if let Some(color) = self { FillChoice::Solid(*color) } else { FillChoice::None })
|
||||
.disabled(true)
|
||||
.menu_direction(Some(MenuDirection::Top))
|
||||
.widget_holder()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![self.element_widget(0)];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for f64 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (f64)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Number (f64)".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for u32 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (u32)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Number (u32)".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for u64 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (u64)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Number (u64)".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for bool {
|
||||
fn type_name() -> &'static str {
|
||||
"Bool"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Bool".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for String {
|
||||
fn type_name() -> &'static str {
|
||||
"String"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"String".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Option<f64> {
|
||||
fn type_name() -> &'static str {
|
||||
"Option<f64>"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Option<f64>".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for DVec2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Vec2"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Vec2".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for DAffine2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Transform"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Transform".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
fn format_transform_matrix(transform: &DAffine2) -> String {
|
||||
let (scale, angle, translation) = transform.to_scale_angle_translation();
|
||||
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
|
||||
format!(
|
||||
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
|
||||
round(translation.x),
|
||||
round(translation.y),
|
||||
round(scale.x),
|
||||
round(scale.y)
|
||||
)
|
||||
}
|
||||
|
||||
fn format_dvec2(value: DVec2) -> String {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
format!("({} px, {} px)", round(value.x), round(value.y))
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
mod data_panel_message;
|
||||
mod data_panel_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use data_panel_message::*;
|
||||
#[doc(inline)]
|
||||
pub use data_panel_message_handler::*;
|
||||
|
|
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||
|
||||
use super::utility_types::misc::{GroupFolderType, SnappingState};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
|
|
@ -33,6 +34,8 @@ pub enum DocumentMessage {
|
|||
Overlays(OverlaysMessage),
|
||||
#[child]
|
||||
PropertiesPanel(PropertiesPanelMessage),
|
||||
#[child]
|
||||
DataPanel(DataPanelMessage),
|
||||
|
||||
// Messages
|
||||
AlignSelectedLayers {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
|||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
|
||||
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
|
||||
|
|
@ -18,6 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
|
|||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
|
||||
use crate::messages::portfolio::utility_types::PanelType;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
|
||||
|
|
@ -49,6 +51,9 @@ pub struct DocumentMessageContext<'a> {
|
|||
pub current_tool: &'a ToolType,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub device_pixel_ratio: f64,
|
||||
pub data_panel_open: bool,
|
||||
pub layers_panel_open: bool,
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
|
||||
|
|
@ -63,9 +68,11 @@ pub struct DocumentMessageHandler {
|
|||
#[serde(skip)]
|
||||
pub node_graph_handler: NodeGraphMessageHandler,
|
||||
#[serde(skip)]
|
||||
overlays_message_handler: OverlaysMessageHandler,
|
||||
pub overlays_message_handler: OverlaysMessageHandler,
|
||||
#[serde(skip)]
|
||||
properties_panel_message_handler: PropertiesPanelMessageHandler,
|
||||
pub properties_panel_message_handler: PropertiesPanelMessageHandler,
|
||||
#[serde(skip)]
|
||||
pub data_panel_message_handler: DataPanelMessageHandler,
|
||||
|
||||
// ============================================
|
||||
// Fields that are saved in the document format
|
||||
|
|
@ -144,6 +151,7 @@ impl Default for DocumentMessageHandler {
|
|||
node_graph_handler: NodeGraphMessageHandler::default(),
|
||||
overlays_message_handler: OverlaysMessageHandler::default(),
|
||||
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
|
||||
data_panel_message_handler: DataPanelMessageHandler::default(),
|
||||
// ============================================
|
||||
// Fields that are saved in the document format
|
||||
// ============================================
|
||||
|
|
@ -186,6 +194,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
current_tool,
|
||||
preferences,
|
||||
device_pixel_ratio,
|
||||
data_panel_open,
|
||||
layers_panel_open,
|
||||
properties_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
|
|
@ -223,9 +234,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
document_name: self.name.as_str(),
|
||||
executor,
|
||||
persistent_data,
|
||||
properties_panel_open,
|
||||
};
|
||||
self.properties_panel_message_handler.process_message(message, responses, context);
|
||||
}
|
||||
DocumentMessage::DataPanel(message) => {
|
||||
self.data_panel_message_handler.process_message(
|
||||
message,
|
||||
responses,
|
||||
DataPanelMessageContext {
|
||||
network_interface: &mut self.network_interface,
|
||||
data_panel_open,
|
||||
},
|
||||
);
|
||||
}
|
||||
DocumentMessage::NodeGraph(message) => {
|
||||
self.node_graph_handler.process_message(
|
||||
message,
|
||||
|
|
@ -241,6 +263,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
|
||||
navigation_handler: &self.navigation_handler,
|
||||
preferences,
|
||||
layers_panel_open,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -356,12 +379,15 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses),
|
||||
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
|
||||
DocumentMessage::DocumentStructureChanged => {
|
||||
self.update_layers_panel_control_bar_widgets(responses);
|
||||
self.update_layers_panel_bottom_bar_widgets(responses);
|
||||
if layers_panel_open {
|
||||
self.network_interface.load_structure();
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
|
||||
self.network_interface.load_structure();
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
||||
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
||||
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
}
|
||||
}
|
||||
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
|
||||
if !overlay_context.visibility_settings.artboard_name() {
|
||||
|
|
@ -1128,7 +1154,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
}
|
||||
}
|
||||
DocumentMessage::SetActivePanel { active_panel: panel } => {
|
||||
use crate::messages::portfolio::utility_types::PanelType;
|
||||
match panel {
|
||||
PanelType::Document => {
|
||||
if self.graph_view_overlay_open {
|
||||
|
|
@ -2549,7 +2574,11 @@ impl DocumentMessageHandler {
|
|||
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn update_layers_panel_control_bar_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||
pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
|
||||
if !layers_panel_open {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode).
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
|
||||
|
|
@ -2707,7 +2736,11 @@ impl DocumentMessageHandler {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||
pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
|
||||
if !layers_panel_open {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
let mut selected_layers = selected_nodes.selected_layers(self.metadata());
|
||||
let selected_layer = selected_layers.next();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod document_message;
|
||||
mod document_message_handler;
|
||||
|
||||
pub mod data_panel;
|
||||
pub mod graph_operation;
|
||||
pub mod navigation;
|
||||
pub mod node_graph;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> {
|
|||
pub graph_fade_artwork_percentage: f64,
|
||||
pub navigation_handler: &'a NavigationMessageHandler,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub layers_panel_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ExtractField)]
|
||||
|
|
@ -111,6 +112,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
graph_fade_artwork_percentage,
|
||||
navigation_handler,
|
||||
preferences,
|
||||
layers_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
|
|
@ -155,11 +157,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
}
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(NodeGraphMessage::SendSelectedNodes);
|
||||
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
responses.add(PortfolioMessage::SubmitActiveGraphRender);
|
||||
}
|
||||
NodeGraphMessage::CreateWire { output_connector, input_connector } => {
|
||||
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
|
||||
|
|
@ -1215,7 +1219,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
{
|
||||
return None;
|
||||
}
|
||||
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
|
||||
|
||||
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
||||
|
||||
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
|
||||
|
|
@ -1485,7 +1489,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
};
|
||||
selected_nodes.set_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
|
||||
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
|
||||
|
|
@ -1873,7 +1876,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||
}
|
||||
|
||||
NodeGraphMessage::UpdateLayerPanel => {
|
||||
Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses);
|
||||
Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses);
|
||||
}
|
||||
NodeGraphMessage::UpdateEdges => {
|
||||
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
|
||||
|
|
@ -2329,9 +2332,9 @@ impl NodeGraphMessageHandler {
|
|||
.icon(Some("Node".to_string()))
|
||||
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||
.popover_layout({
|
||||
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface);
|
||||
let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface);
|
||||
let compatible_type = {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, &context.network_interface);
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
|
||||
let node_type = graph_layer.horizontal_layer_flow().nth(1);
|
||||
if let Some(node_id) = node_type {
|
||||
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]);
|
||||
|
|
@ -2585,7 +2588,11 @@ impl NodeGraphMessageHandler {
|
|||
Some(subgraph_names)
|
||||
}
|
||||
|
||||
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
|
||||
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
|
||||
if !layers_panel_open {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_layers = network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers(network_interface.document_metadata())
|
||||
|
|
@ -2668,7 +2675,7 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
// A wire is in progress and its start and end connectors are set
|
||||
let wiring = self.wire_in_progress_from_connector.is_some();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> {
|
|||
pub document_name: &'a str,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
|
|
@ -28,16 +29,22 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
|||
document_name,
|
||||
executor,
|
||||
persistent_data,
|
||||
properties_panel_open,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
PropertiesPanelMessage::Clear => {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
layout_target: LayoutTarget::PropertiesPanel,
|
||||
});
|
||||
}
|
||||
PropertiesPanelMessage::Refresh => {
|
||||
if !properties_panel_open {
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut node_properties_context = NodePropertiesContext {
|
||||
persistent_data,
|
||||
responses,
|
||||
|
|
@ -50,7 +57,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
|||
|
||||
node_properties_context.responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
layout_target: LayoutTarget::PropertiesPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ pub struct MenuBarMessageHandler {
|
|||
pub has_selected_nodes: bool,
|
||||
pub has_selected_layers: bool,
|
||||
pub has_selection_history: (bool, bool),
|
||||
pub spreadsheet_view_open: bool,
|
||||
pub message_logging_verbosity: MessageLoggingVerbosity,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
pub make_path_editable_is_allowed: bool,
|
||||
pub data_panel_open: bool,
|
||||
pub layers_panel_open: bool,
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
|
|
@ -585,18 +587,40 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
}],
|
||||
]),
|
||||
),
|
||||
MenuBarEntry::new_root(
|
||||
"Window".into(),
|
||||
false,
|
||||
MenuBarEntryChildren(vec![
|
||||
vec![
|
||||
MenuBarEntry {
|
||||
label: "Properties".into(),
|
||||
icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen),
|
||||
action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()),
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
MenuBarEntry {
|
||||
label: "Layers".into(),
|
||||
icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen),
|
||||
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
],
|
||||
vec![MenuBarEntry {
|
||||
label: "Window: Spreadsheet".into(),
|
||||
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()),
|
||||
disabled: no_active_document,
|
||||
label: "Data".into(),
|
||||
icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
|
||||
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen),
|
||||
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
|
||||
..MenuBarEntry::default()
|
||||
}],
|
||||
]),
|
||||
),
|
||||
MenuBarEntry::new_root(
|
||||
"Help".into(),
|
||||
true,
|
||||
false,
|
||||
MenuBarEntryChildren(vec![
|
||||
vec![MenuBarEntry {
|
||||
label: "About Graphite…".into(),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ mod portfolio_message_handler;
|
|||
pub mod document;
|
||||
pub mod document_migration;
|
||||
pub mod menu_bar;
|
||||
pub mod spreadsheet;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ pub enum PortfolioMessage {
|
|||
MenuBar(MenuBarMessage),
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
#[child]
|
||||
Spreadsheet(SpreadsheetMessage),
|
||||
|
||||
// Messages
|
||||
Init,
|
||||
|
|
@ -128,6 +126,9 @@ pub enum PortfolioMessage {
|
|||
document_id: DocumentId,
|
||||
ignore_hash: bool,
|
||||
},
|
||||
ToggleDataPanelOpen,
|
||||
TogglePropertiesPanelOpen,
|
||||
ToggleLayersPanelOpen,
|
||||
ToggleRulers,
|
||||
UpdateDocumentWidgets,
|
||||
UpdateOpenDocumentsList,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::spreadsheet::SpreadsheetMessageHandler;
|
||||
use super::utility_types::{PanelType, PersistentData};
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
|
||||
|
|
@ -24,6 +23,7 @@ use crate::messages::tool::common_functionality::utility_functions::make_path_ed
|
|||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
use bezier_rs::BezierHandles;
|
||||
use derivative::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
|
|
@ -37,14 +37,15 @@ use std::vec;
|
|||
pub struct PortfolioMessageContext<'a> {
|
||||
pub ipp: &'a InputPreprocessorMessageHandler,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub animation: &'a AnimationMessageHandler,
|
||||
pub current_tool: &'a ToolType,
|
||||
pub message_logging_verbosity: MessageLoggingVerbosity,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
pub timing_information: TimingInformation,
|
||||
pub animation: &'a AnimationMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ExtractField)]
|
||||
#[derive(Debug, Derivative, ExtractField)]
|
||||
#[derivative(Default)]
|
||||
pub struct PortfolioMessageHandler {
|
||||
menu_bar_message_handler: MenuBarMessageHandler,
|
||||
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
|
||||
|
|
@ -55,10 +56,13 @@ pub struct PortfolioMessageHandler {
|
|||
pub persistent_data: PersistentData,
|
||||
pub executor: NodeGraphExecutor,
|
||||
pub selection_mode: SelectionMode,
|
||||
/// The spreadsheet UI allows for graph data to be previewed.
|
||||
pub spreadsheet: SpreadsheetMessageHandler,
|
||||
device_pixel_ratio: Option<f64>,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
pub data_panel_open: bool,
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub layers_panel_open: bool,
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
|
|
@ -67,11 +71,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
let PortfolioMessageContext {
|
||||
ipp,
|
||||
preferences,
|
||||
animation,
|
||||
current_tool,
|
||||
message_logging_verbosity,
|
||||
reset_node_definitions_on_open,
|
||||
timing_information,
|
||||
animation,
|
||||
} = context;
|
||||
|
||||
match message {
|
||||
|
|
@ -86,7 +90,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
self.menu_bar_message_handler.has_selected_layers = false;
|
||||
self.menu_bar_message_handler.has_selection_history = (false, false);
|
||||
self.menu_bar_message_handler.make_path_editable_is_allowed = false;
|
||||
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
|
||||
self.menu_bar_message_handler.data_panel_open = self.data_panel_open;
|
||||
self.menu_bar_message_handler.layers_panel_open = self.layers_panel_open;
|
||||
self.menu_bar_message_handler.properties_panel_open = self.properties_panel_open;
|
||||
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
|
||||
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
|
||||
|
||||
|
|
@ -108,9 +114,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
self.menu_bar_message_handler.process_message(message, responses, ());
|
||||
}
|
||||
PortfolioMessage::Spreadsheet(message) => {
|
||||
self.spreadsheet.process_message(message, responses, ());
|
||||
}
|
||||
PortfolioMessage::Document(message) => {
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
if let Some(document) = self.documents.get_mut(&document_id) {
|
||||
|
|
@ -122,6 +125,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
current_tool,
|
||||
preferences,
|
||||
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
|
||||
data_panel_open: self.data_panel_open,
|
||||
layers_panel_open: self.layers_panel_open,
|
||||
properties_panel_open: self.properties_panel_open,
|
||||
};
|
||||
document.process_message(message, responses, document_inputs)
|
||||
}
|
||||
|
|
@ -156,6 +162,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
current_tool,
|
||||
preferences,
|
||||
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
|
||||
data_panel_open: self.data_panel_open,
|
||||
layers_panel_open: self.layers_panel_open,
|
||||
properties_panel_open: self.properties_panel_open,
|
||||
};
|
||||
document.process_message(message, responses, document_inputs)
|
||||
}
|
||||
|
|
@ -201,6 +210,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
// Clear relevant UI layouts if there are no documents
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
|
@ -225,6 +235,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
// Clear UI layouts that assume the existence of a document
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
|
@ -344,13 +355,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
self.persistent_data.font_cache.insert(font, preview_url, data);
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
for document_id in self.document_ids.iter() {
|
||||
let inspect_node = self.inspect_node_id();
|
||||
let node_to_inspect = self.node_to_inspect();
|
||||
if let Ok(message) = self.executor.submit_node_graph_evaluation(
|
||||
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
||||
*document_id,
|
||||
ipp.viewport_bounds.size().as_uvec2(),
|
||||
timing_information,
|
||||
inspect_node,
|
||||
node_to_inspect,
|
||||
true,
|
||||
) {
|
||||
responses.add_front(message);
|
||||
|
|
@ -388,7 +399,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
|
||||
}
|
||||
|
||||
self.load_document(new_document, document_id, &mut new_responses, false);
|
||||
self.load_document(new_document, document_id, self.layers_panel_open, &mut new_responses, false);
|
||||
new_responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
new_responses.extend(responses.drain(..));
|
||||
*responses = new_responses;
|
||||
|
|
@ -504,7 +515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document.set_save_state(document_is_saved);
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, responses, to_front);
|
||||
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
||||
}
|
||||
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
|
||||
let mut all_new_ids = Vec::new();
|
||||
|
|
@ -918,13 +929,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
|
||||
let inspect_node = self.inspect_node_id();
|
||||
let node_to_inspect = self.node_to_inspect();
|
||||
let result = self.executor.submit_node_graph_evaluation(
|
||||
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
|
||||
document_id,
|
||||
ipp.viewport_bounds.size().as_uvec2(),
|
||||
timing_information,
|
||||
inspect_node,
|
||||
node_to_inspect,
|
||||
ignore_hash,
|
||||
);
|
||||
|
||||
|
|
@ -938,6 +949,58 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
Ok(message) => responses.add_front(message),
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ToggleDataPanelOpen => {
|
||||
self.data_panel_open = !self.data_panel_open;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
|
||||
// Run the graph to grab the data
|
||||
if self.data_panel_open {
|
||||
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
|
||||
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
} else {
|
||||
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the data panel next time it is opened
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
|
||||
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
|
||||
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
|
||||
}
|
||||
}
|
||||
PortfolioMessage::TogglePropertiesPanelOpen => {
|
||||
self.properties_panel_open = !self.properties_panel_open;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
|
||||
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
|
||||
|
||||
// Run the graph to grab the data
|
||||
if self.properties_panel_open {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
PortfolioMessage::ToggleLayersPanelOpen => {
|
||||
self.layers_panel_open = !self.layers_panel_open;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
|
||||
// Run the graph to grab the data
|
||||
if self.layers_panel_open {
|
||||
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
|
||||
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
|
||||
});
|
||||
} else {
|
||||
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
|
||||
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
|
||||
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ToggleRulers => {
|
||||
if let Some(document) = self.active_document_mut() {
|
||||
document.rulers_visible = !document.rulers_visible;
|
||||
|
|
@ -987,6 +1050,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
PasteIntoFolder,
|
||||
PrevDocument,
|
||||
ToggleRulers,
|
||||
ToggleDataPanelOpen,
|
||||
);
|
||||
|
||||
// Extend with actions that require an active document
|
||||
|
|
@ -1056,14 +1120,14 @@ impl PortfolioMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
|
||||
if to_front {
|
||||
self.document_ids.push_front(document_id);
|
||||
} else {
|
||||
self.document_ids.push_back(document_id);
|
||||
}
|
||||
new_document.update_layers_panel_control_bar_widgets(responses);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(responses);
|
||||
new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
|
|
@ -1111,17 +1175,17 @@ impl PortfolioMessageHandler {
|
|||
result
|
||||
}
|
||||
|
||||
/// Get the id of the node that should be used as the target for the spreadsheet
|
||||
pub fn inspect_node_id(&self) -> Option<NodeId> {
|
||||
// Spreadsheet not open, skipping
|
||||
if !self.spreadsheet.spreadsheet_view_open {
|
||||
/// Get the ID of the selected node that should be used as the current source for the Data panel.
|
||||
pub fn node_to_inspect(&self) -> Option<NodeId> {
|
||||
// Skip if the Data panel is not open
|
||||
if !self.data_panel_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let document = self.documents.get(&self.active_document_id?)?;
|
||||
let selected_nodes = document.network_interface.selected_nodes().0;
|
||||
|
||||
// Selected nodes != 1, skipping
|
||||
// Skip if there is not exactly one selected node
|
||||
if selected_nodes.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
mod spreadsheet_message;
|
||||
mod spreadsheet_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use spreadsheet_message::*;
|
||||
#[doc(inline)]
|
||||
pub use spreadsheet_message_handler::*;
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
use super::VectorDomain;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Context;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::{Artboard, Graphic};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The spreadsheet UI allows for graph data to be previewed.
|
||||
#[derive(Default, Debug, Clone, ExtractField)]
|
||||
pub struct SpreadsheetMessageHandler {
|
||||
/// Sets whether or not the spreadsheet is drawn.
|
||||
pub spreadsheet_view_open: bool,
|
||||
inspect_node: Option<NodeId>,
|
||||
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
|
||||
element_path: Vec<usize>,
|
||||
viewing_vector_domain: VectorDomain,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<SpreadsheetMessage, ()> for SpreadsheetMessageHandler {
|
||||
fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
SpreadsheetMessage::ToggleOpen => {
|
||||
self.spreadsheet_view_open = !self.spreadsheet_view_open;
|
||||
// Run the graph to grab the data
|
||||
if self.spreadsheet_view_open {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
// Update checked UI state for open
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
self.update_layout(responses);
|
||||
}
|
||||
|
||||
SpreadsheetMessage::UpdateLayout { mut inspect_result } => {
|
||||
self.inspect_node = Some(inspect_result.inspect_node);
|
||||
self.introspected_data = inspect_result.take_data();
|
||||
self.update_layout(responses)
|
||||
}
|
||||
|
||||
SpreadsheetMessage::PushToElementPath { index } => {
|
||||
self.element_path.push(index);
|
||||
self.update_layout(responses);
|
||||
}
|
||||
SpreadsheetMessage::TruncateElementPath { len } => {
|
||||
self.element_path.truncate(len);
|
||||
self.update_layout(responses);
|
||||
}
|
||||
|
||||
SpreadsheetMessage::ViewVectorDomain { domain } => {
|
||||
self.viewing_vector_domain = domain;
|
||||
self.update_layout(responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(SpreadsheetMessage;)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpreadsheetMessageHandler {
|
||||
fn update_layout(&mut self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateSpreadsheetState {
|
||||
node: self.inspect_node,
|
||||
open: self.spreadsheet_view_open,
|
||||
});
|
||||
if !self.spreadsheet_view_open {
|
||||
return;
|
||||
}
|
||||
let mut layout_data = LayoutData {
|
||||
current_depth: 0,
|
||||
desired_path: &mut self.element_path,
|
||||
breadcrumbs: Vec::new(),
|
||||
vector_domain: self.viewing_vector_domain,
|
||||
};
|
||||
let mut layout = self
|
||||
.introspected_data
|
||||
.as_ref()
|
||||
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data))
|
||||
.unwrap_or_else(|| Some(label("No data")))
|
||||
.unwrap_or_else(|| label("Failed to downcast data"));
|
||||
|
||||
if layout_data.breadcrumbs.len() > 1 {
|
||||
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
|
||||
.on_update(|&len| SpreadsheetMessage::TruncateElementPath { len: len as usize }.into())
|
||||
.widget_holder();
|
||||
layout.insert(0, LayoutGroup::Row { widgets: vec![breadcrumb] });
|
||||
}
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout { layout }),
|
||||
layout_target: LayoutTarget::Spreadsheet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct LayoutData<'a> {
|
||||
current_depth: usize,
|
||||
desired_path: &'a mut Vec<usize>,
|
||||
breadcrumbs: Vec<String>,
|
||||
vector_domain: VectorDomain,
|
||||
}
|
||||
|
||||
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
||||
// We simply try random types. TODO: better strategy.
|
||||
#[allow(clippy::manual_map)]
|
||||
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Artboard>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Vector>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Graphic>>>() {
|
||||
Some(io.output.layout_with_breadcrumb(data))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
|
||||
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
|
||||
}
|
||||
|
||||
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
|
||||
let error = vec![TextLabel::new(x).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets: error }]
|
||||
}
|
||||
|
||||
trait TableRowLayout {
|
||||
fn type_name() -> &'static str;
|
||||
fn identifier(&self) -> String;
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
data.breadcrumbs.push(self.identifier());
|
||||
self.compute_layout(data)
|
||||
}
|
||||
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup>;
|
||||
}
|
||||
|
||||
impl TableRowLayout for Graphic {
|
||||
fn type_name() -> &'static str {
|
||||
"Graphic"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
match self {
|
||||
Self::Graphic(graphic) => graphic.identifier(),
|
||||
Self::Vector(vector) => vector.identifier(),
|
||||
Self::RasterCPU(_) => "Raster (on CPU)".to_string(),
|
||||
Self::RasterGPU(_) => "Raster (on GPU)".to_string(),
|
||||
Self::Color(_) => "Color".to_string(),
|
||||
}
|
||||
}
|
||||
// Don't put a breadcrumb for Graphic
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.compute_layout(data)
|
||||
}
|
||||
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
match self {
|
||||
Self::Graphic(table) => table.layout_with_breadcrumb(data),
|
||||
Self::Vector(table) => table.layout_with_breadcrumb(data),
|
||||
Self::RasterCPU(_) => label("Raster is not supported"),
|
||||
Self::RasterGPU(_) => label("Raster is not supported"),
|
||||
Self::Color(color) => {
|
||||
let rows = vec![vec![
|
||||
TextLabel::new(format!("Colors:\n{}", color.iter().map(|color| color.element.to_rgba_hex_srgb()).collect::<Vec<_>>().join("\n"))).widget_holder(),
|
||||
]];
|
||||
vec![LayoutGroup::Table { rows }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Vector {
|
||||
fn type_name() -> &'static str {
|
||||
"Vector"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!(
|
||||
"Vector ({} point{}, {} segment{})",
|
||||
self.point_domain.ids().len(),
|
||||
if self.point_domain.ids().len() == 1 { "" } else { "s" },
|
||||
self.segment_domain.ids().len(),
|
||||
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
|
||||
)
|
||||
}
|
||||
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
|
||||
let colinear = if colinear.is_empty() { "None" } else { &colinear };
|
||||
let style = vec![
|
||||
TextLabel::new(format!(
|
||||
"{}\n\nColinear Handle IDs: {}\nPreserves Reference to Upstream Nested Layers for Editing by Tools: {}",
|
||||
self.style,
|
||||
colinear,
|
||||
if self.upstream_nested_layers.is_some() { "Yes" } else { "No" }
|
||||
))
|
||||
.multiline(true)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let domain_entries = [VectorDomain::Points, VectorDomain::Segments, VectorDomain::Regions]
|
||||
.into_iter()
|
||||
.map(|domain| {
|
||||
RadioEntryData::new(format!("{domain:?}"))
|
||||
.label(format!("{domain:?}"))
|
||||
.on_update(move |_| SpreadsheetMessage::ViewVectorDomain { domain }.into())
|
||||
})
|
||||
.collect();
|
||||
let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_domain as u32)).widget_holder()];
|
||||
|
||||
let mut table_rows = Vec::new();
|
||||
match data.vector_domain {
|
||||
VectorDomain::Points => {
|
||||
table_rows.push(column_headings(&["", "position"]));
|
||||
table_rows.extend(
|
||||
self.point_domain
|
||||
.iter()
|
||||
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]),
|
||||
);
|
||||
}
|
||||
VectorDomain::Segments => {
|
||||
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
|
||||
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).widget_holder(),
|
||||
TextLabel::new(format!("{}", start)).widget_holder(),
|
||||
TextLabel::new(format!("{}", end)).widget_holder(),
|
||||
TextLabel::new(format!("{:?}", handles)).widget_holder(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorDomain::Regions => {
|
||||
table_rows.push(column_headings(&["", "segment_range", "fill"]));
|
||||
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).widget_holder(),
|
||||
TextLabel::new(format!("{:?}", segment_range)).widget_holder(),
|
||||
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Image<Color> {
|
||||
fn type_name() -> &'static str {
|
||||
"Image"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Image ({}x{})", self.width, self.height)
|
||||
}
|
||||
fn compute_layout(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let rows = vec![vec![TextLabel::new(format!("Image ({}x{})", self.width, self.height)).widget_holder()]];
|
||||
vec![LayoutGroup::Table { rows }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Artboard {
|
||||
fn type_name() -> &'static str {
|
||||
"Artboard"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.label.clone()
|
||||
}
|
||||
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.content.compute_layout(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TableRowLayout> TableRowLayout for Table<T> {
|
||||
fn type_name() -> &'static str {
|
||||
"Table"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
|
||||
}
|
||||
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
|
||||
if let Some(row) = self.get(index) {
|
||||
data.current_depth += 1;
|
||||
let result = row.element.layout_with_breadcrumb(data);
|
||||
data.current_depth -= 1;
|
||||
return result;
|
||||
} else {
|
||||
warn!("Desired path truncated");
|
||||
data.desired_path.truncate(data.current_depth);
|
||||
}
|
||||
}
|
||||
|
||||
let mut rows = self
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| {
|
||||
let (scale, angle, translation) = row.transform.to_scale_angle_translation();
|
||||
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
vec![
|
||||
TextLabel::new(format!("{index}")).widget_holder(),
|
||||
TextButton::new(row.element.identifier())
|
||||
.on_update(move |_| SpreadsheetMessage::PushToElementPath { index }.into())
|
||||
.widget_holder(),
|
||||
TextLabel::new(format!(
|
||||
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
|
||||
round(translation.x),
|
||||
round(translation.y),
|
||||
round(scale.x),
|
||||
round(scale.y)
|
||||
))
|
||||
.widget_holder(),
|
||||
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
|
||||
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
|
||||
|
||||
vec![LayoutGroup::Table { rows }]
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ pub enum PanelType {
|
|||
Document,
|
||||
Layers,
|
||||
Properties,
|
||||
Spreadsheet,
|
||||
DataPanel,
|
||||
}
|
||||
|
||||
impl From<String> for PanelType {
|
||||
|
|
@ -52,7 +52,7 @@ impl From<String> for PanelType {
|
|||
"Document" => PanelType::Document,
|
||||
"Layers" => PanelType::Layers,
|
||||
"Properties" => PanelType::Properties,
|
||||
"Spreadsheet" => PanelType::Spreadsheet,
|
||||
"Data" => PanelType::DataPanel,
|
||||
_ => panic!("Unknown panel type: {value}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi
|
|||
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
|
||||
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
|
||||
pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler};
|
||||
pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant};
|
||||
pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler};
|
||||
pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler};
|
||||
pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler};
|
||||
|
|
@ -24,12 +25,10 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
|
|||
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
|
||||
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
|
||||
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
|
||||
pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant};
|
||||
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
|
||||
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
|
||||
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
|
||||
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
|
||||
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
|
||||
|
||||
// Message, MessageDiscriminant
|
||||
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
mod workspace_message;
|
||||
mod workspace_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use workspace_message::{WorkspaceMessage, WorkspaceMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use workspace_message_handler::WorkspaceMessageHandler;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Workspace)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum WorkspaceMessage {
|
||||
// Messages
|
||||
NodeGraphToggleVisibility,
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct WorkspaceMessageHandler {
|
||||
node_graph_visible: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<WorkspaceMessage, ()> for WorkspaceMessageHandler {
|
||||
fn process_message(&mut self, message: WorkspaceMessage, _responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
// Messages
|
||||
WorkspaceMessage::NodeGraphToggleVisibility => {
|
||||
self.node_graph_visible = !self.node_graph_visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(WorkspaceMessageDiscriminant;
|
||||
NodeGraphToggleVisibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ pub struct NodeGraphExecutor {
|
|||
current_execution_id: u64,
|
||||
futures: HashMap<u64, ExecutionContext>,
|
||||
node_graph_hash: u64,
|
||||
old_inspect_node: Option<NodeId>,
|
||||
previous_node_to_inspect: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -80,7 +80,7 @@ impl NodeGraphExecutor {
|
|||
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
|
||||
node_graph_hash: 0,
|
||||
current_execution_id: 0,
|
||||
old_inspect_node: None,
|
||||
previous_node_to_inspect: None,
|
||||
};
|
||||
(node_runtime, node_executor)
|
||||
}
|
||||
|
|
@ -113,22 +113,22 @@ impl NodeGraphExecutor {
|
|||
let instrumented = Instrumented::new(&mut network);
|
||||
|
||||
self.runtime_io
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None }))
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(instrumented)
|
||||
}
|
||||
|
||||
/// Update the cached network if necessary.
|
||||
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
|
||||
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
|
||||
let network_hash = document.network_interface.document_network().current_hash();
|
||||
// Refresh the graph when it changes or the inspect node changes
|
||||
if network_hash != self.node_graph_hash || self.old_inspect_node != inspect_node || ignore_hash {
|
||||
if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash {
|
||||
let network = document.network_interface.document_network().clone();
|
||||
self.old_inspect_node = inspect_node;
|
||||
self.previous_node_to_inspect = node_to_inspect;
|
||||
self.node_graph_hash = network_hash;
|
||||
|
||||
self.runtime_io
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node }))
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect }))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -173,10 +173,10 @@ impl NodeGraphExecutor {
|
|||
document_id: DocumentId,
|
||||
viewport_resolution: UVec2,
|
||||
time: TimingInformation,
|
||||
inspect_node: Option<NodeId>,
|
||||
node_to_inspect: Option<NodeId>,
|
||||
ignore_hash: bool,
|
||||
) -> Result<Message, String> {
|
||||
self.update_node_graph(document, inspect_node, ignore_hash)?;
|
||||
self.update_node_graph(document, node_to_inspect, ignore_hash)?;
|
||||
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, time)
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ impl NodeGraphExecutor {
|
|||
|
||||
// Execute the node graph
|
||||
self.runtime_io
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None }))
|
||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let execution_id = self.queue_execution(render_config);
|
||||
let execution_context = ExecutionContext {
|
||||
|
|
@ -293,11 +293,11 @@ impl NodeGraphExecutor {
|
|||
}
|
||||
responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
|
||||
|
||||
// Update the spreadsheet on the frontend using the value of the inspect result.
|
||||
if self.old_inspect_node.is_some() {
|
||||
if let Some(inspect_result) = inspect_result {
|
||||
responses.add(SpreadsheetMessage::UpdateLayout { inspect_result });
|
||||
}
|
||||
// Update the Data panel on the frontend using the value of the inspect result.
|
||||
if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() {
|
||||
responses.add(DataPanelMessage::UpdateLayout { inspect_result });
|
||||
} else {
|
||||
responses.add(DataPanelMessage::ClearLayout);
|
||||
}
|
||||
}
|
||||
NodeGraphUpdate::CompilationResponse(execution_response) => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ pub struct NodeRuntime {
|
|||
node_graph_errors: GraphErrors,
|
||||
monitor_nodes: Vec<Vec<NodeId>>,
|
||||
|
||||
/// Which node is inspected and which monitor node is used (if any) for the current execution
|
||||
/// Which node is inspected and which monitor node is used (if any) for the current execution.
|
||||
inspect_state: Option<InspectState>,
|
||||
|
||||
/// Mapping of the fully-qualified node paths to their preprocessor substitutions.
|
||||
|
|
@ -69,7 +69,7 @@ pub enum GraphRuntimeRequest {
|
|||
pub struct GraphUpdate {
|
||||
pub(super) network: NodeNetwork,
|
||||
/// The node that should be temporary inspected during execution
|
||||
pub(super) inspect_node: Option<NodeId>,
|
||||
pub(super) node_to_inspect: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -190,18 +190,19 @@ impl NodeRuntime {
|
|||
let _ = self.update_network(graph).await;
|
||||
}
|
||||
}
|
||||
GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, inspect_node }) => {
|
||||
GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, node_to_inspect }) => {
|
||||
// Insert the monitor node to manage the inspection
|
||||
self.inspect_state = inspect_node.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
|
||||
self.inspect_state = node_to_inspect.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
|
||||
|
||||
self.old_graph = Some(network.clone());
|
||||
|
||||
self.node_graph_errors.clear();
|
||||
let result = self.update_network(network).await;
|
||||
let node_graph_errors = self.node_graph_errors.clone();
|
||||
|
||||
self.update_thumbnails = true;
|
||||
self.sender.send_generation_response(CompilationResponse {
|
||||
result,
|
||||
node_graph_errors: self.node_graph_errors.clone(),
|
||||
});
|
||||
|
||||
self.sender.send_generation_response(CompilationResponse { result, node_graph_errors });
|
||||
}
|
||||
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => {
|
||||
let result = self.execute_network(render_config).await;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
export let colorOrGradient: FillChoice;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let disabled = false;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||
export let open: boolean;
|
||||
|
|
@ -133,6 +134,8 @@
|
|||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
const target = (e.target || undefined) as HTMLElement | undefined;
|
||||
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
||||
|
||||
|
|
@ -403,7 +406,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<LayoutRow
|
||||
styles={{
|
||||
"--new-color": newColor.toHexOptionalAlpha(),
|
||||
|
|
@ -418,7 +421,7 @@
|
|||
>
|
||||
<LayoutCol class="pickers-and-gradient">
|
||||
<LayoutRow class="pickers">
|
||||
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker>
|
||||
<LayoutCol class="saturation-value-picker" title={disabled ? "Saturation and value (disabled)" : "Saturation and value"} on:pointerdown={onPointerDown} data-saturation-value-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
{/if}
|
||||
|
|
@ -432,12 +435,12 @@
|
|||
/>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
||||
<LayoutCol class="hue-picker" title={disabled ? "Hue (disabled)" : "Hue"} on:pointerdown={onPointerDown} data-hue-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
|
||||
<LayoutCol class="alpha-picker" title={disabled ? "Alpha (disabled)" : "Alpha"} on:pointerdown={onPointerDown} data-alpha-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
||||
{/if}
|
||||
|
|
@ -447,6 +450,7 @@
|
|||
<LayoutRow class="gradient">
|
||||
<SpectrumInput
|
||||
{gradient}
|
||||
{disabled}
|
||||
on:gradient={() => {
|
||||
gradient = gradient;
|
||||
if (gradient) dispatch("colorOrGradient", gradient);
|
||||
|
|
@ -459,6 +463,7 @@
|
|||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
<NumberInput
|
||||
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||
}}
|
||||
|
|
@ -478,7 +483,7 @@
|
|||
styles={{ "--outline-amount": outlineFactor }}
|
||||
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
|
||||
>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
{#if !newColor.equals(oldColor) && !disabled}
|
||||
<div class="swap-button-background"></div>
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
|
||||
{/if}
|
||||
|
|
@ -500,6 +505,7 @@
|
|||
<LayoutRow>
|
||||
<TextInput
|
||||
value={newColor.toHexOptionalAlpha() || "-"}
|
||||
{disabled}
|
||||
on:commitText={({ detail }) => {
|
||||
dispatch("startHistoryTransaction");
|
||||
setColorCode(detail);
|
||||
|
|
@ -520,6 +526,7 @@
|
|||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
setColorRGB(channel, detail);
|
||||
|
|
@ -547,6 +554,7 @@
|
|||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
setColorHSV(channel, detail);
|
||||
|
|
@ -573,6 +581,7 @@
|
|||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (detail !== undefined) alpha = detail / 100;
|
||||
setColorAlphaPercent(detail);
|
||||
|
|
@ -593,14 +602,14 @@
|
|||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#if allowNone && !gradient}
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<button class="preset-color none" {disabled} on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
{/if}
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<button class="preset-color black" {disabled} on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<button class="preset-color white" {disabled} on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<button class="preset-color pure" {disabled} on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
|
||||
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
|
||||
|
|
@ -609,7 +618,7 @@
|
|||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
|
||||
</button>
|
||||
<Separator type="Related" />
|
||||
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
|
@ -954,7 +963,7 @@
|
|||
// For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors.
|
||||
// We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover.
|
||||
background: var(--pure-color-gray);
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
|
|
@ -963,5 +972,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled .pickers-and-gradient .pickers :is(.saturation-value-picker, .hue-picker, .alpha-picker),
|
||||
&.disabled .details .preset-color,
|
||||
&.disabled .details .choice-preview {
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled .details .preset-color.pure:hover div {
|
||||
background: var(--pure-color-gray);
|
||||
}
|
||||
}
|
||||
|
||||
// paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDataPanelLayout } from "@graphite/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let dataPanelLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDataPanelLayout, (updateDataPanelLayout) => {
|
||||
patchWidgetLayout(dataPanelLayout, updateDataPanelLayout);
|
||||
dataPanelLayout = dataPanelLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDataPanelLayout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutCol class="data-panel">
|
||||
<LayoutCol class="body" scrollableY={true}>
|
||||
<WidgetLayout layout={dataPanelLayout} />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.data-panel {
|
||||
flex-grow: 1;
|
||||
padding: 4px;
|
||||
|
||||
table {
|
||||
margin: -4px;
|
||||
width: calc(100% + 2 * 4px);
|
||||
|
||||
.text-label {
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -675,7 +675,7 @@
|
|||
|
||||
&[title^="Coming Soon"] {
|
||||
opacity: 0.25;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
|
@ -825,7 +825,7 @@
|
|||
|
||||
.graph-view {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0;
|
||||
|
||||
&.open {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@
|
|||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);
|
||||
|
||||
removeEventListener("pointermove", clippingHover);
|
||||
removeEventListener("keydown", clippingKeyPress);
|
||||
removeEventListener("keyup", clippingKeyPress);
|
||||
|
|
@ -489,7 +495,9 @@
|
|||
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
|
||||
<LayoutRow class="control-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layersPanelControlBarLeftLayout} />
|
||||
<Separator />
|
||||
{#if layersPanelControlBarLeftLayout?.layout?.length > 0 && layersPanelControlBarRightLayout?.layout?.length > 0}
|
||||
<Separator />
|
||||
{/if}
|
||||
<WidgetLayout layout={layersPanelControlBarRightLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
|
|
@ -605,6 +613,10 @@
|
|||
.widget-span:first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom bar
|
||||
|
|
@ -619,6 +631,10 @@
|
|||
.widget-span > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Layer hierarchy
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertiesPanelLayout } from "@graphite/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let propertiesSectionsLayout = defaultWidgetLayout();
|
||||
let propertiesPanelLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
||||
propertiesSectionsLayout = propertiesSectionsLayout;
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (updatePropertiesPanelLayout) => {
|
||||
patchWidgetLayout(propertiesPanelLayout, updatePropertiesPanelLayout);
|
||||
propertiesPanelLayout = propertiesPanelLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutCol class="properties">
|
||||
<LayoutCol class="sections" scrollableY={true}>
|
||||
<WidgetLayout layout={propertiesSectionsLayout} />
|
||||
<WidgetLayout layout={propertiesPanelLayout} />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts">
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { PortfolioState } from "/src/state-providers/portfolio";
|
||||
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
</script>
|
||||
|
||||
<LayoutCol class="spreadsheet">
|
||||
<LayoutRow class="control-bar">
|
||||
<TextLabel>Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}:</TextLabel>
|
||||
</LayoutRow>
|
||||
<LayoutCol class="body" scrollableY={true}>
|
||||
<WidgetLayout layout={$portfolio.spreadsheetWidgets} />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.spreadsheet {
|
||||
flex-grow: 1;
|
||||
padding: 4px;
|
||||
|
||||
.control-bar {
|
||||
height: 32px;
|
||||
--widget-height: 24px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.text-label {
|
||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||
min-height: var(--widget-height);
|
||||
line-height: var(--widget-height);
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 -4px;
|
||||
width: calc(100% + 2 * 4px);
|
||||
margin-top: 8px;
|
||||
|
||||
.text-label {
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1008,7 +1008,7 @@
|
|||
border-radius: 4px;
|
||||
bottom: calc(100% + 12px);
|
||||
z-index: -1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0.5;
|
||||
|
||||
// Tail
|
||||
|
|
@ -1040,7 +1040,7 @@
|
|||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
opacity 0.2s,
|
||||
z-index 0s 0.2s;
|
||||
|
||||
&::selection {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
|
@ -124,6 +125,10 @@
|
|||
{#if iconLabel}
|
||||
<IconLabel {...exclude(iconLabel)} />
|
||||
{/if}
|
||||
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
|
||||
{#if imageLabel}
|
||||
<ImageLabel {...exclude(imageLabel)} />
|
||||
{/if}
|
||||
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
|
||||
{#if imageButton}
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@
|
|||
.join(" ");
|
||||
</script>
|
||||
|
||||
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
|
||||
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-button ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
|
||||
|
||||
<style lang="scss" global>
|
||||
.image-label {
|
||||
.image-button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
|
||||
+ .image-label.image-label {
|
||||
+ .image-button.image-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import type { FillChoice, MenuDirection } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
|
|
@ -15,6 +14,7 @@
|
|||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
|
|
@ -25,16 +25,18 @@
|
|||
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
|
||||
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
<!-- {#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
{/if} -->
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
{disabled}
|
||||
colorOrGradient={value}
|
||||
direction={menuDirection || "Bottom"}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
|
|
@ -137,5 +139,9 @@
|
|||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.direction-top > .floating-menu {
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let disabled = false;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
// export let disabled = false;
|
||||
// export let tooltip: string | undefined = undefined;
|
||||
|
|
@ -22,6 +23,8 @@
|
|||
let deletionRestore: boolean | undefined = undefined;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndex = index;
|
||||
|
|
@ -47,6 +50,8 @@
|
|||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
let position = markerPosition(e);
|
||||
|
|
@ -79,6 +84,8 @@
|
|||
}
|
||||
|
||||
function deleteStop(e: KeyboardEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
|
|
@ -88,6 +95,8 @@
|
|||
}
|
||||
|
||||
function deleteStopByIndex(index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (gradient.stops.length <= 2) return;
|
||||
|
||||
gradient.stops.splice(index, 1);
|
||||
|
|
@ -103,6 +112,8 @@
|
|||
}
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Just in case the mouseup event is lost
|
||||
if (e.buttons === 0) stopDrag();
|
||||
|
||||
|
|
@ -120,6 +131,8 @@
|
|||
}
|
||||
|
||||
export function setPosition(index: number, position: number) {
|
||||
if (disabled) return;
|
||||
|
||||
const active = gradient.stops[index];
|
||||
active.position = position;
|
||||
gradient.stops.sort((a, b) => a.position - b.position);
|
||||
|
|
@ -131,6 +144,8 @@
|
|||
}
|
||||
|
||||
function abortDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
|
|
@ -143,6 +158,8 @@
|
|||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
|
|
@ -152,19 +169,27 @@
|
|||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (disabled) return;
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
const element = markerTrack?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
|
@ -193,25 +218,28 @@
|
|||
document.removeEventListener("keydown", deleteStop);
|
||||
});
|
||||
|
||||
// Future design notes:
|
||||
//
|
||||
// # Backend -> Frontend
|
||||
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
|
||||
// UpdateGradient(gradient)
|
||||
// UpdateMarkers({ index, position, color }[])
|
||||
|
||||
//
|
||||
// # Frontend -> Backend
|
||||
// SendNewActive(index)
|
||||
// SendPositions({ index, position }[])
|
||||
// AddMarker(position)
|
||||
// RemoveMarkers(index[])
|
||||
// ResetMarkerToDefault(index)
|
||||
|
||||
// // We need a way to encode constraints on some markers, like locking them in place or preventing reordering
|
||||
// // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
|
||||
// // We need the ability to multi-select markers and move them all at once
|
||||
//
|
||||
// We need a way to encode constraints on some markers, like locking them in place or preventing reordering
|
||||
// We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
|
||||
// We need the ability to multi-select markers and move them all at once
|
||||
</script>
|
||||
|
||||
<LayoutCol
|
||||
class="spectrum-input"
|
||||
classes={{ disabled }}
|
||||
styles={{
|
||||
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
|
||||
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
|
||||
|
|
@ -232,6 +260,9 @@
|
|||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
|
||||
{#if disabled}
|
||||
<path class="disabled-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
|
||||
{/if}
|
||||
<path
|
||||
class="outer-border"
|
||||
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
|
||||
|
|
@ -269,6 +300,14 @@
|
|||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.disabled .gradient-strip {
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.marker-track {
|
||||
margin-top: calc(24px - 16px - 12px);
|
||||
margin-left: var(--marker-half-width);
|
||||
|
|
@ -294,28 +333,40 @@
|
|||
.outer-border {
|
||||
fill: var(--color-5-dullgray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
}
|
||||
&.disabled .marker-track .marker {
|
||||
.disabled-fill {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.outer-border {
|
||||
fill: var(--color-4-dimgray);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) .marker-track .marker {
|
||||
&:not(.active) {
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
|
||||
&.active {
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
// Styling
|
||||
export let centered = false;
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
|
@ -63,7 +64,10 @@
|
|||
<FieldInput
|
||||
class={`text-input ${className}`.trim()}
|
||||
classes={{ centered, ...classes }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
styles={{
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
}}
|
||||
{value}
|
||||
on:value
|
||||
on:textFocused={onTextFocused}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
|
||||
export let url: string;
|
||||
export let width: string | undefined;
|
||||
export let height: string | undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
</script>
|
||||
|
||||
<img src={url} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" />
|
||||
|
||||
<style lang="scss" global>
|
||||
.image-label {
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
|
||||
+ .image-label.image-label {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
<script lang="ts" context="module">
|
||||
import Data from "@graphite/components/panels/Data.svelte";
|
||||
import Document from "@graphite/components/panels/Document.svelte";
|
||||
import Layers from "@graphite/components/panels/Layers.svelte";
|
||||
import Properties from "@graphite/components/panels/Properties.svelte";
|
||||
import Spreadsheet from "@graphite/components/panels/Spreadsheet.svelte";
|
||||
|
||||
const PANEL_COMPONENTS = {
|
||||
Document,
|
||||
Layers,
|
||||
Properties,
|
||||
Spreadsheet,
|
||||
Data,
|
||||
};
|
||||
type PanelType = keyof typeof PANEL_COMPONENTS;
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
/**/ root: 100,
|
||||
/* ├─ */ content: 80,
|
||||
/* │ ├─ */ document: 70,
|
||||
/* │ └─ */ spreadsheet: 30,
|
||||
/* │ └─ */ data: 30,
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
|
|
@ -148,23 +148,31 @@
|
|||
bind:this={documentPanel}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{#if $portfolio.spreadsheetOpen}
|
||||
{#if $portfolio.dataPanelOpen}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet">
|
||||
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["data"] }} data-subdivision-name="data">
|
||||
<Panel panelType="Data" tabLabels={[{ name: "Data" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
|
||||
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
|
||||
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
{#if $portfolio.propertiesPanelOpen || $portfolio.layersPanelOpen}
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
{#if $portfolio.propertiesPanelOpen}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
|
||||
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{#if $portfolio.propertiesPanelOpen && $portfolio.layersPanelOpen}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
{/if}
|
||||
{#if $portfolio.layersPanelOpen}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
|
||||
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
{#if $dialog.visible}
|
||||
<Dialog />
|
||||
|
|
|
|||
|
|
@ -763,10 +763,16 @@ export class UpdateGraphFadeArtwork extends JsMessage {
|
|||
readonly percentage!: number;
|
||||
}
|
||||
|
||||
export class UpdateSpreadsheetState extends JsMessage {
|
||||
export class UpdateDataPanelState extends JsMessage {
|
||||
readonly open!: boolean;
|
||||
}
|
||||
|
||||
readonly node!: bigint | undefined;
|
||||
export class UpdatePropertiesPanelState extends JsMessage {
|
||||
readonly open!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateLayersPanelState extends JsMessage {
|
||||
readonly open!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateMouseCursor extends JsMessage {
|
||||
|
|
@ -981,9 +987,11 @@ export class ColorInput extends WidgetProps {
|
|||
})
|
||||
value!: FillChoice;
|
||||
|
||||
allowNone!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
allowNone!: boolean;
|
||||
menuDirection!: MenuDirection | undefined;
|
||||
|
||||
// allowTransparency!: boolean; // TODO: Implement
|
||||
|
||||
|
|
@ -1140,6 +1148,19 @@ export class ImageButton extends WidgetProps {
|
|||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class ImageLabel extends WidgetProps {
|
||||
url!: string;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
width!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
height!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
|
||||
export type NumberInputMode = "Increment" | "Range";
|
||||
|
||||
|
|
@ -1332,6 +1353,8 @@ export class TextInput extends WidgetProps {
|
|||
|
||||
minWidth!: number;
|
||||
|
||||
maxWidth!: number;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
|
@ -1383,6 +1406,7 @@ const widgetSubTypes = [
|
|||
{ value: FontInput, name: "FontInput" },
|
||||
{ value: IconButton, name: "IconButton" },
|
||||
{ value: ImageButton, name: "ImageButton" },
|
||||
{ value: ImageLabel, name: "ImageLabel" },
|
||||
{ value: IconLabel, name: "IconLabel" },
|
||||
{ value: NodeCatalog, name: "NodeCatalog" },
|
||||
{ value: NumberInput, name: "NumberInput" },
|
||||
|
|
@ -1472,11 +1496,11 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
|
|||
|
||||
updates.diff.forEach((update) => {
|
||||
// Find the object where the diff applies to
|
||||
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
|
||||
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if ("layout" in targetLayout) return targetLayout.layout[index];
|
||||
const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
|
||||
if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
|
||||
if (targetLayout instanceof Widget) {
|
||||
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
|
||||
return targetLayout.props.popoverLayout[index];
|
||||
|
|
@ -1487,10 +1511,21 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
|
|||
}
|
||||
// This is a path traversal so we can assume from the backend that it exists
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if ("action" in targetLayout) return targetLayout.children![index];
|
||||
return targetLayout[index];
|
||||
if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
|
||||
|
||||
return targetLayout?.[index];
|
||||
}, layout.layout as UIItem);
|
||||
|
||||
// Exit if we failed to produce a valid patch for the existing layout.
|
||||
// This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for
|
||||
// example, if a panel is destroyed in the frontend but was never cleared in the backend, so the next time the backend
|
||||
// tries to update the layout, it attempts to insert only the changes against the old layout that no longer exists.
|
||||
if (diffObject === undefined) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("In `patchWidgetLayout`, the `diffObject` is undefined. The layout has not been updated. See the source code comment above this error for hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a list with a length, then set the length to 0 to clear the list
|
||||
if ("length" in diffObject) {
|
||||
diffObject.length = 0;
|
||||
|
|
@ -1613,9 +1648,9 @@ export class UpdateMenuBarLayout extends JsMessage {
|
|||
|
||||
export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
|
||||
export class UpdatePropertiesPanelLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {}
|
||||
export class UpdateDataPanelLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
|
||||
|
||||
|
|
@ -1712,9 +1747,11 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateNodeThumbnail,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdatePlatform,
|
||||
UpdatePropertyPanelSectionsLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateSpreadsheetState,
|
||||
UpdatePropertiesPanelLayout,
|
||||
UpdateDataPanelLayout,
|
||||
UpdateDataPanelState,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateLayersPanelState,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateViewportHolePunch,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,9 @@ import {
|
|||
TriggerOpenDocument,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateSpreadsheetState,
|
||||
defaultWidgetLayout,
|
||||
patchWidgetLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateDataPanelState,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateLayersPanelState,
|
||||
} from "@graphite/messages";
|
||||
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
||||
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
||||
|
|
@ -27,9 +26,9 @@ export function createPortfolioState(editor: Editor) {
|
|||
unsaved: false,
|
||||
documents: [] as FrontendDocumentDetails[],
|
||||
activeDocumentIndex: 0,
|
||||
spreadsheetOpen: false,
|
||||
spreadsheetNode: BigInt(0) as bigint | undefined,
|
||||
spreadsheetWidgets: defaultWidgetLayout(),
|
||||
dataPanelOpen: false,
|
||||
propertiesPanelOpen: true,
|
||||
layersPanelOpen: true,
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
|
|
@ -107,16 +106,21 @@ export function createPortfolioState(editor: Editor) {
|
|||
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (updateDataPanelState) => {
|
||||
update((state) => {
|
||||
state.spreadsheetOpen = updateSpreadsheetState.open;
|
||||
state.spreadsheetNode = updateSpreadsheetState.node;
|
||||
state.dataPanelOpen = updateDataPanelState.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (updatePropertiesPanelState) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout);
|
||||
state.propertiesPanelOpen = updatePropertiesPanelState.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (updateLayersPanelState) => {
|
||||
update((state) => {
|
||||
state.layersPanelOpen = updateLayersPanelState.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ export function createSubscriptionRouter() {
|
|||
subscriptions[messageType.name] = callback;
|
||||
};
|
||||
|
||||
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => {
|
||||
delete subscriptions[messageType.name];
|
||||
};
|
||||
|
||||
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
|
||||
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
|
||||
const messageMaker = messageMakers[messageType];
|
||||
|
|
@ -68,6 +72,7 @@ export function createSubscriptionRouter() {
|
|||
|
||||
return {
|
||||
subscribeJsMessage,
|
||||
unsubscribeJsMessage,
|
||||
handleJsMessage,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,11 +231,7 @@ impl EditorHandle {
|
|||
let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data);
|
||||
|
||||
if let Err(error) = js_return_value {
|
||||
error!(
|
||||
"While handling FrontendMessage \"{:?}\", JavaScript threw an error: {:?}",
|
||||
message.to_discriminant().local_name(),
|
||||
error,
|
||||
)
|
||||
error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use dyn_any::StaticType;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::DocumentNode;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::raster::*;
|
||||
use graphene_core::raster_types::{CPU, GPU, Raster};
|
||||
#[cfg(feature = "gpu")]
|
||||
use graphene_core::raster_types::GPU;
|
||||
use graphene_core::raster_types::{CPU, Raster};
|
||||
use graphene_core::{Artboard, concrete, generic};
|
||||
use graphene_core::{Cow, ProtoNodeIdentifier, Type};
|
||||
use graphene_core::{NodeIO, NodeIOTypes};
|
||||
|
|
@ -15,7 +18,10 @@ use graphene_std::Graphic;
|
|||
use graphene_std::any::DowncastBothNode;
|
||||
use graphene_std::any::{ComposeTypeErased, DynAnyNode, IntoTypeErasedNode};
|
||||
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
|
||||
use graphene_std::brush::brush_cache::BrushCache;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::Vector;
|
||||
#[cfg(feature = "gpu")]
|
||||
use graphene_std::wasm_application_io::{WasmEditorApi, WasmSurfaceHandle};
|
||||
|
|
@ -84,6 +90,40 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_core::vector::VectorModification>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => [f64; 4]]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<Color>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::text::Font]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<BrushStroke>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BrushCache]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DocumentNode]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::curve::Curve]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::transform::Footprint]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::blending::BlendMode]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::extract_xy::XY]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::animation::RealTimeMode]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::FillType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::transform::ReferencePoint]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::CentroidType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::text::TextAlign]),
|
||||
// ==========
|
||||
// MEMO NODES
|
||||
// ==========
|
||||
|
|
|
|||
Loading…
Reference in New Issue