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:
Keavon Chambers 2025-08-10 07:46:42 -07:00 committed by GitHub
parent 2f4aef34e5
commit 2bb4509647
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1300 additions and 645 deletions

1
Cargo.lock generated
View File

@ -2130,6 +2130,7 @@ dependencies = [
name = "graphite-editor"
version = "0.0.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"bitflags 2.9.1",
"bytemuck",

View File

@ -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"

View File

@ -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));

View File

@ -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 });
}
}
}

View File

@ -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>,

View File

@ -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),

View File

@ -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 },

View File

@ -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(_)

View File

@ -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)]

View File

@ -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")]

View File

@ -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

View File

@ -33,8 +33,6 @@ pub enum Message {
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
// Messages
NoOp,

View File

@ -16,4 +16,3 @@ pub mod portfolio;
pub mod preferences;
pub mod prelude;
pub mod tool;
pub mod workspace;

View File

@ -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,

View File

@ -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))
}

View File

@ -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::*;

View File

@ -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 {

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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,
});
}
}

View File

@ -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(),

View File

@ -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)]

View File

@ -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,

View File

@ -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;
}

View File

@ -1,7 +0,0 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;

View File

@ -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 }]
}
}

View File

@ -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}"),
}
}

View File

@ -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};

View File

@ -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;

View File

@ -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,
}

View File

@ -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,
)
}
}

View File

@ -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) => {

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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)} />

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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 />

View File

@ -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,

View File

@ -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;
});
});

View File

@ -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,
};
}

View File

@ -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,)
}
}

View File

@ -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
// ==========