Improve the node graph with revamped top bar and disabling tools when graph is open (#2093)

* Add "Fade Artwork" slider and disable tools when graph is open

* Add navigation and layer/node management buttons to graph top bar

* Reduce code duplication
This commit is contained in:
Keavon Chambers 2024-11-04 12:41:53 -08:00 committed by GitHub
parent 12ca06035c
commit f1b0d8fa87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 480 additions and 285 deletions

View File

@ -199,10 +199,11 @@ impl Dispatcher {
Message::Portfolio(message) => {
let ipp = &self.message_handlers.input_preprocessor_message_handler;
let preferences = &self.message_handlers.preferences_message_handler;
let current_tool = &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type;
self.message_handlers
.portfolio_message_handler
.process_message(message, &mut queue, PortfolioMessageData { ipp, preferences });
.process_message(message, &mut queue, PortfolioMessageData { ipp, preferences, current_tool });
}
Message::Preferences(message) => {
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
@ -244,8 +245,10 @@ impl Dispatcher {
list.extend(self.message_handlers.input_preprocessor_message_handler.actions());
list.extend(self.message_handlers.key_mapping_message_handler.actions());
list.extend(self.message_handlers.debug_message_handler.actions());
if self.message_handlers.portfolio_message_handler.active_document().is_some() {
list.extend(self.message_handlers.tool_message_handler.actions());
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() {
if !document.graph_view_overlay_open {
list.extend(self.message_handlers.tool_message_handler.actions());
}
}
list.extend(self.message_handlers.portfolio_message_handler.actions());
list

View File

@ -84,9 +84,6 @@ pub enum FrontendMessage {
#[serde(rename = "isDefault")]
is_default: bool,
},
TriggerGraphViewOverlay {
open: bool,
},
TriggerImport,
TriggerIndexedDbRemoveDocument {
#[serde(rename = "documentId")]
@ -153,6 +150,9 @@ pub enum FrontendMessage {
#[serde(rename = "clickTargets")]
click_targets: Option<FrontendClickTargets>,
},
UpdateGraphViewOverlay {
open: bool,
},
UpdateLayerWidths {
#[serde(rename = "layerWidths")]
layer_widths: HashMap<NodeId, u32>,
@ -221,6 +221,9 @@ pub enum FrontendMessage {
#[serde(rename = "setColorChoice")]
set_color_choice: Option<String>,
},
UpdateGraphFadeArtwork {
percentage: f64,
},
UpdateInputHints {
#[serde(rename = "hintData")]
hint_data: HintData,

View File

@ -52,6 +52,7 @@ pub fn input_mappings() -> Mapping {
//
// Hack to prevent Left Click + Accel + Z combo (this effectively blocks you from making a double undo with AbortTransaction)
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
//
// NodeGraphMessage
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: false}),
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: false, alt_click: false, right_click: false}),
@ -69,6 +70,8 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy),
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes),
entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility),
entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked),
entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes),
entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=NodeGraphMessage::SendClickTargets),

View File

@ -131,6 +131,9 @@ pub enum DocumentMessage {
SetBlendModeForSelectedLayers {
blend_mode: BlendMode,
},
SetGraphFadeArtwork {
percentage: f64,
},
SetNodePinned {
node_id: NodeId,
pinned: bool,

View File

@ -39,44 +39,7 @@ pub struct DocumentMessageData<'a> {
pub ipp: &'a InputPreprocessorMessageHandler,
pub persistent_data: &'a PersistentData,
pub executor: &'a mut NodeGraphExecutor,
}
// TODO: Eventually remove this (probably starting late 2024)
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OldDocumentMessageHandler {
// ============================================
// Fields that are saved in the document format
// ============================================
//
/// The node graph that generates this document's artwork.
/// It recursively stores its sub-graphs, so this root graph is the whole snapshot of the document content.
pub network: OldNodeNetwork,
/// List of the [`NodeId`]s that are currently selected by the user.
pub selected_nodes: SelectedNodes,
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
pub collapsed: CollapsedLayers,
/// The name of the document, which is displayed in the tab and title bar of the editor.
pub name: String,
/// The full Git commit hash of the Graphite repository that was used to build the editor.
/// We save this to provide a hint about which version of the editor was used to create the document.
pub commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visible: bool,
/// Sets whether or not the rulers should be drawn along the top and left edges of the viewport area.
pub rulers_visible: bool,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current user choices for snapping behavior, including whether snapping is enabled at all.
pub snapping_state: SnappingState,
pub current_tool: &'a ToolType,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -121,10 +84,12 @@ pub struct DocumentMessageHandler {
pub overlays_visible: bool,
/// Sets whether or not the rulers should be drawn along the top and left edges of the viewport area.
pub rulers_visible: bool,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current user choices for snapping behavior, including whether snapping is enabled at all.
pub snapping_state: SnappingState,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current opacity of the faded node graph background that covers up the artwork.
pub graph_fade_artwork_percentage: f64,
// =============================================
// Fields omitted from the saved document format
@ -178,6 +143,7 @@ impl Default for DocumentMessageHandler {
rulers_visible: true,
graph_view_overlay_open: false,
snapping_state: SnappingState::default(),
graph_fade_artwork_percentage: 80.,
// =============================================
// Fields omitted from the saved document format
// =============================================
@ -199,6 +165,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
ipp,
persistent_data,
executor,
current_tool,
} = data;
let selected_nodes_bounding_box_viewport = self.network_interface.selected_nodes_bounding_box_viewport(&self.breadcrumb_network_path);
@ -247,6 +214,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
collapsed: &mut self.collapsed,
ipp,
graph_view_overlay_open: self.graph_view_overlay_open,
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
navigation_handler: &self.navigation_handler,
},
);
}
@ -477,15 +446,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::GraphViewOverlay { open } => {
self.graph_view_overlay_open = open;
responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
responses.add(FrontendMessage::UpdateGraphViewOverlay { open });
responses.add(FrontendMessage::UpdateGraphFadeArtwork {
percentage: self.graph_fade_artwork_percentage,
});
// Update the tilt menu bar buttons to be disabled when the graph is open
responses.add(MenuBarMessage::SendLayout);
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
if open {
responses.add(ToolMessage::DeactivateTools);
responses.add(OverlaysMessage::Draw); // Clear the overlays
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(NodeGraphMessage::UpdateGraphBarRight);
responses.add(NodeGraphMessage::SendGraph);
} else {
responses.add(ToolMessage::ActivateTool { tool_type: *current_tool });
}
}
DocumentMessage::GraphViewOverlayToggle => {
@ -1019,6 +998,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(GraphOperationMessage::BlendModeSet { layer, blend_mode });
}
}
DocumentMessage::SetGraphFadeArtwork { percentage } => {
self.graph_fade_artwork_percentage = percentage;
responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage });
}
DocumentMessage::SetNodePinned { node_id, pinned } => {
responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
@ -1338,11 +1321,13 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
SelectedLayersRaise,
SelectedLayersRaiseToFront,
UngroupSelectedLayers,
ToggleSelectedVisibility,
ToggleSelectedLocked
);
if !self.graph_view_overlay_open {
select.extend(actions!(DocumentMessageDiscriminant; NudgeSelectedLayers));
select.extend(actions!(DocumentMessageDiscriminant;
NudgeSelectedLayers,
ToggleSelectedVisibility,
));
}
common.extend(select);
}
@ -1825,68 +1810,10 @@ impl DocumentMessageHandler {
])
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
IconButton::new("ZoomIn", 24)
.tooltip("Zoom In")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease))
.on_update(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
.widget_holder(),
IconButton::new("ZoomOut", 24)
.tooltip("Zoom Out")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomDecrease))
.on_update(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
.widget_holder(),
IconButton::new("ZoomReset", 24)
.tooltip("Reset Tilt and Zoom to 100%")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasTiltResetAndZoomTo100Percent))
.on_update(|_| NavigationMessage::CanvasTiltResetAndZoomTo100Percent.into())
.disabled(self.document_ptz.tilt.abs() < 1e-4 && (self.document_ptz.zoom() - 1.).abs() < 1e-4)
.widget_holder(),
PopoverButton::new()
.popover_layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Canvas Navigation").bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(
"
Interactive controls in this\n\
menu are coming soon.\n\
\n\
Pan:\n\
Middle Click Drag\n\
\n\
Tilt:\n\
Alt + Middle Click Drag\n\
\n\
Zoom:\n\
Shift + Middle Click Drag\n\
Ctrl + Scroll Wheel Roll
"
.trim(),
)
.multiline(true)
.widget_holder()],
},
])
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.navigation_handler.snapped_zoom(self.document_ptz.zoom()) * 100.))
.unit("%")
.min(0.000001)
.max(1000000.)
.tooltip("Document zoom within the viewport")
.on_update(|number_input: &NumberInput| {
NavigationMessage::CanvasZoomSet {
zoom_factor: number_input.value.unwrap() / 100.,
}
.into()
})
.increment_behavior(NumberInputIncrementBehavior::Callback)
.increment_callback_decrease(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
.increment_callback_increase(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
.widget_holder(),
];
widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, "Canvas"));
let tilt_value = self.navigation_handler.snapped_tilt(self.document_ptz.tilt) / (std::f64::consts::PI / 180.);
if tilt_value.abs() > 0.00001 {
widgets.extend([
@ -2059,15 +1986,15 @@ impl DocumentMessageHandler {
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedLocked))
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleSelectedLocked))
.on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into())
.disabled(!has_selection)
.widget_holder(),
IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24)
.hover_icon(Some((if selection_all_visible { "EyeHide" } else { "EyeShow" }).into()))
.tooltip(if selection_all_visible { "Hide Selected" } else { "Show Selected" })
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedVisibility))
.on_update(|_| NodeGraphMessage::ToggleSelectedVisibility.into())
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleSelectedVisibility))
.on_update(|_| DocumentMessage::ToggleSelectedVisibility.into())
.disabled(!has_selection)
.widget_holder(),
],
@ -2235,6 +2162,71 @@ impl<'a> ClickXRayIter<'a> {
}
}
pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHandler, tooltip_name: &str) -> [WidgetHolder; 6] {
[
IconButton::new("ZoomIn", 24)
.tooltip("Zoom In")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease))
.on_update(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
.widget_holder(),
IconButton::new("ZoomOut", 24)
.tooltip("Zoom Out")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomDecrease))
.on_update(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
.widget_holder(),
IconButton::new("ZoomReset", 24)
.tooltip("Reset Tilt and Zoom to 100%")
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasTiltResetAndZoomTo100Percent))
.on_update(|_| NavigationMessage::CanvasTiltResetAndZoomTo100Percent.into())
.disabled(ptz.tilt.abs() < 1e-4 && (ptz.zoom() - 1.).abs() < 1e-4)
.widget_holder(),
PopoverButton::new()
.popover_layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("{tooltip_name} Navigation")).bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new({
let tilt = if tooltip_name == "Canvas" { "Tilt:\n• Alt + Middle Click Drag\n\n" } else { "" };
format!(
"
Interactive controls in this\n\
menu are coming soon.\n\
\n\
Pan:\n\
Middle Click Drag\n\
\n\
{tilt}Zoom:\n\
Shift + Middle Click Drag\n\
Ctrl + Scroll Wheel Roll
"
)
.trim()
})
.multiline(true)
.widget_holder()],
},
])
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(navigation_handler.snapped_zoom(ptz.zoom()) * 100.))
.unit("%")
.min(0.000001)
.max(1000000.)
.tooltip(format!("{tooltip_name} Zoom"))
.on_update(|number_input: &NumberInput| {
NavigationMessage::CanvasZoomSet {
zoom_factor: number_input.value.unwrap() / 100.,
}
.into()
})
.increment_behavior(NumberInputIncrementBehavior::Callback)
.increment_callback_decrease(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
.increment_callback_increase(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
.widget_holder(),
]
}
impl<'a> Iterator for ClickXRayIter<'a> {
type Item = LayerNodeIdentifier;
@ -2266,3 +2258,41 @@ impl<'a> Iterator for ClickXRayIter<'a> {
None
}
}
// TODO: Eventually remove this (probably starting late 2024)
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OldDocumentMessageHandler {
// ============================================
// Fields that are saved in the document format
// ============================================
//
/// The node graph that generates this document's artwork.
/// It recursively stores its sub-graphs, so this root graph is the whole snapshot of the document content.
pub network: OldNodeNetwork,
/// List of the [`NodeId`]s that are currently selected by the user.
pub selected_nodes: SelectedNodes,
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
pub collapsed: CollapsedLayers,
/// The name of the document, which is displayed in the tab and title bar of the editor.
pub name: String,
/// The full Git commit hash of the Graphite repository that was used to build the editor.
/// We save this to provide a hint about which version of the editor was used to create the document.
pub commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visible: bool,
/// Sets whether or not the rulers should be drawn along the top and left edges of the viewport area.
pub rulers_visible: bool,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current user choices for snapping behavior, including whether snapping is enabled at all.
pub snapping_state: SnappingState,
}

View File

@ -181,7 +181,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
};
ptz.tilt = 0.;
ptz.set_zoom(1.);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
if graph_view_overlay_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
@ -192,6 +196,9 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
};
ptz.tilt = angle_radians;
responses.add(DocumentMessage::PTZUpdate);
if !graph_view_overlay_open {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
}
NavigationMessage::CanvasZoomDecrease { center_on_mouse } => {
let Some(ptz) = get_ptz(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
@ -252,7 +259,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
let zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
let zoom = zoom * Self::clamp_zoom(zoom, document_bounds, old_zoom, ipp);
ptz.set_zoom(zoom);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
if graph_view_overlay_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
@ -281,6 +292,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
ptz.tilt = self.snapped_tilt(ptz.tilt);
ptz.set_zoom(self.snapped_zoom(ptz.zoom()));
responses.add(DocumentMessage::PTZUpdate);
if graph_view_overlay_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(NodeGraphMessage::SetGridAlignedEdges);
// Reset the navigation operation now that it's done
self.navigation_operation = NavigationOperation::None;
@ -336,7 +352,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
ptz.set_zoom(1.);
}
responses.add(PortfolioMessage::UpdateDocumentWidgets);
if graph_view_overlay_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
@ -478,11 +498,7 @@ impl NavigationMessageHandler {
}
pub fn snapped_zoom(&self, zoom: f64) -> f64 {
if matches!(self.navigation_operation, NavigationOperation::Zoom { snap: true, .. }) {
*VIEWPORT_ZOOM_LEVELS.iter().min_by(|a, b| (**a - zoom).abs().partial_cmp(&(**b - zoom).abs()).unwrap()).unwrap_or(&zoom)
} else {
zoom
}
snapped_zoom(&self.navigation_operation, zoom)
}
pub fn calculate_offset_transform(&self, viewport_center: DVec2, ptz: &PTZ) -> DAffine2 {
@ -523,3 +539,11 @@ impl NavigationMessageHandler {
VIEWPORT_ZOOM_MIN_FRACTION_COVER / scale_factor
}
}
pub fn snapped_zoom(navigation_operation: &NavigationOperation, zoom: f64) -> f64 {
if matches!(navigation_operation, NavigationOperation::Zoom { snap: true, .. }) {
*VIEWPORT_ZOOM_LEVELS.iter().min_by(|a, b| (**a - zoom).abs().partial_cmp(&(**b - zoom).abs()).unwrap()).unwrap_or(&zoom)
} else {
zoom
}
}

View File

@ -30,8 +30,7 @@ pub enum NodeGraphMessage {
CreateNodeFromContextMenu {
node_id: Option<NodeId>,
node_type: String,
x: i32,
y: i32,
xy: Option<(i32, i32)>,
},
CreateWire {
output_connector: OutputConnector,
@ -182,6 +181,7 @@ pub enum NodeGraphMessage {
node_graph_errors: GraphErrors,
},
UpdateActionButtons,
UpdateGraphBarRight,
UpdateInSelectedNetwork,
SendSelectedNodes,
}

View File

@ -1,7 +1,9 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeWire, WirePath};
use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE;
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::document_message_handler::navigation_controls;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType};
@ -28,6 +30,8 @@ pub struct NodeGraphHandlerData<'a> {
pub collapsed: &'a mut CollapsedLayers,
pub ipp: &'a InputPreprocessorMessageHandler,
pub graph_view_overlay_open: bool,
pub graph_fade_artwork_percentage: f64,
pub navigation_handler: &'a NavigationMessageHandler,
}
#[derive(Debug, Clone)]
@ -76,8 +80,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
breadcrumb_network_path,
document_id,
collapsed,
graph_view_overlay_open,
ipp,
graph_view_overlay_open,
graph_fade_artwork_percentage,
navigation_handler,
} = data;
match message {
@ -149,7 +155,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, x, y } => {
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, xy } => {
let (x, y) = if let Some((x, y)) = xy {
(x, y)
} else if let Some(node_graph_ptz) = network_interface.node_graph_ptz(breadcrumb_network_path) {
((-node_graph_ptz.pan.x / GRID_SIZE as f64) as i32, (-node_graph_ptz.pan.y / GRID_SIZE as f64) as i32)
} else {
(0, 0)
};
let node_id = node_id.unwrap_or_else(NodeId::new);
let Some(document_node_type) = document_node_definitions::resolve_document_node_type(&node_type) else {
@ -1374,9 +1388,14 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
NodeGraphMessage::UpdateActionButtons => {
if selection_network_path == breadcrumb_network_path {
self.update_selection_action_buttons(network_interface, breadcrumb_network_path, responses);
self.update_graph_bar_left(network_interface, breadcrumb_network_path, responses);
self.send_node_bar_layout(responses);
}
}
NodeGraphMessage::UpdateGraphBarRight => {
self.update_graph_bar_right(graph_fade_artwork_percentage, network_interface, breadcrumb_network_path, navigation_handler);
self.send_node_bar_layout(responses);
}
NodeGraphMessage::UpdateInSelectedNetwork => responses.add(FrontendMessage::UpdateInSelectedNetwork {
in_selected_network: selection_network_path == breadcrumb_network_path,
}),
@ -1435,7 +1454,7 @@ impl NodeGraphMessageHandler {
}
/// Updates the buttons for visibility, locked, and preview
fn update_selection_action_buttons(&mut self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId], responses: &mut VecDeque<Message>) {
fn update_graph_bar_left(&mut self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId], responses: &mut VecDeque<Message>) {
let Some(subgraph_path_names) = Self::collect_subgraph_names(network_interface, breadcrumb_network_path) else {
// If a node in a nested network could not be found, exit the nested network
let breadcrumb_network_path_len = breadcrumb_network_path.len();
@ -1457,100 +1476,172 @@ impl NodeGraphMessageHandler {
return;
};
let subgraph_path_names_length = subgraph_path_names.len();
let breadcrumb_trail = BreadcrumbTrailButtons::new(subgraph_path_names).on_update(move |index| {
DocumentMessage::ExitNestedNetwork {
steps_back: subgraph_path_names_length - (*index as usize) - 1,
let has_selection = selected_nodes.has_selected_nodes();
let selection_includes_layers = network_interface.selected_nodes(&[]).unwrap().selected_layers(network_interface.document_metadata()).count() > 0;
let selection_all_locked = network_interface.selected_nodes(&[]).unwrap().selected_unlocked_layers(network_interface).count() == 0;
let selection_all_visible = selected_nodes.selected_nodes().all(|id| {
if let Some(node) = network.nodes.get(id) {
node.visible
} else {
error!("Could not get node {id} in update_selection_action_buttons");
true
}
.into()
});
let mut widgets = if subgraph_path_names_length >= 2 {
vec![breadcrumb_trail.widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]
let mut widgets = vec![
PopoverButton::new()
.icon(Some("Node".to_string()))
.tooltip("Add a new node")
.popover_layout({
let node_chooser = NodeCatalog::new()
.on_update(move |node_type| {
let node_id = NodeId::new();
Message::Batched(Box::new([
NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type: node_type.clone(),
xy: None,
}
.into(),
NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into(),
]))
})
.widget_holder();
vec![LayoutGroup::Row { widgets: vec![node_chooser] }]
})
.widget_holder(),
//
Separator::new(SeparatorType::Unrelated).widget_holder(),
//
IconButton::new("NewLayer", 24)
.tooltip("New Layer")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
.widget_holder(),
IconButton::new("Folder", 24)
.tooltip("Group Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
.on_update(|_| DocumentMessage::GroupSelectedLayers.into())
.disabled(!has_selection)
.widget_holder(),
IconButton::new("Trash", 24)
.tooltip("Delete Selected")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
.disabled(!has_selection)
.widget_holder(),
//
Separator::new(SeparatorType::Unrelated).widget_holder(),
//
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedLocked))
.on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into())
.disabled(!has_selection || !selection_includes_layers)
.widget_holder(),
IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24)
.hover_icon(Some((if selection_all_visible { "EyeHide" } else { "EyeShow" }).into()))
.tooltip(if selection_all_visible { "Hide Selected" } else { "Show Selected" })
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedVisibility))
.on_update(|_| NodeGraphMessage::ToggleSelectedVisibility.into())
.disabled(!has_selection)
.widget_holder(),
];
let mut selection = selected_nodes.selected_nodes();
let (selection, no_other_selections) = (selection.next(), selection.count() == 0);
let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) {
network.exports.iter().find_map(|export| {
let NodeInput::Node { node_id, .. } = export else { return None };
Some(*node_id)
})
} else {
Vec::new()
None
};
let mut selection = selected_nodes.selected_nodes();
// If there is at least one selected node then show the hide or show button
if selection.next().is_some() {
// Check if any of the selected nodes are disabled
let all_visible = selected_nodes.selected_nodes().all(|id| {
if let Some(node) = network.nodes.get(id) {
node.visible
} else {
error!("Could not get node {id} in update_selection_action_buttons");
true
}
});
// Check if multiple nodes are selected
let multiple_nodes = selection.next().is_some();
// Generate the visible/hidden button accordingly
let (hide_show_label, hide_show_icon) = if all_visible { ("Make Hidden", "EyeVisible") } else { ("Make Visible", "EyeHidden") };
let hide_button = TextButton::new(hide_show_label)
.icon(Some(hide_show_icon.to_string()))
.tooltip(if all_visible { "Hide" } else { "Show" }.to_string() + " selected " + if multiple_nodes { "nodes/layers" } else { "node/layer" })
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedVisibility))
.on_update(move |_| NodeGraphMessage::ToggleSelectedVisibility.into())
.widget_holder();
widgets.push(hide_button);
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
}
let mut selection = selected_nodes.selected_nodes();
// If there is at least one selected node then show the pin or unpin button
if selection.next().is_some() {
// Check if any of the selected nodes are pinned
let all_unpinned = !selected_nodes.selected_nodes().all(|id| {
if let Some(node) = network_interface.node_metadata(id, breadcrumb_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {id} in update_selection_action_buttons");
false
}
});
// Check if multiple nodes are selected
let multiple_nodes = selection.next().is_some();
// Generate the visible/hidden button accordingly
let (pin_unpin_label, pin_unpin_icon) = if all_unpinned { ("Pin", "CheckboxUnchecked") } else { ("Unpin", "CheckboxChecked") };
let pin_button = TextButton::new(pin_unpin_label)
.icon(Some(pin_unpin_icon.to_string()))
.tooltip(
if all_unpinned { "Pin" } else { "Unpin" }.to_string()
+ " selected " + if multiple_nodes { "nodes/layers" } else { "node/layer" }
+ " in the Properties panel when nothing is selected",
)
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedIsPinned))
.on_update(move |_| NodeGraphMessage::ToggleSelectedIsPinned.into())
.widget_holder();
widgets.push(pin_button);
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
}
let mut selection = selected_nodes.selected_nodes();
// If only one node is selected then show the preview or stop previewing button
if let (Some(&node_id), None) = (selection.next(), selection.next()) {
// Is this node the current output
let is_output = network.outputs_contain(node_id);
let is_previewing = matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. });
let output_button = TextButton::new(if is_output && is_previewing { "End Preview" } else { "Preview" })
if let Some(node_id) = previewing {
let button = TextButton::new("End Preview")
.icon(Some("Rescale".to_string()))
.tooltip(if is_output { "Restore preview to the graph output" } else { "Preview selected node/layer" }.to_string() + " (Shortcut: Alt-click node/layer)")
.tooltip("Restore preview to the graph output")
.on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into())
.widget_holder();
widgets.push(output_button);
widgets.extend([Separator::new(SeparatorType::Unrelated).widget_holder(), button]);
} else if let Some(&node_id) = selection {
let selection_is_not_already_the_output = !network
.exports
.iter()
.any(|export| matches!(export, NodeInput::Node { node_id: export_node_id, .. } if *export_node_id == node_id));
if selection_is_not_already_the_output && no_other_selections {
let button = TextButton::new("Preview")
.icon(Some("Rescale".to_string()))
.tooltip("Preview selected node/layer (Shortcut: Alt-click node/layer)")
.on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into())
.widget_holder();
widgets.extend([Separator::new(SeparatorType::Unrelated).widget_holder(), button]);
}
}
let subgraph_path_names_length = subgraph_path_names.len();
if subgraph_path_names_length >= 2 {
widgets.extend([
Separator::new(SeparatorType::Unrelated).widget_holder(),
BreadcrumbTrailButtons::new(subgraph_path_names)
.on_update(move |index| {
DocumentMessage::ExitNestedNetwork {
steps_back: subgraph_path_names_length - (*index as usize) - 1,
}
.into()
})
.widget_holder(),
]);
}
self.widgets[0] = LayoutGroup::Row { widgets };
self.send_node_bar_layout(responses);
}
fn update_graph_bar_right(
&mut self,
graph_fade_artwork_percentage: f64,
network_interface: &NodeNetworkInterface,
breadcrumb_network_path: &[NodeId],
navigation_handler: &NavigationMessageHandler,
) {
let Some(node_graph_ptz) = network_interface.node_graph_ptz(breadcrumb_network_path) else {
log::error!("Could not get node graph PTZ");
return;
};
let mut widgets = vec![
NumberInput::new(Some(graph_fade_artwork_percentage))
.percentage()
.display_decimal_places(0)
.label("Fade Artwork")
.tooltip("Opacity of the graph background that covers the artwork")
.on_update(move |number_input: &NumberInput| {
DocumentMessage::SetGraphFadeArtwork {
percentage: number_input.value.unwrap_or(graph_fade_artwork_percentage),
}
.into()
})
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
];
widgets.extend(navigation_controls(node_graph_ptz, navigation_handler, "Node Graph"));
widgets.extend([
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextButton::new("Node Graph")
.icon(Some("GraphViewOpen".into()))
.hover_icon(Some("GraphViewClosed".into()))
.tooltip("Hide Node Graph")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GraphViewOverlayToggle))
.on_update(move |_| DocumentMessage::GraphViewOverlayToggle.into())
.widget_holder(),
]);
self.widgets[1] = LayoutGroup::Row { widgets };
}
/// Collate the properties panel sections for a node graph
@ -1587,7 +1678,18 @@ impl NodeGraphMessageHandler {
0 => {
let selected_nodes = nodes
.iter()
.filter_map(|node_id| network.nodes.get(node_id).map(|node| node_properties::generate_node_properties(node, *node_id, false, context)))
.filter_map(|node_id| {
network.nodes.get(node_id).map(|node| {
let pinned = if let Some(node) = context.network_interface.node_metadata(node_id, context.selection_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {node_id} in collate_properties");
false
};
node_properties::generate_node_properties(node, *node_id, pinned, context)
})
})
.collect::<Vec<_>>();
if !selected_nodes.is_empty() {
return selected_nodes;
@ -1618,7 +1720,7 @@ impl NodeGraphMessageHandler {
};
if pinned {
Some(node_properties::generate_node_properties(node, *node_id, true, context))
Some(node_properties::generate_node_properties(node, *node_id, pinned, context))
} else {
None
}
@ -1655,23 +1757,22 @@ impl NodeGraphMessageHandler {
})
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
{
let node_chooser = NodeCatalog::new()
.on_update(move |node_type| {
NodeGraphMessage::CreateNodeInLayerWithTransaction {
node_type: node_type.clone(),
layer: LayerNodeIdentifier::new_unchecked(layer),
}
.into()
})
.widget_holder();
let popover_layout = vec![LayoutGroup::Row { widgets: vec![node_chooser] }];
PopoverButton::new()
.icon(Some("Node".to_string()))
.tooltip("Add an operation to the end of this layer's chain of nodes")
.popover_layout(popover_layout)
.widget_holder()
},
PopoverButton::new()
.icon(Some("Node".to_string()))
.tooltip("Add an operation to the end of this layer's chain of nodes")
.popover_layout({
let node_chooser = NodeCatalog::new()
.on_update(move |node_type| {
NodeGraphMessage::CreateNodeInLayerWithTransaction {
node_type: node_type.clone(),
layer: LayerNodeIdentifier::new_unchecked(layer),
}
.into()
})
.widget_holder();
vec![LayoutGroup::Row { widgets: vec![node_chooser] }]
})
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
],
}];
@ -1689,7 +1790,16 @@ impl NodeGraphMessageHandler {
}
})
.filter_map(|(_, node_id)| network.nodes.get(&node_id).map(|node| (node, node_id)))
.map(|(node, node_id)| node_properties::generate_node_properties(node, node_id, false, context))
.map(|(node, node_id)| {
let pinned = if let Some(node) = context.network_interface.node_metadata(&node_id, context.selection_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {node_id} in collate_properties");
false
};
node_properties::generate_node_properties(node, node_id, pinned, context)
})
.collect::<Vec<_>>();
layer_properties.extend(node_properties);
@ -2125,7 +2235,7 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface:
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a seperate loop because it requires a mutable reference to the `network_interface`)
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path);
value.ty = ty;
value.type_source = type_source;
@ -2136,24 +2246,11 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface:
impl Default for NodeGraphMessageHandler {
fn default() -> Self {
let right_side_widgets = vec![
// TODO: Replace this with an "Add Node" button, also next to an "Add Layer" button
TextLabel::new("Right Click in Graph to Add Nodes").italic(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextButton::new("Node Graph")
.icon(Some("GraphViewOpen".into()))
.hover_icon(Some("GraphViewClosed".into()))
.tooltip("Hide Node Graph")
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GraphViewOverlayToggle))
.on_update(move |_| DocumentMessage::GraphViewOverlayToggle.into())
.widget_holder(),
];
Self {
network: Vec::new(),
node_graph_errors: Vec::new(),
has_selection: false,
widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: right_side_widgets }],
widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: Vec::new() }],
drag_start: None,
begin_dragging: false,
drag_occurred: false,

View File

@ -2569,6 +2569,14 @@ impl NodeNetworkInterface {
&& (input_count <= 2)
}
pub fn node_graph_ptz(&self, network_path: &[NodeId]) -> Option<&PTZ> {
let Some(network_metadata) = self.network_metadata(network_path) else {
log::error!("Could not get nested network_metadata in node_graph_ptz_mut");
return None;
};
Some(&network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz)
}
pub fn node_graph_ptz_mut(&mut self, network_path: &[NodeId]) -> Option<&mut PTZ> {
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
log::error!("Could not get nested network_metadata in node_graph_ptz_mut");

View File

@ -10,7 +10,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::DocumentMessageData;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup};
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use graph_craft::document::value::TaggedValue;
@ -25,6 +25,7 @@ use std::vec;
pub struct PortfolioMessageData<'a> {
pub ipp: &'a InputPreprocessorMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
pub current_tool: &'a ToolType,
}
#[derive(Debug, Default)]
@ -41,7 +42,7 @@ pub struct PortfolioMessageHandler {
impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
fn process_message(&mut self, message: PortfolioMessage, responses: &mut VecDeque<Message>, data: PortfolioMessageData) {
let PortfolioMessageData { ipp, preferences } = data;
let PortfolioMessageData { ipp, preferences, current_tool } = data;
match message {
// Sub-messages
@ -73,6 +74,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
ipp,
persistent_data: &self.persistent_data,
executor: &mut self.executor,
current_tool,
};
document.process_message(message, responses, document_inputs)
}
@ -87,6 +89,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
ipp,
persistent_data: &self.persistent_data,
executor: &mut self.executor,
current_tool,
};
document.process_message(message, responses, document_inputs)
}
@ -754,10 +757,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(OverlaysMessage::Draw);
responses.add(BroadcastEvent::ToolAbort);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
if node_graph_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
}
PortfolioMessage::SubmitDocumentExport {
file_name,

View File

@ -25,6 +25,7 @@ pub struct ToolMessageHandler {
pub tool_state: ToolFsmState,
pub transform_layer_handler: TransformLayerMessageHandler,
pub shape_editor: ShapeState,
pub tool_is_active: bool,
}
impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
@ -69,9 +70,10 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
let old_tool = tool_data.active_tool_type;
// Do nothing if switching to the same tool
if tool_type == old_tool {
if self.tool_is_active && tool_type == old_tool {
return;
}
self.tool_is_active = true;
// Send the old and new tools a transition to their FSM Abort states
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
@ -85,6 +87,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
shape_editor: &mut self.shape_editor,
node_graph,
};
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
tool.process_message(tool_abort_message, responses, &mut data);
}
@ -133,6 +136,11 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
responses.add(BroadcastMessage::UnsubscribeEvent { message, on });
responses.add(OverlaysMessage::RemoveProvider(ARTBOARD_OVERLAY_PROVIDER));
responses.add(FrontendMessage::UpdateInputHints { hint_data: Default::default() });
responses.add(FrontendMessage::UpdateMouseCursor { cursor: Default::default() });
self.tool_is_active = false;
}
ToolMessage::InitTools => {
// Subscribe the transform layer to selection change events
@ -141,6 +149,8 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
send: Box::new(TransformLayerMessage::SelectionChanged.into()),
});
self.tool_is_active = true;
let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data;
let active_tool = &tool_data.active_tool_type;

View File

@ -539,7 +539,6 @@ impl Fsm for PenToolFsmState {
// Perform extension of an existing path
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) {
log::debug!("Should extend: {:?}", layer);
tool_data.add_point(LastPoint {
id: point,
pos: position,
@ -550,7 +549,6 @@ impl Fsm for PenToolFsmState {
tool_data.next_point = position;
tool_data.next_handle_start = position;
} else if let (Some(layer), None) = (selected_layers.next(), selected_layers.next()) {
log::debug!("Adding to layer: {:?}", layer);
// Add the first point to a new layer
// Generate first point
let id = PointId::generate();
@ -566,7 +564,6 @@ impl Fsm for PenToolFsmState {
tool_data.next_point = pos;
tool_data.next_handle_start = pos;
} else {
log::debug!("Creating new layer");
// New path layer
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let nodes = vec![(NodeId(0), node_type.default_node_template())];

View File

@ -477,7 +477,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct HintData(pub Vec<HintGroup>);
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]

View File

@ -504,7 +504,7 @@
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
</canvas>
</div>
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork="80%" data-graph>
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>
<Graph />
</div>
</LayoutCol>

View File

@ -989,7 +989,7 @@
}
&.disabled {
background: var(--color-3-darkgray);
background: rgba(var(--color-4-dimgray-rgb), 0.33);
color: var(--color-a-softgray);
.icon-label {
@ -1041,10 +1041,10 @@
}
&.selected {
background: rgba(var(--color-5-dullgray-rgb), 0.5);
background: rgba(var(--color-5-dullgray-rgb), 0.33);
&.in-selected-network {
background: rgba(var(--color-6-lowergray-rgb), 0.5);
background: rgba(var(--color-6-lowergray-rgb), 0.33);
}
}

View File

@ -27,18 +27,16 @@
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
<div class="expand-arrow" />
<TextLabel bold={true}>{widgetData.name}</TextLabel>
{#if widgetData.pinned}
<IconButton
icon={"CheckboxChecked"}
tooltip={"Unpin this node so it's no longer shown here without a selection"}
size={24}
action={(e) => {
editor.handle.unpinNode(widgetData.id);
e?.stopPropagation();
}}
class={"show-only-on-hover"}
/>
{/if}
<IconButton
icon={widgetData.pinned ? "CheckboxChecked" : "CheckboxUnchecked"}
tooltip={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected" : "Pin this node so it's shown here when nothing is selected"}
size={24}
action={(e) => {
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
e?.stopPropagation();
}}
class={"show-only-on-hover"}
/>
<IconButton
icon={"Trash"}
tooltip={"Delete this node from the layer chain"}

View File

@ -11,8 +11,9 @@ import {
UpdateToolShelfLayout,
UpdateWorkingColorsLayout,
UpdateNodeGraphBarLayout,
TriggerGraphViewOverlay,
UpdateGraphViewOverlay,
TriggerDelayedZoomCanvasToFitAll,
UpdateGraphFadeArtwork,
} from "@graphite/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@ -27,10 +28,17 @@ export function createDocumentState(editor: Editor) {
nodeGraphBarLayout: defaultWidgetLayout(),
// Graph view overlay
graphViewOverlayOpen: false,
fadeArtwork: 100,
});
const { subscribe, update } = state;
// Update layouts
editor.subscriptions.subscribeJsMessage(UpdateGraphFadeArtwork, (updateGraphFadeArtwork) => {
update((state) => {
state.fadeArtwork = updateGraphFadeArtwork.percentage;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, async (updateDocumentModeLayout) => {
await tick();
@ -84,9 +92,9 @@ export function createDocumentState(editor: Editor) {
});
// Show or hide the graph view overlay
editor.subscriptions.subscribeJsMessage(TriggerGraphViewOverlay, (triggerGraphViewOverlay) => {
editor.subscriptions.subscribeJsMessage(UpdateGraphViewOverlay, (updateGraphViewOverlay) => {
update((state) => {
state.graphViewOverlayOpen = triggerGraphViewOverlay.open;
state.graphViewOverlayOpen = updateGraphViewOverlay.open;
return state;
});
});

View File

@ -742,6 +742,14 @@ const mouseCursorIconCSSNames = {
export type MouseCursor = keyof typeof mouseCursorIconCSSNames;
export type MouseCursorIcon = (typeof mouseCursorIconCSSNames)[MouseCursor];
export class UpdateGraphViewOverlay extends JsMessage {
open!: boolean;
}
export class UpdateGraphFadeArtwork extends JsMessage {
readonly percentage!: number;
}
export class UpdateMouseCursor extends JsMessage {
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias")
readonly cursor!: MouseCursorIcon;
@ -888,10 +896,6 @@ export class TriggerFontLoad extends JsMessage {
isDefault!: boolean;
}
export class TriggerGraphViewOverlay extends JsMessage {
open!: boolean;
}
export class TriggerVisitLink extends JsMessage {
url!: string;
}
@ -1561,12 +1565,11 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerAboutGraphiteLocalizedCommitDate,
TriggerCopyToClipboardBlobUrl,
TriggerDelayedZoomCanvasToFitAll,
TriggerFetchAndOpenDocument,
TriggerDownloadBlobUrl,
TriggerDownloadImage,
TriggerDownloadTextFile,
TriggerFetchAndOpenDocument,
TriggerFontLoad,
TriggerGraphViewOverlay,
TriggerImport,
TriggerIndexedDbRemoveDocument,
TriggerIndexedDbWriteDocument,
@ -1584,9 +1587,6 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateBox,
UpdateClickTargets,
UpdateContextMenuInformation,
UpdateInSelectedNetwork,
UpdateImportsExports,
UpdateLayerWidths,
UpdateDialogButtons,
UpdateDialogColumn1,
UpdateDialogColumn2,
@ -1598,8 +1598,13 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDocumentRulers,
UpdateDocumentScrollbars,
UpdateEyedropperSamplingState,
UpdateGraphFadeArtwork,
UpdateGraphViewOverlay,
UpdateImportsExports,
UpdateInputHints,
UpdateInSelectedNetwork,
UpdateLayersPanelOptionsLayout,
UpdateLayerWidths,
UpdateMenuBarLayout,
UpdateMouseCursor,
UpdateNodeGraph,
@ -1611,8 +1616,8 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdatePropertyPanelSectionsLayout,
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateWorkingColorsLayout,
UpdateWirePathInProgress,
UpdateWorkingColorsLayout,
UpdateZoomWithScroll,
} as const;
export type JsMessageType = keyof typeof messageMakers;

View File

@ -577,8 +577,7 @@ impl EditorHandle {
let message = NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(id),
node_type,
x: x / 24,
y: y / 24,
xy: Some((x / 24, y / 24)),
};
self.dispatch(message);
}
@ -652,10 +651,10 @@ impl EditorHandle {
self.dispatch(message);
}
/// Unpin a node given its node ID
#[wasm_bindgen(js_name = unpinNode)]
pub fn unpin_node(&self, id: u64) {
self.dispatch(DocumentMessage::SetNodePinned { node_id: NodeId(id), pinned: false });
/// Pin or unpin a node given its node ID
#[wasm_bindgen(js_name = setNodePinned)]
pub fn set_node_pinned(&self, id: u64, pinned: bool) {
self.dispatch(DocumentMessage::SetNodePinned { node_id: NodeId(id), pinned });
}
/// Delete a layer or node given its node ID