Merge remote-tracking branch 'origin/master' into circular-repeat-gizmos

This commit is contained in:
0SlowPoke0
2026-02-08 03:49:52 +05:30
959 changed files with 48232 additions and 27961 deletions
+70 -21
View File
@@ -1,24 +1,31 @@
use crate::dispatcher::Dispatcher;
use crate::messages::prelude::*;
pub use graphene_std::uuid::*;
use std::sync::OnceLock;
// TODO: serialize with serde to save the current editor state
pub struct Editor {
pub dispatcher: Dispatcher,
}
impl Editor {
/// Construct the editor.
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
pub fn new() -> Self {
pub fn new(environment: Environment, uuid_random_seed: u64) -> Self {
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
graphene_std::uuid::set_uuid_seed(uuid_random_seed);
Self { dispatcher: Dispatcher::new() }
}
#[cfg(test)]
pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) {
let _ = ENVIRONMENT.set(*Editor::environment());
graphene_std::uuid::set_uuid_seed(0);
let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
let dispatcher = Dispatcher::with_executor(executor);
(Self { dispatcher }, runtime)
let editor = Self {
dispatcher: Dispatcher::with_executor(executor),
};
(editor, runtime)
}
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
@@ -32,26 +39,68 @@ impl Editor {
}
}
impl Default for Editor {
fn default() -> Self {
Self::new()
static ENVIRONMENT: OnceLock<Environment> = OnceLock::new();
impl Editor {
#[cfg(not(test))]
pub fn environment() -> &'static Environment {
ENVIRONMENT.get().expect("Editor environment accessed before initialization")
}
#[cfg(test)]
pub fn environment() -> &'static Environment {
&Environment {
platform: Platform::Desktop,
host: Host::Linux,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Environment {
pub platform: Platform,
pub host: Host,
}
#[derive(Clone, Copy, Debug)]
pub enum Platform {
Desktop,
Web,
}
#[derive(Clone, Copy, Debug)]
pub enum Host {
Windows,
Mac,
Linux,
}
impl Environment {
pub fn is_desktop(&self) -> bool {
matches!(self.platform, Platform::Desktop)
}
pub fn is_web(&self) -> bool {
matches!(self.platform, Platform::Web)
}
pub fn is_windows(&self) -> bool {
matches!(self.host, Host::Windows)
}
pub fn is_mac(&self) -> bool {
matches!(self.host, Host::Mac)
}
pub fn is_linux(&self) -> bool {
matches!(self.host, Host::Linux)
}
}
pub const GRAPHITE_RELEASE_SERIES: &str = env!("GRAPHITE_RELEASE_SERIES");
pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE");
pub const GRAPHITE_GIT_COMMIT_BRANCH: Option<&str> = option_env!("GRAPHITE_GIT_COMMIT_BRANCH");
pub const GRAPHITE_GIT_COMMIT_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH");
pub const GRAPHITE_GIT_COMMIT_BRANCH: &str = env!("GRAPHITE_GIT_COMMIT_BRANCH");
pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE");
pub fn commit_info_localized(localized_commit_date: &str) -> String {
format!(
"Release Series: {}\n\
Branch: {}\n\
Commit: {}\n\
{}",
GRAPHITE_RELEASE_SERIES,
GRAPHITE_GIT_COMMIT_BRANCH,
&GRAPHITE_GIT_COMMIT_HASH[..8],
localized_commit_date
)
let mut info = String::new();
info.push_str(&format!("Release Series: {GRAPHITE_RELEASE_SERIES}\n"));
if let Some(branch) = GRAPHITE_GIT_COMMIT_BRANCH {
info.push_str(&format!("Branch: {branch}\n"));
}
info.push_str(&format!("Commit: {}\n", GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH)));
info.push_str(localized_commit_date);
info
}
+11
View File
@@ -56,6 +56,8 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
pub const SELECTION_TOLERANCE: f64 = 5.;
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
pub const LAYER_ORIGIN_CROSS_DIAMETER: f64 = 10.;
pub const LAYER_ORIGIN_CROSS_THICKNESS: f64 = 1.;
// PIVOT
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
@@ -122,6 +124,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
// EYEDROPPER TOOL
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
// GIZMOS
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
@@ -131,6 +136,7 @@ pub const ARC_SNAP_THRESHOLD: f64 = 5.;
pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.;
pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
pub const GRID_ROW_COLUMN_GIZMO_OFFSET: f64 = 15.;
// SCROLLBARS
pub const SCROLLBAR_SPACING: f64 = 0.1;
@@ -157,3 +163,8 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
// INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
// UI
pub const UI_SCALE_DEFAULT: f64 = 1.;
pub const UI_SCALE_MIN: f64 = 0.5;
pub const UI_SCALE_MAX: f64 = 3.;
+119 -44
View File
@@ -2,12 +2,15 @@ use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::defer::DeferMessageContext;
use crate::messages::dialog::DialogMessageContext;
use crate::messages::layout::layout_message_handler::LayoutMessageContext;
use crate::messages::preferences::preferences_message_handler::PreferencesMessageContext;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
#[derive(Debug, Default)]
pub struct Dispatcher {
message_queues: Vec<VecDeque<Message>>,
pub responses: Vec<FrontendMessage>,
pub frontend_update_messages: Vec<Message>,
pub message_handlers: DispatcherMessageHandlers,
}
@@ -16,16 +19,18 @@ pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler,
app_window_message_handler: AppWindowMessageHandler,
broadcast_message_handler: BroadcastMessageHandler,
clipboard_message_handler: ClipboardMessageHandler,
debug_message_handler: DebugMessageHandler,
defer_message_handler: DeferMessageHandler,
dialog_message_handler: DialogMessageHandler,
globals_message_handler: GlobalsMessageHandler,
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
key_mapping_message_handler: KeyMappingMessageHandler,
layout_message_handler: LayoutMessageHandler,
pub portfolio_message_handler: PortfolioMessageHandler,
menu_bar_message_handler: MenuBarMessageHandler,
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler,
viewport_message_handler: ViewportMessageHandler,
}
impl DispatcherMessageHandlers {
@@ -41,19 +46,30 @@ impl DispatcherMessageHandlers {
/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior.
/// In addition, these messages do not change any state in the backend (aside from caches).
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
NodeGraphMessageDiscriminant::RunDocumentGraph,
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
];
/// Since we don't need to update the frontend multiple times per frame,
/// we have a set of messages which we will buffer until the next frame is requested.
const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
PropertiesPanelMessageDiscriminant::Refresh,
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::UpdateDocumentWidgets),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::AutoSaveAllDocuments),
];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
@@ -100,6 +116,19 @@ impl Dispatcher {
while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) {
// Skip processing of this message if it will be processed later (at the end of the shallowest level queue)
if FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) {
let already_in_queue = self.message_queues.first().is_some_and(|queue| queue.contains(&message));
if already_in_queue {
self.cleanup_queues(false);
continue;
} else if self.message_queues.len() > 1 {
if !self.frontend_update_messages.contains(&message) {
self.frontend_update_messages.push(message);
}
self.cleanup_queues(false);
continue;
}
}
if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) {
let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some();
if already_in_queue {
@@ -123,12 +152,16 @@ impl Dispatcher {
// Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend
match message {
Message::Animation(message) => {
if let AnimationMessage::IncrementFrameCounter = &message {
self.message_queues[0].extend(self.frontend_update_messages.drain(..));
}
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
}
Message::AppWindow(message) => {
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
}
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
Message::Clipboard(message) => self.message_handlers.clipboard_message_handler.process_message(message, &mut queue, ()),
Message::Debug(message) => {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
}
@@ -142,13 +175,12 @@ impl Dispatcher {
let context = DialogMessageContext {
portfolio: &self.message_handlers.portfolio_message_handler,
preferences: &self.message_handlers.preferences_message_handler,
viewport_bounds: &self.message_handlers.input_preprocessor_message_handler.viewport_bounds,
};
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
}
Message::Frontend(message) => {
// Handle these messages immediately by returning early
if let FrontendMessage::TriggerFontLoad { .. } = message {
if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message {
self.responses.push(message);
self.cleanup_queues(false);
@@ -159,15 +191,14 @@ impl Dispatcher {
self.responses.push(message);
}
}
Message::Globals(message) => {
self.message_handlers.globals_message_handler.process_message(message, &mut queue, ());
}
Message::InputPreprocessor(message) => {
let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
self.message_handlers
.input_preprocessor_message_handler
.process_message(message, &mut queue, InputPreprocessorMessageContext { keyboard_platform });
self.message_handlers.input_preprocessor_message_handler.process_message(
message,
&mut queue,
InputPreprocessorMessageContext {
viewport: &self.message_handlers.viewport_message_handler,
},
);
}
Message::KeyMapping(message) => {
let input = &self.message_handlers.input_preprocessor_message_handler;
@@ -184,30 +215,68 @@ impl Dispatcher {
self.message_handlers.layout_message_handler.process_message(message, &mut queue, context);
}
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;
let message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity;
let reset_node_definitions_on_open = self.message_handlers.portfolio_message_handler.reset_node_definitions_on_open;
let timing_information = self.message_handlers.animation_message_handler.timing_information();
let animation = &self.message_handlers.animation_message_handler;
self.message_handlers.portfolio_message_handler.process_message(
message,
&mut queue,
PortfolioMessageContext {
ipp,
preferences,
current_tool,
message_logging_verbosity,
reset_node_definitions_on_open,
timing_information,
animation,
ipp: &self.message_handlers.input_preprocessor_message_handler,
preferences: &self.message_handlers.preferences_message_handler,
current_tool: &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type,
reset_node_definitions_on_open: self.message_handlers.portfolio_message_handler.reset_node_definitions_on_open,
timing_information: self.message_handlers.animation_message_handler.timing_information(),
animation: &self.message_handlers.animation_message_handler,
viewport: &self.message_handlers.viewport_message_handler,
},
);
}
Message::MenuBar(message) => {
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
menu_bar_message_handler.focus_document = self.message_handlers.portfolio_message_handler.focus_document;
menu_bar_message_handler.data_panel_open = self.message_handlers.portfolio_message_handler.data_panel_open;
menu_bar_message_handler.layers_panel_open = self.message_handlers.portfolio_message_handler.layers_panel_open;
menu_bar_message_handler.properties_panel_open = self.message_handlers.portfolio_message_handler.properties_panel_open;
menu_bar_message_handler.message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity;
menu_bar_message_handler.reset_node_definitions_on_open = self.message_handlers.portfolio_message_handler.reset_node_definitions_on_open;
if let Some(document) = self
.message_handlers
.portfolio_message_handler
.active_document_id
.and_then(|document_id| self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id))
{
let selected_nodes = document.network_interface.selected_nodes();
let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
menu_bar_message_handler.has_active_document = true;
menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.;
menu_bar_message_handler.canvas_flipped = document.document_ptz.flip;
menu_bar_message_handler.rulers_visible = document.rulers_visible;
menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open();
menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some();
menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some();
menu_bar_message_handler.has_selection_history = (!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty());
menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
} else {
menu_bar_message_handler.has_active_document = false;
menu_bar_message_handler.canvas_tilted = false;
menu_bar_message_handler.canvas_flipped = false;
menu_bar_message_handler.rulers_visible = false;
menu_bar_message_handler.node_graph_open = false;
menu_bar_message_handler.has_selected_nodes = false;
menu_bar_message_handler.has_selected_layers = false;
menu_bar_message_handler.has_selection_history = (false, false);
menu_bar_message_handler.make_path_editable_is_allowed = false;
}
menu_bar_message_handler.process_message(message, &mut queue, ());
}
Message::Preferences(message) => {
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
let context = PreferencesMessageContext {
tool_message_handler: &self.message_handlers.tool_message_handler,
};
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, context);
}
Message::Tool(message) => {
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
@@ -226,13 +295,17 @@ impl Dispatcher {
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler,
viewport: &self.message_handlers.viewport_message_handler,
};
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
}
Message::Viewport(message) => {
self.message_handlers.viewport_message_handler.process_message(message, &mut queue, ());
}
Message::NoOp => {}
Message::Batched { messages } => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
messages.into_iter().for_each(|message| self.handle_message(message, false));
}
}
@@ -248,15 +321,17 @@ impl Dispatcher {
pub fn collect_actions(&self) -> ActionList {
// TODO: Reduce the number of heap allocations
let mut list = Vec::new();
list.extend(self.message_handlers.app_window_message_handler.actions());
list.extend(self.message_handlers.clipboard_message_handler.actions());
list.extend(self.message_handlers.dialog_message_handler.actions());
list.extend(self.message_handlers.animation_message_handler.actions());
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 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());
}
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document()
&& !document.graph_view_overlay_open
{
list.extend(self.message_handlers.tool_message_handler.actions_with_preferences(&self.message_handlers.preferences_message_handler));
}
list.extend(self.message_handlers.portfolio_message_handler.actions());
list
@@ -284,8 +359,9 @@ impl Dispatcher {
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
let discriminant = MessageDiscriminant::from(message);
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
if !is_blocked {
if !is_blocked && !is_empty_batched {
match message_logging_verbosity {
MessageLoggingVerbosity::Off => {}
MessageLoggingVerbosity::Names => {
@@ -496,10 +572,9 @@ mod test {
"Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)",
);
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
document_name: Some(document_name.to_string()),
document_path: None,
document_serialized_content,
let responses = editor.editor.handle_message(PortfolioMessage::OpenFile {
path: file_name.into(),
content: document_serialized_content.bytes().collect(),
});
// Check if the graph renders
@@ -509,10 +584,10 @@ mod test {
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file
if let FrontendMessage::UpdateDialogColumn1 { layout_target: _, diff } = response {
if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value {
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
if let FrontendMessage::UpdateDialogColumn1 { diff } = response {
if let DiffUpdate::Layout(sub_layout) = &diff[0].new_value {
if let LayoutGroup::Row { widgets } = &sub_layout.0[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &*widgets[0].widget {
print_problem_to_terminal_on_failure(value);
}
}
@@ -101,7 +101,6 @@ impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
AnimationMessage::UpdateTime => {
if self.is_playing() {
responses.add(PortfolioMessage::SubmitActiveGraphRender);
if self.live_preview_recently_zero {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
@@ -1,12 +1,16 @@
use crate::messages::prelude::*;
use super::app_window_message_handler::AppWindowPlatform;
#[impl_message(Message, AppWindow)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AppWindowMessage {
AppWindowMinimize,
AppWindowMaximize,
AppWindowUpdatePlatform { platform: AppWindowPlatform },
AppWindowClose,
PointerLock,
PointerLockMove { x: f64, y: f64 },
Close,
Minimize,
Maximize,
Fullscreen,
Drag,
Hide,
HideOthers,
ShowAll,
}
@@ -1,45 +1,56 @@
use crate::messages::app_window::AppWindowMessage;
use crate::application::{Environment, Platform};
use crate::messages::prelude::*;
use crate::{application::Host, messages::app_window::AppWindowMessage};
use graphite_proc_macros::{ExtractField, message_handler_data};
#[derive(Debug, Clone, Default, ExtractField)]
pub struct AppWindowMessageHandler {
platform: AppWindowPlatform,
maximized: bool,
minimized: bool,
}
pub struct AppWindowMessageHandler {}
#[message_handler_data]
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
match message {
AppWindowMessage::AppWindowMaximize => {
self.maximized = !self.maximized;
responses.add(FrontendMessage::UpdateWindowState {
maximized: self.maximized,
minimized: self.minimized,
});
AppWindowMessage::PointerLock => {
responses.add(FrontendMessage::WindowPointerLock);
}
AppWindowMessage::AppWindowMinimize => {
self.minimized = !self.minimized;
responses.add(FrontendMessage::UpdateWindowState {
maximized: self.maximized,
minimized: self.minimized,
});
AppWindowMessage::PointerLockMove { x, y } => {
responses.add(FrontendMessage::WindowPointerLockMove { x, y });
}
AppWindowMessage::AppWindowUpdatePlatform { platform } => {
self.platform = platform;
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
AppWindowMessage::Close => {
responses.add(FrontendMessage::WindowClose);
}
AppWindowMessage::AppWindowClose => {
responses.add(FrontendMessage::CloseWindow);
AppWindowMessage::Minimize => {
responses.add(FrontendMessage::WindowMinimize);
}
AppWindowMessage::Maximize => {
responses.add(FrontendMessage::WindowMaximize);
}
AppWindowMessage::Fullscreen => {
responses.add(FrontendMessage::WindowFullscreen);
}
AppWindowMessage::Drag => {
responses.add(FrontendMessage::WindowDrag);
}
AppWindowMessage::Hide => {
responses.add(FrontendMessage::WindowHide);
}
AppWindowMessage::HideOthers => {
responses.add(FrontendMessage::WindowHideOthers);
}
AppWindowMessage::ShowAll => {
responses.add(FrontendMessage::WindowShowAll);
}
}
}
fn actions(&self) -> ActionList {
actions!(AppWindowMessageDiscriminant;)
}
advertise_actions!(AppWindowMessageDiscriminant;
Close,
Minimize,
Maximize,
Fullscreen,
Drag,
Hide,
HideOthers,
);
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -50,3 +61,14 @@ pub enum AppWindowPlatform {
Mac,
Linux,
}
impl From<&Environment> for AppWindowPlatform {
fn from(environment: &Environment) -> Self {
match (environment.platform, environment.host) {
(Platform::Web, _) => AppWindowPlatform::Web,
(Platform::Desktop, Host::Linux) => AppWindowPlatform::Linux,
(Platform::Desktop, Host::Mac) => AppWindowPlatform::Mac,
(Platform::Desktop, Host::Windows) => AppWindowPlatform::Windows,
}
}
}
@@ -0,0 +1,13 @@
use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw};
use crate::messages::prelude::*;
#[impl_message(Message, Clipboard)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ClipboardMessage {
Cut,
Copy,
Paste,
ReadClipboard { content: ClipboardContentRaw },
ReadSelection { content: Option<String>, cut: bool },
Write { content: ClipboardContent },
}
@@ -0,0 +1,85 @@
use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw};
use crate::messages::prelude::*;
use graphene_std::raster::Image;
use graphite_proc_macros::{ExtractField, message_handler_data};
const CLIPBOARD_PREFIX_LAYER: &str = "graphite/layer: ";
const CLIPBOARD_PREFIX_NODES: &str = "graphite/nodes: ";
const CLIPBOARD_PREFIX_VECTOR: &str = "graphite/vector: ";
#[derive(Debug, Clone, Default, ExtractField)]
pub struct ClipboardMessageHandler {}
#[message_handler_data]
impl MessageHandler<ClipboardMessage, ()> for ClipboardMessageHandler {
fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
match message {
ClipboardMessage::Cut => responses.add(FrontendMessage::TriggerSelectionRead { cut: true }),
ClipboardMessage::Copy => responses.add(FrontendMessage::TriggerSelectionRead { cut: false }),
ClipboardMessage::Paste => responses.add(FrontendMessage::TriggerClipboardRead),
ClipboardMessage::ReadClipboard { content } => match content {
ClipboardContentRaw::Text(text) => {
if let Some(layer) = text.strip_prefix(CLIPBOARD_PREFIX_LAYER) {
responses.add(PortfolioMessage::PasteSerializedData { data: layer.to_string() });
} else if let Some(nodes) = text.strip_prefix(CLIPBOARD_PREFIX_NODES) {
responses.add(NodeGraphMessage::PasteNodes { serialized_nodes: nodes.to_string() });
} else if let Some(vector) = text.strip_prefix(CLIPBOARD_PREFIX_VECTOR) {
responses.add(PortfolioMessage::PasteSerializedVector { data: vector.to_string() });
} else {
responses.add(FrontendMessage::TriggerSelectionWrite { content: text });
}
}
ClipboardContentRaw::Svg(svg) => {
responses.add(PortfolioMessage::PasteSvg {
svg,
name: None,
mouse: None,
parent_and_insert_index: None,
});
}
ClipboardContentRaw::Image { data, width, height } => {
responses.add(PortfolioMessage::PasteImage {
image: Image::from_image_data(&data, width, height),
name: None,
mouse: None,
parent_and_insert_index: None,
});
}
},
ClipboardMessage::ReadSelection { content, cut } => {
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
if let Some(text) = content {
responses.add(ClipboardMessage::Write {
content: ClipboardContent::Text(text),
});
} else if cut {
responses.add(PortfolioMessage::Cut { clipboard: Clipboard::Device });
} else {
responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Device });
}
}
ClipboardMessage::Write { content } => {
let text = match content {
ClipboardContent::Svg(_) => {
log::error!("SVG copying is not yet supported");
return;
}
ClipboardContent::Image { .. } => {
log::error!("Image copying is not yet supported");
return;
}
ClipboardContent::Layer(layer) => format!("{CLIPBOARD_PREFIX_LAYER}{layer}"),
ClipboardContent::Nodes(nodes) => format!("{CLIPBOARD_PREFIX_NODES}{nodes}"),
ClipboardContent::Vector(vector) => format!("{CLIPBOARD_PREFIX_VECTOR}{vector}"),
ClipboardContent::Text(text) => text,
};
responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
}
}
}
advertise_actions!(ClipboardMessageDiscriminant;
Cut,
Copy,
Paste,
);
}
+8
View File
@@ -0,0 +1,8 @@
mod clipboard_message;
pub mod clipboard_message_handler;
pub mod utility_types;
#[doc(inline)]
pub use clipboard_message::{ClipboardMessage, ClipboardMessageDiscriminant};
#[doc(inline)]
pub use clipboard_message_handler::ClipboardMessageHandler;
@@ -0,0 +1,16 @@
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ClipboardContentRaw {
Text(String),
Svg(String),
Image { data: Vec<u8>, width: u32, height: u32 },
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ClipboardContent {
Layer(String),
Nodes(String),
Vector(String),
Text(String),
Svg(String),
Image { data: Vec<u8>, width: u32, height: u32 },
}
@@ -25,9 +25,6 @@ pub enum DialogMessage {
localized_commit_date: String,
localized_commit_year: String,
},
RequestComingSoonDialog {
issue: Option<u32>,
},
RequestDemoArtworkDialog,
RequestExportDialog,
RequestLicensesDialogWithLocalizedCommitDate {
@@ -1,14 +1,12 @@
use super::new_document_dialog::NewDocumentDialogMessageContext;
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
use super::simple_dialogs::{self, AboutGraphiteDialog, DemoArtworkDialog, LicensesDialog};
use crate::application::GRAPHITE_GIT_COMMIT_DATE;
use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct DialogMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
pub viewport_bounds: &'a ViewportBounds,
pub preferences: &'a PreferencesMessageHandler,
}
@@ -23,15 +21,11 @@ pub struct DialogMessageHandler {
#[message_handler_data]
impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHandler {
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, context: DialogMessageContext) {
let DialogMessageContext {
portfolio,
preferences,
viewport_bounds,
} = context;
let DialogMessageContext { portfolio, preferences } = context;
match message {
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ExportDialogMessageContext { portfolio }),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, NewDocumentDialogMessageContext { viewport_bounds }),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
DialogMessage::CloseAllDocumentsWithConfirmation => {
@@ -55,7 +49,7 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
}
DialogMessage::RequestAboutGraphiteDialog => {
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
commit_date: GRAPHITE_GIT_COMMIT_DATE.into(),
});
}
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
@@ -69,10 +63,6 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
dialog.send_dialog_to_frontend(responses);
}
DialogMessage::RequestComingSoonDialog { issue } => {
let dialog = ComingSoonDialog { issue };
dialog.send_dialog_to_frontend(responses);
}
DialogMessage::RequestDemoArtworkDialog => {
let dialog = DemoArtworkDialog;
dialog.send_dialog_to_frontend(responses);
@@ -43,13 +43,21 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
file_type: self.file_type,
scale_factor: self.scale_factor,
bounds: self.bounds,
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
}),
ExportDialogMessage::Submit => {
let artboard_name = match self.bounds {
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
_ => None,
};
responses.add_front(PortfolioMessage::SubmitDocumentExport {
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
file_type: self.file_type,
scale_factor: self.scale_factor,
bounds: self.bounds,
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
artboard_name,
artboard_count: self.artboards.len(),
})
}
}
self.send_dialog_to_frontend(responses);
@@ -72,11 +80,11 @@ impl DialogLayoutHolder for ExportDialogMessageHandler {
}
.into()
})
.widget_holder(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
.widget_instance(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
@@ -92,14 +100,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
.collect();
let export_type = vec![
TextLabel::new("File Type").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
TextLabel::new("File Type").table_align(true).min_width(100).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_instance(),
];
let resolution = vec![
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(self.scale_factor))
.unit("")
.min(0.)
@@ -107,7 +115,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
.disabled(self.file_type == FileType::Svg)
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
.min_width(200)
.widget_holder(),
.widget_instance(),
];
let standard_bounds = vec![
@@ -144,27 +152,27 @@ impl LayoutHolder for ExportDialogMessageHandler {
}
let export_area = vec![
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
TextLabel::new("Bounds").table_align(true).min_width(100).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
];
let checkbox_id = CheckboxId::new();
let transparent_background = vec![
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(self.transparent_background)
.disabled(self.file_type == FileType::Jpg)
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
.widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row { widgets: export_type },
LayoutGroup::Row { widgets: resolution },
LayoutGroup::Row { widgets: export_area },
LayoutGroup::Row { widgets: transparent_background },
]))
])
}
}
@@ -4,4 +4,4 @@ mod new_document_dialog_message_handler;
#[doc(inline)]
pub use new_document_dialog_message::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant};
#[doc(inline)]
pub use new_document_dialog_message_handler::{NewDocumentDialogMessageContext, NewDocumentDialogMessageHandler};
pub use new_document_dialog_message_handler::NewDocumentDialogMessageHandler;
@@ -1,13 +1,8 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use crate::messages::{input_mapper::utility_types::input_mouse::ViewportBounds, layout::utility_types::widget_prelude::*};
use glam::{IVec2, UVec2};
use graph_craft::document::NodeId;
#[derive(ExtractField)]
pub struct NewDocumentDialogMessageContext<'a> {
pub viewport_bounds: &'a ViewportBounds,
}
/// A dialog to allow users to set some initial options about a new document.
#[derive(Debug, Clone, Default, ExtractField)]
pub struct NewDocumentDialogMessageHandler {
@@ -17,8 +12,8 @@ pub struct NewDocumentDialogMessageHandler {
}
#[message_handler_data]
impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContext<'a>> for NewDocumentDialogMessageHandler {
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, context: NewDocumentDialogMessageContext<'a>) {
impl<'a> MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHandler {
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
NewDocumentDialogMessage::Name { name } => self.name = name,
NewDocumentDialogMessage::Infinite { infinite } => self.infinite = infinite,
@@ -35,16 +30,15 @@ impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContex
});
responses.add(NavigationMessage::CanvasPan { delta: self.dimensions.as_dvec2() });
responses.add(NodeGraphMessage::RunDocumentGraph);
// If we already have bounds, we won't receive a viewport bounds update so we just fabricate one ourselves
if *context.viewport_bounds != ViewportBounds::default() {
responses.add(InputPreprocessorMessage::BoundsOfViewports {
bounds_of_viewports: vec![context.viewport_bounds.clone()],
});
}
responses.add(ViewportMessage::RepropagateUpdate);
responses.add(DeferMessage::AfterNavigationReady {
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()],
});
}
responses.add(DocumentMessage::MarkAsSaved);
}
}
@@ -68,38 +62,38 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
}
.into()
})
.widget_holder(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
.widget_instance(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
impl LayoutHolder for NewDocumentDialogMessageHandler {
fn layout(&self) -> Layout {
let name = vec![
TextLabel::new("Name").table_align(true).min_width("90px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Name").table_align(true).min_width(90).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextInput::new(&self.name)
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
.widget_holder(),
.widget_instance(),
];
let checkbox_id = CheckboxId::new();
let infinite = vec![
TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(self.infinite)
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
.widget_instance(),
];
let scale = vec![
TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(self.dimensions.x as f64))
.label("W")
.unit(" px")
@@ -109,8 +103,8 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
.disabled(self.infinite)
.min_width(100)
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(self.dimensions.y as f64))
.label("H")
.unit(" px")
@@ -120,13 +114,9 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
.disabled(self.infinite)
.min_width(100)
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY { height: number_input.value.unwrap() }.into())
.widget_holder(),
.widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: name },
LayoutGroup::Row { widgets: infinite },
LayoutGroup::Row { widgets: scale },
]))
Layout(vec![LayoutGroup::Row { widgets: name }, LayoutGroup::Row { widgets: infinite }, LayoutGroup::Row { widgets: scale }])
}
}
@@ -36,186 +36,255 @@ impl PreferencesDialogMessageHandler {
const TITLE: &'static str = "Editor Preferences";
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
let mut rows = Vec::new();
// ==========
// NAVIGATION
// ==========
{
let header = vec![TextLabel::new("Navigation").italic(true).widget_instance()];
let navigation_header = vec![TextLabel::new("Navigation").italic(true).widget_holder()];
let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50).";
let zoom_rate_label = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
];
let zoom_rate = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
.tooltip_label("Zoom Rate")
.tooltip_description(zoom_rate_description)
.mode_range()
.int()
.min(1.)
.max(100.)
.on_update(|number_input: &NumberInput| {
if let Some(display_value) = number_input.value {
let actual_rate = map_display_to_zoom_rate(display_value);
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into()
} else {
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into()
}
})
.widget_instance(),
];
let zoom_rate_tooltip = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50)";
let zoom_rate_label = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Zoom Rate").tooltip(zoom_rate_tooltip).widget_holder(),
];
let zoom_rate = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
.tooltip(zoom_rate_tooltip)
.mode_range()
.int()
.min(1.)
.max(100.)
.on_update(|number_input: &NumberInput| {
if let Some(display_value) = number_input.value {
let actual_rate = map_display_to_zoom_rate(display_value);
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into()
} else {
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into()
}
})
.widget_holder(),
];
let checkbox_id = CheckboxId::new();
let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).";
let zoom_with_scroll = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(preferences.zoom_with_scroll)
.tooltip_label("Zoom with Scroll")
.tooltip_description(zoom_with_scroll_description)
.on_update(|checkbox_input: &CheckboxInput| {
PreferencesMessage::ModifyLayout {
zoom_with_scroll: checkbox_input.checked,
}
.into()
})
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Zoom with Scroll")
.tooltip_label("Zoom with Scroll")
.tooltip_description(zoom_with_scroll_description)
.for_checkbox(checkbox_id)
.widget_instance(),
];
let checkbox_id = CheckboxId::new();
let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)";
let zoom_with_scroll = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(preferences.zoom_with_scroll)
.tooltip(zoom_with_scroll_tooltip)
.on_update(|checkbox_input: &CheckboxInput| {
PreferencesMessage::ModifyLayout {
zoom_with_scroll: checkbox_input.checked,
}
.into()
})
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Zoom with Scroll")
.table_align(true)
.tooltip(zoom_with_scroll_tooltip)
.for_checkbox(checkbox_id)
.widget_holder(),
];
rows.extend_from_slice(&[header, zoom_rate_label, zoom_rate, zoom_with_scroll]);
}
// =======
// EDITING
// =======
{
let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
let editing_header = vec![TextLabel::new("Editing").italic(true).widget_holder()];
let selection_label = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextLabel::new("Selection")
.tooltip_label("Selection")
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
.widget_instance(),
];
let selection_label = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Selection").widget_holder(),
];
let selection_mode = RadioInput::new(vec![
RadioEntryData::new(SelectionMode::Touched.to_string())
.label(SelectionMode::Touched.to_string())
.tooltip_label(SelectionMode::Touched.to_string())
.tooltip_description(SelectionMode::Touched.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Touched,
}
.into()
}),
RadioEntryData::new(SelectionMode::Enclosed.to_string())
.label(SelectionMode::Enclosed.to_string())
.tooltip_label(SelectionMode::Enclosed.to_string())
.tooltip_description(SelectionMode::Enclosed.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Enclosed,
}
.into()
}),
RadioEntryData::new(SelectionMode::Directional.to_string())
.label(SelectionMode::Directional.to_string())
.tooltip_label(SelectionMode::Directional.to_string())
.tooltip_description(SelectionMode::Directional.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Directional,
}
.into()
}),
])
.selected_index(Some(preferences.selection_mode as u32))
.widget_instance();
let selection_mode = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
selection_mode,
];
let selection_mode = RadioInput::new(vec![
RadioEntryData::new(SelectionMode::Touched.to_string())
.label(SelectionMode::Touched.to_string())
.tooltip(SelectionMode::Touched.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Touched,
}
.into()
}),
RadioEntryData::new(SelectionMode::Enclosed.to_string())
.label(SelectionMode::Enclosed.to_string())
.tooltip(SelectionMode::Enclosed.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Enclosed,
}
.into()
}),
RadioEntryData::new(SelectionMode::Directional.to_string())
.label(SelectionMode::Directional.to_string())
.tooltip(SelectionMode::Directional.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Directional,
}
.into()
}),
])
.selected_index(Some(preferences.selection_mode as u32))
.widget_holder();
let selection_mode = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
selection_mode,
];
rows.extend_from_slice(&[header, selection_label, selection_mode]);
}
// =========
// INTERFACE
// =========
#[cfg(not(target_family = "wasm"))]
{
let header = vec![TextLabel::new("Interface").italic(true).widget_instance()];
let scale_description = "Adjust the scale of the entire user interface (100% is default).";
let scale_label = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
];
let scale = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
.tooltip_label("Scale")
.tooltip_description(scale_description)
.mode_range()
.int()
.min(ui_scale_to_display(crate::consts::UI_SCALE_MIN))
.max(ui_scale_to_display(crate::consts::UI_SCALE_MAX))
.unit("%")
.on_update(|number_input: &NumberInput| {
if let Some(display_value) = number_input.value {
let scale = map_display_to_ui_scale(display_value);
PreferencesMessage::UIScale { scale }.into()
} else {
PreferencesMessage::UIScale {
scale: crate::consts::UI_SCALE_DEFAULT,
}
.into()
}
})
.widget_instance(),
];
rows.extend_from_slice(&[header, scale_label, scale]);
}
// ============
// EXPERIMENTAL
// ============
{
let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
let experimental_header = vec![TextLabel::new("Experimental").italic(true).widget_holder()];
let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph.";
let node_graph_wires_label = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextLabel::new("Node Graph Wires")
.tooltip_label("Node Graph Wires")
.tooltip_description(node_graph_section_description)
.widget_instance(),
];
let graph_wire_style = RadioInput::new(vec![
RadioEntryData::new(GraphWireStyle::Direct.to_string())
.label(GraphWireStyle::Direct.to_string())
.tooltip_label(GraphWireStyle::Direct.to_string())
.tooltip_description(GraphWireStyle::Direct.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
.label(GraphWireStyle::GridAligned.to_string())
.tooltip_label(GraphWireStyle::GridAligned.to_string())
.tooltip_description(GraphWireStyle::GridAligned.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
])
.selected_index(Some(preferences.graph_wire_style as u32))
.widget_instance();
let graph_wire_style = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
graph_wire_style,
];
let node_graph_section_tooltip = "Appearance of the wires running between node connections in the graph";
let node_graph_wires_label = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Node Graph Wires").tooltip(node_graph_section_tooltip).widget_holder(),
];
let graph_wire_style = RadioInput::new(vec![
RadioEntryData::new(GraphWireStyle::Direct.to_string())
.label(GraphWireStyle::Direct.to_string())
.tooltip(GraphWireStyle::Direct.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
.label(GraphWireStyle::GridAligned.to_string())
.tooltip(GraphWireStyle::GridAligned.tooltip_description())
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
])
.selected_index(Some(preferences.graph_wire_style as u32))
.widget_holder();
let graph_wire_style = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
graph_wire_style,
];
let checkbox_id = CheckboxId::new();
let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string();
#[cfg(target_family = "wasm")]
let mut vello_description = vello_description;
#[cfg(target_family = "wasm")]
vello_description.push_str("\n\n(Your browser must support WebGPU.)");
let checkbox_id = CheckboxId::new();
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
let use_vello = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Vello Renderer")
.table_align(true)
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.for_checkbox(checkbox_id)
.widget_holder(),
];
let use_vello = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
.tooltip_label("Vello Renderer")
.tooltip_description(vello_description.clone())
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Vello Renderer")
.tooltip_label("Vello Renderer")
.tooltip_description(vello_description)
.disabled(!preferences.supports_wgpu())
.for_checkbox(checkbox_id)
.widget_instance(),
];
let checkbox_id = CheckboxId::new();
let vector_mesh_tooltip =
"Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills.";
let vector_meshes = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).for_checkbox(checkbox_id).widget_holder(),
];
let checkbox_id = CheckboxId::new();
let brush_tool_description = "
Enable the Brush tool to support basic raster-based layer painting.\n\
\n\
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing.\n\
\n\
Content created with the Brush tool may not be compatible with future versions of Graphite.
"
.trim();
let brush_tool = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(preferences.brush_tool)
.tooltip_label("Brush Tool")
.tooltip_description(brush_tool_description)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::BrushTool { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id)
.widget_instance(),
TextLabel::new("Brush Tool")
.tooltip_label("Brush Tool")
.tooltip_description(brush_tool_description)
.for_checkbox(checkbox_id)
.widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: navigation_header },
LayoutGroup::Row { widgets: zoom_rate_label },
LayoutGroup::Row { widgets: zoom_rate },
LayoutGroup::Row { widgets: zoom_with_scroll },
LayoutGroup::Row { widgets: editing_header },
LayoutGroup::Row { widgets: selection_label },
LayoutGroup::Row { widgets: selection_mode },
LayoutGroup::Row { widgets: experimental_header },
LayoutGroup::Row { widgets: node_graph_wires_label },
LayoutGroup::Row { widgets: graph_wire_style },
LayoutGroup::Row { widgets: use_vello },
LayoutGroup::Row { widgets: vector_meshes },
]))
rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, brush_tool]);
}
Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect())
}
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
@@ -246,11 +315,11 @@ impl PreferencesDialogMessageHandler {
}
.into()
})
.widget_holder(),
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(),
.widget_instance(),
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
fn send_layout_buttons(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
@@ -287,3 +356,15 @@ fn map_zoom_rate_to_display(rate: f64) -> f64 {
let display = 50. + distance_from_reference;
display.clamp(1., 100.).round()
}
/// Maps display values in percent to actual ui scale.
#[cfg(not(target_family = "wasm"))]
fn map_display_to_ui_scale(display: f64) -> f64 {
display / 100.
}
/// Maps actual ui scale back to display values in percent.
#[cfg(not(target_family = "wasm"))]
fn ui_scale_to_display(scale: f64) -> f64 {
scale * 100.
}
@@ -13,16 +13,16 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
const TITLE: &'static str = "About Graphite";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
fn layout_column_2(&self) -> Layout {
let links = [
("Heart", "Donate", "https://graphite.rs/donate/"),
("Volunteer", "Volunteer", "https://graphite.rs/volunteer/"),
("GraphiteLogo", "Website", "https://graphite.rs"),
("Heart", "Donate", "https://graphite.art/donate/"),
("GraphiteLogo", "Website", "https://graphite.art"),
("Volunteer", "Volunteer", "https://graphite.art/volunteer/"),
("Credits", "Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"),
];
let mut widgets = links
@@ -32,7 +32,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
.icon(Some(icon.into()))
.flush(true)
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
.widget_holder()
.widget_instance()
})
.collect::<Vec<_>>();
@@ -48,25 +48,25 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
}
.into()
})
.widget_holder(),
.widget_instance(),
);
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
Layout(vec![LayoutGroup::Column { widgets }])
}
}
impl LayoutHolder for AboutGraphiteDialog {
fn layout(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("About this release").bold(true).widget_holder()],
widgets: vec![TextLabel::new("About this release").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_holder()],
widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_instance()],
},
]))
])
}
}
@@ -20,11 +20,11 @@ impl DialogLayoutHolder for CloseAllDocumentsDialog {
}
.into()
})
.widget_holder(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
.widget_instance(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
@@ -32,13 +32,13 @@ impl LayoutHolder for CloseAllDocumentsDialog {
fn layout(&self) -> Layout {
let unsaved_list = "".to_string() + &self.unsaved_document_names.join("\n");
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_holder()],
widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_instance()],
},
]))
])
}
}
@@ -23,7 +23,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
}
.into()
})
.widget_holder(),
.widget_instance(),
TextButton::new("Discard")
.on_update(move |_| {
DialogMessage::CloseDialogAndThen {
@@ -31,11 +31,11 @@ impl DialogLayoutHolder for CloseDocumentDialog {
}
.into()
})
.widget_holder(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
.widget_instance(),
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
@@ -51,13 +51,13 @@ impl LayoutHolder for CloseDocumentDialog {
let break_lines = if self.document_name.len() > max_one_line_length { '\n' } else { ' ' };
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()],
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_instance()],
},
]))
])
}
}
@@ -1,48 +0,0 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
pub struct ComingSoonDialog {
pub issue: Option<u32>,
}
impl DialogLayoutHolder for ComingSoonDialog {
const ICON: &'static str = "Delay";
const TITLE: &'static str = "Coming Soon";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
impl LayoutHolder for ComingSoonDialog {
fn layout(&self) -> Layout {
let header = vec![TextLabel::new("You've stumbled upon a placeholder").bold(true).widget_holder()];
let row1 = vec![TextLabel::new("This feature is not implemented yet.").widget_holder()];
let mut rows = vec![LayoutGroup::Row { widgets: header }, LayoutGroup::Row { widgets: row1 }];
if let Some(issue) = self.issue {
let row2 = vec![TextLabel::new("But you can help build it! Visit its issue:").widget_holder()];
let row3 = vec![
TextButton::new(format!("GitHub Issue #{issue}"))
.icon(Some("Website".into()))
.flush(true)
.on_update(move |_| {
FrontendMessage::TriggerVisitLink {
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
}
.into()
})
.widget_holder(),
];
rows.push(LayoutGroup::Row { widgets: row2 });
rows.push(LayoutGroup::Row { widgets: row3 });
}
Layout::WidgetLayout(WidgetLayout::new(rows))
}
}
@@ -20,9 +20,9 @@ impl DialogLayoutHolder for DemoArtworkDialog {
const TITLE: &'static str = "Demo Artwork";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
@@ -46,12 +46,12 @@ impl LayoutHolder for DemoArtworkDialog {
let images = chunk
.iter()
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width(Some("256px".into())).on_update(|_| make_dialog(name, filename)).widget_holder())
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width(Some("256px".into())).on_update(|_| make_dialog(name, filename)).widget_instance())
.collect();
let buttons = chunk
.iter()
.map(|(name, _, filename)| TextButton::new(*name).min_width(256).flush(true).on_update(|_| make_dialog(name, filename)).widget_holder())
.map(|(name, _, filename)| TextButton::new(*name).min_width(256).flush(true).on_update(|_| make_dialog(name, filename)).widget_instance())
.collect();
vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }, LayoutGroup::Row { widgets: vec![] }]
@@ -59,6 +59,6 @@ impl LayoutHolder for DemoArtworkDialog {
.collect();
let _ = rows_of_images_with_buttons.pop();
Layout::WidgetLayout(WidgetLayout::new(rows_of_images_with_buttons))
Layout(rows_of_images_with_buttons)
}
}
@@ -12,21 +12,21 @@ impl DialogLayoutHolder for ErrorDialog {
const TITLE: &'static str = "Error";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
impl LayoutHolder for ErrorDialog {
fn layout(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new(&self.title).bold(true).widget_holder()],
widgets: vec![TextLabel::new(&self.title).bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(&self.description).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(&self.description).multiline(true).widget_instance()],
},
]))
])
}
}
@@ -10,63 +10,58 @@ impl DialogLayoutHolder for LicensesDialog {
const TITLE: &'static str = "Licenses";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
fn layout_column_2(&self) -> Layout {
#[allow(clippy::type_complexity)]
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
("GraphiteLogo", "Graphite Logo", || {
("Code", "Source Code License", || {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.rs/logo/".into(),
url: "https://graphite.art/license#source-code".into(),
}
.into()
}),
("IconsGrid", "Graphite Icons", || {
("GraphiteLogo", "Branding License", || {
FrontendMessage::TriggerVisitLink {
url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(),
url: "https://graphite.art/license#branding".into(),
}
.into()
}),
("License", "Graphite License", || {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.rs/license/".into(),
}
.into()
}),
("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
("IconsGrid", "Dependency Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
];
let widgets = button_definitions
.iter()
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_holder())
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_instance())
.collect();
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
Layout(vec![LayoutGroup::Column { widgets }])
}
}
impl LayoutHolder for LicensesDialog {
fn layout(&self) -> Layout {
let description = concat!(
"The Graphite logo and brand identity are copyright © [YEAR]\nGraphite Labs, LLC. See \"Graphite Logo\" for usage policy.",
"\n\n",
"The Graphite editor's icons and design assets are copyright\n© [YEAR] Graphite Labs, LLC. See \"Graphite Icons\" for details.",
"\n\n",
"Graphite code is copyright © [YEAR] Graphite contributors\nand is made available under the Apache 2.0 license. See\n\"Graphite License\" for details.",
"\n\n",
"Graphite is distributed with third-party open source code\ndependencies. See \"Other Licenses\" for details.",
)
.replace("[YEAR]", &self.localized_commit_year);
let year = &self.localized_commit_year;
let description = format!(
"
Graphite source code is copyright © {year} Graphite contrib-\nutors and is available under the Apache License 2.0. See\n\"Source Code License\" for details.\n\
\n\
The Graphite logo, icons, and visual identity are copyright ©\n{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
\n\
Graphite is distributed with third-party open source code\ndependencies. See \"Dependency Licenses\" for details.
"
);
let description = description.trim();
Layout::WidgetLayout(WidgetLayout::new(vec![
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_holder()],
widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(description).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(description).multiline(true).widget_instance()],
},
]))
])
}
}
@@ -10,9 +10,9 @@ impl DialogLayoutHolder for LicensesThirdPartyDialog {
const TITLE: &'static str = "Third-Party Software License Notices";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
Layout(vec![LayoutGroup::Row { widgets }])
}
}
@@ -29,16 +29,16 @@ impl LayoutHolder for LicensesThirdPartyDialog {
};
// Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count()).max().unwrap_or(0) + 2 + 1;
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count() as u32).max().unwrap_or(0) + 2 + 1;
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
Layout(vec![LayoutGroup::Row {
widgets: vec![
TextLabel::new(license_text)
.monospace(true)
.multiline(true)
.min_width(format!("{non_wrapping_column_width}ch"))
.widget_holder(),
.min_width_characters(non_wrapping_column_width)
.widget_instance(),
],
}]))
}])
}
}
@@ -1,7 +1,6 @@
mod about_graphite_dialog;
mod close_all_documents_dialog;
mod close_document_dialog;
mod coming_soon_dialog;
mod demo_artwork_dialog;
mod error_dialog;
mod licenses_dialog;
@@ -10,7 +9,6 @@ mod licenses_third_party_dialog;
pub use about_graphite_dialog::AboutGraphiteDialog;
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
pub use close_document_dialog::CloseDocumentDialog;
pub use coming_soon_dialog::ComingSoonDialog;
pub use demo_artwork_dialog::ARTWORK;
pub use demo_artwork_dialog::DemoArtworkDialog;
pub use error_dialog::ErrorDialog;
+101 -64
View File
@@ -1,13 +1,15 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform,
};
use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer};
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use glam::IVec2;
use graph_craft::document::NodeId;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
@@ -38,7 +40,8 @@ pub enum FrontendMessage {
#[serde(rename = "fontSize")]
font_size: f64,
color: Color,
url: String,
#[serde(rename = "fontData")]
font_data: Vec<u8>,
transform: [f64; 6],
#[serde(rename = "maxWidth")]
max_width: Option<f64>,
@@ -46,6 +49,10 @@ pub enum FrontendMessage {
max_height: Option<f64>,
align: TextAlign,
},
DisplayEditableTextboxUpdateFontData {
#[serde(rename = "fontData")]
font_data: Vec<u8>,
},
DisplayEditableTextboxTransform {
transform: [f64; 6],
},
@@ -58,8 +65,19 @@ pub enum FrontendMessage {
#[serde(rename = "nodeTypes")]
node_types: Vec<FrontendNodeType>,
},
SendShortcutFullscreen {
shortcut: Option<ActionShortcut>,
#[serde(rename = "shortcutMac")]
shortcut_mac: Option<ActionShortcut>,
},
SendShortcutAltClick {
shortcut: Option<ActionShortcut>,
},
SendShortcutShiftClick {
shortcut: Option<ActionShortcut>,
},
// Trigger prefix: cause a browser API to do something
// Trigger prefix: cause a frontend specific API to do something
TriggerAboutGraphiteLocalizedCommitDate {
#[serde(rename = "commitDate")]
commit_date: String,
@@ -85,23 +103,27 @@ pub enum FrontendMessage {
name: String,
filename: String,
},
TriggerFontLoad {
TriggerFontCatalogLoad,
TriggerFontDataLoad {
font: Font,
url: String,
},
TriggerImport,
TriggerIndexedDbRemoveDocument {
TriggerPersistenceRemoveDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
},
TriggerIndexedDbWriteDocument {
TriggerPersistenceWriteDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
document: String,
details: FrontendDocumentDetails,
details: DocumentDetails,
},
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerOpenLaunchDocuments,
TriggerLoadPreferences,
TriggerOpenDocument,
TriggerPaste,
TriggerOpen,
TriggerImport,
TriggerSavePreferences {
preferences: PreferencesMessageHandler,
},
@@ -110,13 +132,19 @@ pub enum FrontendMessage {
document_id: DocumentId,
},
TriggerTextCommit,
TriggerTextCopy {
#[serde(rename = "copyText")]
copy_text: String,
},
TriggerVisitLink {
url: String,
},
TriggerClipboardRead,
TriggerClipboardWrite {
content: String,
},
TriggerSelectionRead {
cut: bool,
},
TriggerSelectionWrite {
content: String,
},
// Update prefix: give the frontend a new value or state for it to use
UpdateActiveDocument {
@@ -124,12 +152,19 @@ pub enum FrontendMessage {
document_id: DocumentId,
},
UpdateImportsExports {
imports: Vec<(FrontendGraphOutput, i32, i32)>,
exports: Vec<(FrontendGraphInput, i32, i32)>,
#[serde(rename = "addImport")]
add_import: Option<(i32, i32)>,
#[serde(rename = "addExport")]
add_export: Option<(i32, i32)>,
/// If the primary import is not visible, then it is None.
imports: Vec<Option<FrontendGraphOutput>>,
/// If the primary export is not visible, then it is None.
exports: Vec<Option<FrontendGraphInput>>,
/// The primary import location.
#[serde(rename = "importPosition")]
import_position: IVec2,
/// The primary export location.
#[serde(rename = "exportPosition")]
export_position: IVec2,
/// The document network does not have an add import or export button.
#[serde(rename = "addImportExport")]
add_import_export: bool,
},
UpdateInSelectedNetwork {
#[serde(rename = "inSelectedNetwork")]
@@ -160,8 +195,6 @@ pub enum FrontendMessage {
open: bool,
},
UpdateDataPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateImportReorderIndex {
@@ -181,18 +214,12 @@ pub enum FrontendMessage {
has_left_input_wire: HashMap<NodeId, bool>,
},
UpdateDialogButtons {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDialogColumn1 {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDialogColumn2 {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentArtwork {
@@ -202,8 +229,6 @@ pub enum FrontendMessage {
image_data: Vec<(u64, Image<Color>)>,
},
UpdateDocumentBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentLayerDetails {
@@ -217,11 +242,6 @@ pub enum FrontendMessage {
#[serde(rename = "dataBuffer")]
data_buffer: JsRawBuffer,
},
UpdateDocumentModeLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentRulers {
origin: (f64, f64),
spacing: f64,
@@ -234,6 +254,7 @@ pub enum FrontendMessage {
multiplier: (f64, f64),
},
UpdateEyedropperSamplingState {
image: Option<EyedropperPreviewImage>,
#[serde(rename = "mousePosition")]
mouse_position: Option<(f64, f64)>,
#[serde(rename = "primaryColor")]
@@ -246,29 +267,17 @@ pub enum FrontendMessage {
UpdateGraphFadeArtwork {
percentage: f64,
},
UpdateInputHints {
#[serde(rename = "hintData")]
hint_data: HintData,
},
UpdateLayersPanelControlBarLeftLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelControlBarRightLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelBottomBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateMenuBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: Vec<MenuBarEntry>,
diff: Vec<WidgetDiff>,
},
UpdateMouseCursor {
cursor: MouseCursorIcon,
@@ -276,6 +285,9 @@ pub enum FrontendMessage {
UpdateNodeGraphNodes {
nodes: Vec<FrontendNode>,
},
UpdateNodeGraphErrorDiagnostic {
error: Option<NodeGraphErrorDiagnostic>,
},
UpdateVisibleNodes {
nodes: Vec<NodeId>,
},
@@ -284,8 +296,6 @@ pub enum FrontendMessage {
},
ClearAllNodeGraphWires,
UpdateNodeGraphControlBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateNodeGraphSelection {
@@ -300,47 +310,74 @@ pub enum FrontendMessage {
},
UpdateOpenDocumentsList {
#[serde(rename = "openDocuments")]
open_documents: Vec<FrontendDocumentDetails>,
open_documents: Vec<OpenDocument>,
},
UpdatePropertiesPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateToolOptionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateToolShelfLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateWirePathInProgress {
#[serde(rename = "wirePath")]
wire_path: Option<WirePath>,
},
UpdateWelcomeScreenButtonsLayout {
diff: Vec<WidgetDiff>,
},
UpdateStatusBarHintsLayout {
diff: Vec<WidgetDiff>,
},
UpdateStatusBarInfoLayout {
diff: Vec<WidgetDiff>,
},
UpdateWorkingColorsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdatePlatform {
platform: AppWindowPlatform,
},
UpdateWindowState {
UpdateMaximized {
maximized: bool,
minimized: bool,
},
CloseWindow,
UpdateFullscreen {
fullscreen: bool,
},
UpdateViewportHolePunch {
active: bool,
},
UpdateViewportPhysicalBounds {
x: f64,
y: f64,
width: f64,
height: f64,
},
UpdateUIScale {
scale: f64,
},
#[cfg(not(target_family = "wasm"))]
RenderOverlays {
#[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
context: OverlayContext,
},
// Window prefix: cause the application window to do something
WindowPointerLock,
WindowPointerLockMove {
x: f64,
y: f64,
},
WindowClose,
WindowMinimize,
WindowMaximize,
WindowFullscreen,
WindowDrag,
WindowHide,
WindowHideOthers,
WindowShowAll,
}
+20 -5
View File
@@ -1,14 +1,22 @@
use std::path::PathBuf;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendDocumentDetails {
#[serde(rename = "isAutoSaved")]
pub is_auto_saved: bool,
pub struct OpenDocument {
pub id: DocumentId,
pub details: DocumentDetails,
}
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct DocumentDetails {
pub name: String,
pub path: Option<PathBuf>,
#[serde(rename = "isSaved")]
pub is_saved: bool,
pub name: String,
pub id: DocumentId,
#[serde(rename = "isAutoSaved")]
pub is_auto_saved: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -54,3 +62,10 @@ pub enum ExportBounds {
Selection,
Artboard(LayerNodeIdentifier),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct EyedropperPreviewImage {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
@@ -1,4 +0,0 @@
use crate::messages::portfolio::utility_types::Platform;
use std::sync::OnceLock;
pub static GLOBAL_PLATFORM: OnceLock<Platform> = OnceLock::new();
@@ -1,8 +0,0 @@
use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*;
#[impl_message(Message, Globals)]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum GlobalsMessage {
SetPlatform { platform: Platform },
}
@@ -1,20 +0,0 @@
use crate::messages::prelude::*;
#[derive(Debug, Default, ExtractField)]
pub struct GlobalsMessageHandler {}
#[message_handler_data]
impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
fn process_message(&mut self, message: GlobalsMessage, _responses: &mut VecDeque<Message>, _: ()) {
match message {
GlobalsMessage::SetPlatform { platform } => {
if GLOBAL_PLATFORM.get() != Some(&platform) {
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
}
}
}
}
advertise_actions!(GlobalsMessageDiscriminant;
);
}
-9
View File
@@ -1,9 +0,0 @@
mod globals_message;
mod globals_message_handler;
pub mod global_variables;
#[doc(inline)]
pub use globals_message::{GlobalsMessage, GlobalsMessageDiscriminant};
#[doc(inline)]
pub use globals_message_handler::GlobalsMessageHandler;
@@ -1,10 +1,9 @@
use super::utility_types::input_keyboard::KeysGroup;
use super::utility_types::misc::Mapping;
use crate::application::Editor;
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use std::fmt::Write;
#[derive(ExtractField)]
pub struct InputMapperMessageContext<'a> {
@@ -34,27 +33,6 @@ impl InputMapperMessageHandler {
self.mapping = mapping;
}
pub fn hints(&self, actions: ActionList) -> String {
let mut output = String::new();
let mut actions = actions
.into_iter()
.flatten()
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::ActivateTool) | MessageDiscriminant::Debug(_)));
self.mapping
.key_down
.iter()
.enumerate()
.filter_map(|(i, m)| {
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
ma.map(|a| ((i as u8).try_into().unwrap(), a))
})
.for_each(|(k, a): (Key, _)| {
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').next_back().unwrap());
});
output.replace("Key", "")
}
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
let all_key_mapping_entries = std::iter::empty()
.chain(self.mapping.key_up.iter())
@@ -70,11 +48,7 @@ impl InputMapperMessageHandler {
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
// Get the `Key` for this platform's accelerator key
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let platform_accel_key = match keyboard_layout() {
KeyboardPlatformLayout::Standard => Key::Control,
KeyboardPlatformLayout::Mac => Key::Command,
};
let platform_accel_key = if Editor::environment().is_mac() { Key::Command } else { Key::Control };
let entry_to_key = |entry: &MappingEntry| {
// Get the modifier keys for the entry (and convert them to Key)
@@ -1,3 +1,4 @@
use crate::application::Editor;
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
@@ -16,16 +17,18 @@ use glam::DVec2;
impl From<MappingVariant> for Mapping {
fn from(value: MappingVariant) -> Self {
match value {
MappingVariant::Default => input_mappings(),
MappingVariant::ZoomWithScroll => zoom_with_scroll(),
MappingVariant::Default => input_mappings(false),
MappingVariant::ZoomWithScroll => input_mappings(true),
}
}
}
pub fn input_mappings() -> Mapping {
pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
use InputMapperMessage::*;
use Key::*;
let is_mac = Editor::environment().is_mac();
// NOTICE:
// If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise
// it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`).
@@ -53,6 +56,16 @@ 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),
//
// AppWindowMessage
entry!(KeyDown(F11); disabled=is_mac, action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=!is_mac, action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(KeyQ); modifiers=[Command], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close),
//
// ClipboardMessage
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy),
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=ClipboardMessage::Paste),
//
// 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 }),
@@ -235,9 +248,9 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAll),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllSelected),
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllSelected),
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
@@ -333,11 +346,12 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: true }),
entry!(KeyDown(KeyC); modifiers=[Alt, Shift], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: false }),
entry!(KeyDownNoRepeat(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath),
// TODO: Change to KeyDownNoRepeat when https://github.com/GraphiteEditor/Graphite/issues/2266 is resolved
entry!(KeyDown(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath),
//
// DocumentMessage
entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle),
entry!(KeyUp(Escape); action_dispatch=DocumentMessage::Escape),
entry!(KeyDownNoRepeat(Escape); action_dispatch=DocumentMessage::Escape),
entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyO); modifiers=[Alt], action_dispatch=DocumentMessage::ToggleOverlaysVisibility),
@@ -418,12 +432,18 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(MouseMiddle); action_dispatch=NavigationMessage::BeginCanvasPan),
entry!(KeyDown(MouseLeft); modifiers=[Space], action_dispatch=NavigationMessage::BeginCanvasPan),
entry!(KeyDown(NumpadAdd); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }),
// `FakeKeyPlus` is a nonfunctional key mapping that must be accompanied by its real `Equal` key counterpart. This is used only to set the canonical key label so it shows "+" instead of "=" in the UI.
entry!(KeyDown(FakeKeyPlus); modifiers=[Accel], canonical, action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }),
entry!(KeyDown(Equal); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }),
entry!(KeyDown(Minus); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }),
entry!(KeyDown(KeyF); modifiers=[Alt], action_dispatch=NavigationMessage::CanvasFlip),
entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),
entry!(WheelScroll; modifiers=[Control], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(WheelScroll; modifiers=[Command], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(WheelScroll; modifiers=[Shift], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
entry!(WheelScroll; disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),
// On Mac, the OS already converts Shift+scroll into horizontal scrolling so we have to reverse the behavior from normal to produce the same outcome
entry!(WheelScroll; modifiers=[Control], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: is_mac }),
entry!(WheelScroll; modifiers=[Shift], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: !is_mac }),
entry!(WheelScroll; disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(KeyDown(PageUp); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(1., 0.) }),
entry!(KeyDown(PageDown); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(-1., 0.) }),
entry!(KeyDown(PageUp); action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(0., 1.) }),
@@ -435,15 +455,13 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::Open),
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
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),
entry!(KeyDown(Enter); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleFocusDocument),
//
// DialogMessage
entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog),
@@ -489,39 +507,3 @@ pub fn input_mappings() -> Mapping {
pointer_shake,
}
}
/// Default mappings except that scrolling without modifier keys held down is bound to zooming instead of vertical panning
pub fn zoom_with_scroll() -> Mapping {
use InputMapperMessage::*;
let mut mapping = input_mappings();
let remove = [
entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),
];
let add = [
entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),
entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
];
apply_mapping_patch(&mut mapping, remove, add);
mapping
}
fn apply_mapping_patch<'a, const N: usize, const M: usize, const X: usize, const Y: usize>(
mapping: &mut Mapping,
remove: impl IntoIterator<Item = &'a [&'a [MappingEntry; N]; M]>,
add: impl IntoIterator<Item = &'a [&'a [MappingEntry; X]; Y]>,
) {
for entry in remove.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) {
mapping.remove(entry);
}
for entry in add.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) {
mapping.add(entry.clone());
}
}
@@ -1,13 +1,21 @@
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::application::Editor;
use crate::messages::prelude::*;
use bitflags::bitflags;
use std::fmt::{self, Display, Formatter};
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign};
// ===========
// StorageType
// ===========
// TODO: Increase size of type
/// Edit this to specify the storage type used.
pub type StorageType = u128;
// =========
// KeyStates
// =========
// Base-2 logarithm of the storage type used to represents how many bits you need to fully address every bit in that storage type
const STORAGE_SIZE: u32 = (std::mem::size_of::<StorageType>() * 8).trailing_zeros();
const STORAGE_SIZE_BITS: usize = 1 << STORAGE_SIZE;
@@ -23,11 +31,19 @@ pub fn all_required_modifiers_pressed(keyboard_state: &KeyStates, modifiers: &Ke
all_modifiers_without_pressed_modifiers.is_empty()
}
// ===========
// KeyPosition
// ===========
pub enum KeyPosition {
Pressed,
Released,
}
// ============
// ModifierKeys
// ============
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[repr(transparent)]
@@ -40,6 +56,10 @@ bitflags! {
}
}
// ===
// Key
// ===
// Currently this is mostly based on the JS `KeyboardEvent.code` list: <https://www.w3.org/TR/uievents-code/>
// But in the future, especially once users can customize keyboard mappings, we should deviate more from this so we have actual symbols
// like `+` (which doesn't exist because it's the shifted version of `=` on the US keyboard, after which these scan codes are named).
@@ -61,7 +81,7 @@ pub enum Key {
Digit7,
Digit8,
Digit9,
//
KeyA,
KeyB,
KeyC,
@@ -88,7 +108,7 @@ pub enum Key {
KeyX,
KeyY,
KeyZ,
//
Backquote,
Backslash,
BracketLeft,
@@ -197,17 +217,30 @@ pub enum Key {
Unidentified,
// Other keys that aren't part of the W3C spec
//
/// "Cmd" on Mac (not present on other platforms).
Command,
/// "Ctrl" on Windows/Linux, "Cmd" on Mac
/// "Ctrl" on Windows/Linux, "Cmd" on Mac.
Accel,
/// Left mouse button click (LMB).
MouseLeft,
/// Right mouse button click (RMB).
MouseRight,
/// Middle mouse button click (MMB).
MouseMiddle,
/// Mouse backward navigation button (typically on the side of the mouse).
MouseBack,
/// Mouse forward navigation button (typically on the side of the mouse).
MouseForward,
// This has to be the last element in the enum
NumKeys,
// Fake keys for displaying special labels in the UI
//
/// Not a physical key that can be pressed. May be used so that an actual shortcut bound to `Equal` can separately map this fake "key" as an additional binding to display the "+" shortcut label in the UI.
FakeKeyPlus,
/// Not a physical key that can be pressed. May be used so that an actual shortcut bound to all ten number keys (0, ..., 9) can separately map this fake "key" as an additional binding to display the "09" shortcut label in the UI.
FakeKeyNumbers,
_KeysVariantCount, // This has to be the last element in the enum
}
impl fmt::Display for Key {
@@ -217,15 +250,15 @@ impl fmt::Display for Key {
// Writing system keys
const DIGIT_PREFIX: &str = "Digit";
if key_name.len() == DIGIT_PREFIX.len() + 1 && &key_name[0..DIGIT_PREFIX.len()] == "Digit" {
if key_name.len() == DIGIT_PREFIX.len() + 1 && &key_name[0..DIGIT_PREFIX.len()] == DIGIT_PREFIX {
return write!(f, "{}", key_name.chars().skip(DIGIT_PREFIX.len()).collect::<String>());
}
const KEY_PREFIX: &str = "Key";
if key_name.len() == KEY_PREFIX.len() + 1 && &key_name[0..KEY_PREFIX.len()] == "Key" {
if key_name.len() == KEY_PREFIX.len() + 1 && &key_name[0..KEY_PREFIX.len()] == KEY_PREFIX {
return write!(f, "{}", key_name.chars().skip(KEY_PREFIX.len()).collect::<String>());
}
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let is_mac = Editor::environment().is_mac();
let name = match self {
// Writing system keys
@@ -242,21 +275,21 @@ impl fmt::Display for Key {
Self::Slash => "/",
// Functional keys
Self::Alt => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Alt",
KeyboardPlatformLayout::Mac => "",
Self::Alt => match is_mac {
true => "",
false => "Alt",
},
Self::Meta => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "",
KeyboardPlatformLayout::Mac => "",
Self::Meta => match is_mac {
true => "",
false => "",
},
Self::Shift => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Shift",
KeyboardPlatformLayout::Mac => "",
Self::Shift => match is_mac {
true => "",
false => "Shift",
},
Self::Control => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Ctrl",
KeyboardPlatformLayout::Mac => "",
Self::Control => match is_mac {
true => "",
false => "Ctrl",
},
Self::Backspace => "",
@@ -284,16 +317,19 @@ impl fmt::Display for Key {
// Other keys that aren't part of the W3C spec
Self::Command => "",
Self::Accel => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Ctrl",
KeyboardPlatformLayout::Mac => "",
Self::Accel => match is_mac {
true => "",
false => "Ctrl",
},
Self::MouseLeft => "LMB",
Self::MouseRight => "RMB",
Self::MouseMiddle => "MMB",
Self::MouseLeft => "Click",
Self::MouseRight => "R.Click",
Self::MouseMiddle => "M.Click",
Self::MouseBack => "Mouse Back",
Self::MouseForward => "Mouse Fwd",
Self::NumKeys => "09",
// Fake keys for displaying special labels in the UI
Self::FakeKeyPlus => "+",
Self::FakeKeyNumbers => "09",
_ => key_name.as_str(),
};
@@ -302,22 +338,11 @@ impl fmt::Display for Key {
}
}
impl From<Key> for LayoutKey {
fn from(key: Key) -> Self {
Self {
key: format!("{key:?}"),
label: key.to_string(),
}
}
}
pub const NUMBER_OF_KEYS: usize = Key::_KeysVariantCount as usize - 1;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
struct LayoutKey {
key: String,
label: String,
}
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
// =========
// KeysGroup
// =========
/// Only `Key`s that exist on a physical keyboard should be used.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -331,10 +356,9 @@ impl fmt::Display for KeysGroup {
.0
.iter()
.map(|key| {
let keyboard_layout = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let key_is_modifier = matches!(*key, Key::Control | Key::Command | Key::Alt | Key::Shift | Key::Meta | Key::Accel);
if keyboard_layout == KeyboardPlatformLayout::Mac && key_is_modifier {
if Editor::environment().is_mac() && key_is_modifier {
key.to_string()
} else {
key.to_string() + JOINER_MARK
@@ -351,21 +375,25 @@ impl fmt::Display for KeysGroup {
}
}
impl From<KeysGroup> for String {
fn from(keys: KeysGroup) -> Self {
let layout_keys: LayoutKeysGroup = keys.into();
serde_json::to_string(&layout_keys).expect("Failed to serialize KeysGroup")
// ==========
// LabeledKey
// ==========
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct LabeledKey {
key: Key,
label: String,
}
impl LabeledKey {
pub fn key(&self) -> Key {
self.key
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct LayoutKeysGroup(Vec<LayoutKey>);
impl From<KeysGroup> for LayoutKeysGroup {
fn from(keys_group: KeysGroup) -> Self {
Self(keys_group.0.into_iter().map(|key| key.into()).collect())
}
}
// ===========
// MouseMotion
// ===========
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum MouseMotion {
@@ -383,6 +411,45 @@ pub enum MouseMotion {
MmbDrag,
}
// =======================
// LabeledKeyOrMouseMotion
// =======================
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(untagged)]
pub enum LabeledKeyOrMouseMotion {
Key(LabeledKey),
MouseMotion(MouseMotion),
}
impl From<Key> for LabeledKeyOrMouseMotion {
fn from(key: Key) -> Self {
match key {
Key::MouseLeft => Self::MouseMotion(MouseMotion::Lmb),
Key::MouseRight => Self::MouseMotion(MouseMotion::Rmb),
Key::MouseMiddle => Self::MouseMotion(MouseMotion::Mmb),
_ => Self::Key(LabeledKey { key, label: key.to_string() }),
}
}
}
// ===============
// LabeledShortcut
// ===============
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct LabeledShortcut(pub Vec<LabeledKeyOrMouseMotion>);
impl From<KeysGroup> for LabeledShortcut {
fn from(keys_group: KeysGroup) -> Self {
Self(keys_group.0.into_iter().map(|key| key.into()).collect())
}
}
// =========
// BitVector
// =========
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);
@@ -3,41 +3,13 @@ use crate::messages::prelude::*;
use bitflags::bitflags;
use glam::DVec2;
use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
// Origin is top left
pub type DocumentPosition = DVec2;
pub type ViewportPosition = DVec2;
pub type EditorPosition = DVec2;
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ViewportBounds {
pub top_left: DVec2,
pub bottom_right: DVec2,
}
impl ViewportBounds {
pub fn from_slice(slice: &[f64]) -> Self {
Self {
top_left: DVec2::from_slice(&slice[0..2]),
bottom_right: DVec2::from_slice(&slice[2..4]),
}
}
pub fn size(&self) -> DVec2 {
(self.bottom_right - self.top_left).ceil()
}
pub fn center(&self) -> DVec2 {
(self.bottom_right - self.top_left).ceil() / 2.
}
pub fn in_bounds(&self, position: ViewportPosition) -> bool {
position.x >= 0. && position.y >= 0. && position.x <= self.bottom_right.x && position.y <= self.bottom_right.y
}
}
use std::hash::{Hash, Hasher};
#[derive(Debug, Copy, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ScrollDelta {
pub x: f64,
@@ -104,7 +76,8 @@ pub struct EditorMouseState {
impl EditorMouseState {
pub fn from_keys_and_editor_position(keys: u8, editor_position: EditorPosition) -> Self {
let mouse_keys = MouseKeys::from_bits(keys).expect("Invalid decoding of MouseKeys");
// TODO: Some graphic tablets send key codes not mentioned in the spec. In the future we would like to support these as well.
let mouse_keys = MouseKeys::from_bits_truncate(keys);
Self {
editor_position,
@@ -113,9 +86,9 @@ impl EditorMouseState {
}
}
pub fn to_mouse_state(&self, active_viewport_bounds: &ViewportBounds) -> MouseState {
pub fn to_mouse_state(&self, viewport: &ViewportMessageHandler) -> MouseState {
MouseState {
position: self.editor_position - active_viewport_bounds.top_left,
position: (viewport.logical(self.editor_position) - viewport.offset()).into(),
mouse_keys: self.mouse_keys,
scroll_delta: self.scroll_delta,
}
@@ -25,17 +25,44 @@ macro_rules! modifiers {
/// When an action is currently available, and the user enters that input, the action's message is dispatched on the message bus.
macro_rules! entry {
// Pattern with canonical parameter
($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? canonical, action_dispatch=$action_dispatch:expr_2021$(,)?) => {
entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; true)
(
$input:expr_2021;
$(modifiers=[$($modifier:ident),*],)?
$(refresh_keys=[$($refresh:ident),* $(,)?],)?
canonical,
$(disabled=$disabled:expr,)?
action_dispatch=$action_dispatch:expr_2021$(,)?
) => {
entry!(
$input;
$($($modifier),*)?;
$($($refresh),*)?;
$action_dispatch;
true;
false $( || $disabled )?
)
};
// Pattern without canonical parameter
($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? action_dispatch=$action_dispatch:expr_2021$(,)?) => {
entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; false)
(
$input:expr_2021;
$(modifiers=[$($modifier:ident),*],)?
$(refresh_keys=[$($refresh:ident),* $(,)?],)?
$(disabled=$disabled:expr,)?
action_dispatch=$action_dispatch:expr_2021$(,)?
) => {
entry!(
$input;
$($($modifier),*)?;
$($($refresh),*)?;
$action_dispatch;
false;
false $( || $disabled )?
)
};
// Implementation macro to avoid code duplication
($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr) => {
($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr; $disabled:expr) => {
&[&[
// Cause the `action_dispatch` message to be sent when the specified input occurs.
MappingEntry {
@@ -43,33 +70,37 @@ macro_rules! entry {
input: $input,
modifiers: modifiers!($($modifier),*),
canonical: $canonical,
disabled: $disabled,
},
// Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change.
$(
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyDown(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
disabled: $disabled,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyUp(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
disabled: $disabled,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
disabled: $disabled,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh),
modifiers: modifiers!(),
canonical: $canonical,
disabled: $disabled,
},
)*
]]
@@ -97,6 +128,10 @@ macro_rules! mapping {
for entry_slice in $entry {
// Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries)
for entry in entry_slice.into_iter() {
if entry.disabled {
continue;
}
let corresponding_list = match entry.input {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
@@ -117,14 +152,23 @@ macro_rules! mapping {
}};
}
/// Constructs an `ActionKeys` macro with a certain `Action` variant, conveniently wrapped in `Some()`.
macro_rules! action_keys {
/// Constructs an `ActionShortcut` macro with a certain `Action` variant, conveniently wrapped in `Some()`.
macro_rules! action_shortcut {
($action:expr_2021) => {
Some(crate::messages::input_mapper::utility_types::misc::ActionKeys::Action($action.into()))
Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Action($action.into()))
};
}
pub(crate) use action_keys;
macro_rules! action_shortcut_manual {
($($keys:expr),*) => {
Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Shortcut(
crate::messages::input_mapper::utility_types::input_keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(),
))
};
}
pub(crate) use action_shortcut;
pub(crate) use action_shortcut_manual;
pub(crate) use entry;
pub(crate) use mapping;
pub(crate) use modifiers;
@@ -1,4 +1,4 @@
use super::input_keyboard::{KeysGroup, LayoutKeysGroup, all_required_modifiers_pressed};
use super::input_keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed};
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::input_mapper::utility_types::input_keyboard::{KeyStates, NUMBER_OF_KEYS};
use crate::messages::input_mapper::utility_types::input_mouse::NUMBER_OF_MOUSE_BUTTONS;
@@ -29,16 +29,6 @@ impl Mapping {
list.match_mapping(keyboard_state, actions)
}
pub fn remove(&mut self, target_entry: &MappingEntry) {
let list = self.associated_entries_mut(&target_entry.input);
list.remove(target_entry);
}
pub fn add(&mut self, new_entry: MappingEntry) {
let list = self.associated_entries_mut(&new_entry.input);
list.push(new_entry);
}
fn associated_entries(&self, message: &InputMapperMessage) -> &KeyMappingEntries {
match message {
InputMapperMessage::KeyDown(key) => &self.key_down[*key as usize],
@@ -51,19 +41,6 @@ impl Mapping {
InputMapperMessage::PointerShake => &self.pointer_shake,
}
}
fn associated_entries_mut(&mut self, message: &InputMapperMessage) -> &mut KeyMappingEntries {
match message {
InputMapperMessage::KeyDown(key) => &mut self.key_down[*key as usize],
InputMapperMessage::KeyUp(key) => &mut self.key_up[*key as usize],
InputMapperMessage::KeyDownNoRepeat(key) => &mut self.key_down_no_repeat[*key as usize],
InputMapperMessage::KeyUpNoRepeat(key) => &mut self.key_up_no_repeat[*key as usize],
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
InputMapperMessage::PointerMove => &mut self.pointer_move,
InputMapperMessage::PointerShake => &mut self.pointer_shake,
}
}
}
#[derive(Debug, Clone)]
@@ -125,31 +102,24 @@ pub struct MappingEntry {
pub modifiers: KeyStates,
/// True indicates that this takes priority as the labeled hotkey shown in UI menus and tooltips instead of an alternate binding for the same action
pub canonical: bool,
/// Whether this mapping is disabled
pub disabled: bool,
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ActionKeys {
pub enum ActionShortcut {
Action(MessageDiscriminant),
#[serde(rename = "keys")]
Keys(LayoutKeysGroup),
#[serde(rename = "shortcut")]
Shortcut(LabeledShortcut),
}
impl ActionKeys {
pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) -> String {
match self {
Self::Action(action) => {
if let Some(keys) = action_input_mapping(action) {
let description = keys.to_string();
*self = Self::Keys(keys.into());
description
} else {
*self = Self::Keys(KeysGroup::default().into());
String::new()
}
}
Self::Keys(keys) => {
warn!("Calling `.to_keys()` on a `ActionKeys::Keys` is a mistake/bug. Keys are: {keys:?}.");
String::new()
impl ActionShortcut {
pub fn realize_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
if let Self::Action(action) = self {
if let Some(keys) = action_input_mapping(action) {
*self = Self::Shortcut(keys.into());
} else {
*self = Self::Shortcut(KeysGroup::default().into());
}
}
}
@@ -1,11 +1,10 @@
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ViewportBounds};
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
use crate::messages::prelude::*;
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum InputPreprocessorMessage {
BoundsOfViewports { bounds_of_viewports: Vec<ViewportBounds> },
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
KeyDown { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
KeyUp { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
@@ -1,14 +1,13 @@
use crate::application::Editor;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState, ViewportBounds};
use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState};
use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use glam::DVec2;
use std::time::Duration;
#[derive(ExtractField)]
pub struct InputPreprocessorMessageContext {
pub keyboard_platform: KeyboardPlatformLayout,
pub struct InputPreprocessorMessageContext<'a> {
pub viewport: &'a ViewportMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
@@ -17,38 +16,18 @@ pub struct InputPreprocessorMessageHandler {
pub time: u64,
pub keyboard: KeyStates,
pub mouse: MouseState,
pub viewport_bounds: ViewportBounds,
}
#[message_handler_data]
impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> for InputPreprocessorMessageHandler {
fn process_message(&mut self, message: InputPreprocessorMessage, responses: &mut VecDeque<Message>, context: InputPreprocessorMessageContext) {
let InputPreprocessorMessageContext { keyboard_platform } = context;
impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext<'a>> for InputPreprocessorMessageHandler {
fn process_message(&mut self, message: InputPreprocessorMessage, responses: &mut VecDeque<Message>, context: InputPreprocessorMessageContext<'a>) {
let InputPreprocessorMessageContext { viewport } = context;
match message {
InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } => {
assert_eq!(bounds_of_viewports.len(), 1, "Only one viewport is currently supported");
for bounds in bounds_of_viewports {
// TODO: Extend this to multiple viewports instead of setting it to the value of this last loop iteration
self.viewport_bounds = bounds;
responses.add(NavigationMessage::CanvasPan { delta: DVec2::ZERO });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
responses.add(DeferMessage::AfterGraphRun {
messages: vec![
DeferMessage::AfterGraphRun {
messages: vec![DeferMessage::TriggerNavigationReady.into()],
}
.into(),
],
});
}
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
for key in mouse_state.mouse_keys {
@@ -63,7 +42,7 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
}
}
InputPreprocessorMessage::KeyDown { key, key_repeat, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
self.keyboard.set(key as usize);
if !key_repeat {
responses.add(InputMapperMessage::KeyDownNoRepeat(key));
@@ -71,7 +50,7 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
responses.add(InputMapperMessage::KeyDown(key));
}
InputPreprocessorMessage::KeyUp { key, key_repeat, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
self.keyboard.unset(key as usize);
if !key_repeat {
responses.add(InputMapperMessage::KeyUpNoRepeat(key));
@@ -79,17 +58,17 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
responses.add(InputMapperMessage::KeyUp(key));
}
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
self.translate_mouse_event(mouse_state, true, responses);
}
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
responses.add(InputMapperMessage::PointerMove);
@@ -98,17 +77,17 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
responses.add(InputMapperMessage::PointerShake);
@@ -119,9 +98,9 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
self.frame_time.advance_timestamp(Duration::from_millis(timestamp));
}
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
self.mouse.scroll_delta = mouse_state.scroll_delta;
@@ -168,7 +147,7 @@ impl InputPreprocessorMessageHandler {
self.mouse = new_state;
}
fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, keyboard_platform: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, responses: &mut VecDeque<Message>) {
let is_key_pressed = |key_to_check: ModifierKeys| pressed_modifier_keys.contains(key_to_check);
// Update the state of the concrete modifier keys based on the source state
@@ -177,16 +156,16 @@ impl InputPreprocessorMessageHandler {
self.update_modifier_key(Key::Control, is_key_pressed(ModifierKeys::CONTROL), responses);
// Update the state of either the concrete Meta or the Command keys based on which one is applicable for this platform
let meta_or_command = match keyboard_platform {
KeyboardPlatformLayout::Mac => Key::Command,
KeyboardPlatformLayout::Standard => Key::Meta,
let meta_or_command = match Editor::environment().is_mac() {
true => Key::Command,
false => Key::Meta,
};
self.update_modifier_key(meta_or_command, is_key_pressed(ModifierKeys::META_OR_COMMAND), responses);
// Update the state of the virtual Accel key (the primary accelerator key) based on the source state of the Control or Command key, whichever is relevant on this platform
let accel_virtual_key_state = match keyboard_platform {
KeyboardPlatformLayout::Mac => is_key_pressed(ModifierKeys::META_OR_COMMAND),
KeyboardPlatformLayout::Standard => is_key_pressed(ModifierKeys::CONTROL),
let accel_virtual_key_state = match Editor::environment().is_mac() {
true => is_key_pressed(ModifierKeys::META_OR_COMMAND),
false => is_key_pressed(ModifierKeys::CONTROL),
};
self.update_modifier_key(Key::Accel, accel_virtual_key_state, responses);
}
@@ -202,18 +181,12 @@ impl InputPreprocessorMessageHandler {
responses.add(InputMapperMessage::KeyDown(key));
}
}
pub fn document_bounds(&self) -> [DVec2; 2] {
// IPP bounds are relative to the entire application
[(0., 0.).into(), self.viewport_bounds.bottom_right - self.viewport_bounds.top_left]
}
}
#[cfg(test)]
mod test {
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
#[test]
@@ -231,7 +204,7 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@@ -250,7 +223,7 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@@ -269,7 +242,7 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@@ -290,7 +263,7 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@@ -310,7 +283,7 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@@ -12,6 +12,9 @@ pub enum LayoutMessage {
layout: Layout,
layout_target: LayoutTarget,
},
DestroyLayout {
layout_target: LayoutTarget,
},
WidgetValueCommit {
layout_target: LayoutTarget,
widget_id: WidgetId,
@@ -2,9 +2,9 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::vector::style::{FillChoice, GradientStops};
use serde_json::Value;
use std::collections::HashMap;
#[derive(ExtractField)]
pub struct LayoutMessageContext<'a> {
@@ -13,7 +13,7 @@ pub struct LayoutMessageContext<'a> {
#[derive(Debug, Clone, Default, ExtractField)]
pub struct LayoutMessageHandler {
layouts: [Layout; LayoutTarget::LayoutTargetLength as usize],
layouts: [Layout; LayoutTarget::_LayoutTargetLength as usize],
}
#[message_handler_data]
@@ -24,13 +24,10 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
match message {
LayoutMessage::ResendActiveWidget { layout_target, widget_id } => {
// Find the updated diff based on the specified layout target
let Some(diff) = (match &self.layouts[layout_target as usize] {
Layout::MenuLayout(_) => return,
Layout::WidgetLayout(layout) => Self::get_widget_path(layout, widget_id).map(|(widget, widget_path)| {
// Create a widget update diff for the relevant id
let new_value = DiffUpdate::Widget(widget.clone());
WidgetDiff { widget_path, new_value }
}),
let Some(diff) = Self::get_widget_path(&self.layouts[layout_target as usize], widget_id).map(|(widget, widget_path)| {
// Create a widget update diff for the relevant id
let new_value = DiffUpdate::Widget(widget.clone());
WidgetDiff { widget_path, new_value }
}) else {
return;
};
@@ -40,12 +37,16 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
LayoutMessage::SendLayout { layout, layout_target } => {
self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping);
}
LayoutMessage::DestroyLayout { layout_target } => {
if let Some(layout) = self.layouts.get_mut(layout_target as usize) {
*layout = Layout::default();
}
}
LayoutMessage::WidgetValueCommit { layout_target, widget_id, value } => {
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Commit, responses);
}
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
responses.add(LayoutMessage::ResendActiveWidget { layout_target, widget_id });
}
}
}
@@ -57,8 +58,8 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
impl LayoutMessageHandler {
/// Get the widget path for the widget with the specified id
fn get_widget_path(widget_layout: &WidgetLayout, widget_id: WidgetId) -> Option<(&WidgetHolder, Vec<usize>)> {
let mut stack = widget_layout.layout.iter().enumerate().map(|(index, val)| (vec![index], val)).collect::<Vec<_>>();
fn get_widget_path(widget_layout: &Layout, widget_id: WidgetId) -> Option<(&WidgetInstance, Vec<usize>)> {
let mut stack = widget_layout.0.iter().enumerate().map(|(index, val)| (vec![index], val)).collect::<Vec<_>>();
while let Some((mut widget_path, layout_group)) = stack.pop() {
match layout_group {
// Check if any of the widgets in the current column or row have the correct id
@@ -70,30 +71,37 @@ impl LayoutMessageHandler {
return Some((widget, widget_path));
}
if let Widget::PopoverButton(popover) = &widget.widget {
stack.extend(popover.popover_layout.iter().enumerate().map(|(child, val)| ([widget_path.as_slice(), &[index, child]].concat(), val)));
if let Widget::PopoverButton(popover) = &*widget.widget {
stack.extend(
popover
.popover_layout
.0
.iter()
.enumerate()
.map(|(child, val)| ([widget_path.as_slice(), &[index, child]].concat(), val)),
);
}
}
}
// A section contains more LayoutGroups which we add to the stack.
LayoutGroup::Section { layout, .. } => {
stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val)));
stack.extend(layout.0.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val)));
}
LayoutGroup::Table { rows } => {
for (row_index, cell) in rows.iter().enumerate() {
for (cell_index, entry) in cell.iter().enumerate() {
LayoutGroup::Table { rows, .. } => {
for (row_index, row) in rows.iter().enumerate() {
for (cell_index, cell) in row.iter().enumerate() {
// Return if this is the correct ID
if entry.widget_id == widget_id {
if cell.widget_id == widget_id {
widget_path.push(row_index);
widget_path.push(cell_index);
return Some((entry, widget_path));
return Some((cell, widget_path));
}
if let Widget::PopoverButton(popover) = &entry.widget {
if let Widget::PopoverButton(popover) = &*cell.widget {
stack.extend(
popover
.popover_layout
.0
.iter()
.enumerate()
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, cell_index, child]].concat(), val)),
@@ -113,12 +121,12 @@ impl LayoutMessageHandler {
return;
};
let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else {
let Some(widget_instance) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else {
warn!("handle_widget_callback was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
match &mut widget_holder.widget {
match &mut *widget_instance.widget {
Widget::BreadcrumbTrailButtons(breadcrumb_trail_buttons) => {
let callback_message = match action {
WidgetValueAction::Commit => (breadcrumb_trail_buttons.on_commit.callback)(&()),
@@ -157,10 +165,10 @@ impl LayoutMessageHandler {
let blue = color.get("blue").and_then(|x| x.as_f64()).map(|x| x as f32);
let alpha = color.get("alpha").and_then(|x| x.as_f64()).map(|x| x as f32);
if let (Some(red), Some(green), Some(blue), Some(alpha)) = (red, green, blue, alpha) {
if let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) {
return Some(color);
}
if let (Some(red), Some(green), Some(blue), Some(alpha)) = (red, green, blue, alpha)
&& let Some(color) = Color::from_rgbaf32(red, green, blue, alpha)
{
return Some(color);
}
None
};
@@ -254,44 +262,6 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::FontInput(font_input) => {
let callback_message = match action {
WidgetValueAction::Commit => (font_input.on_commit.callback)(&()),
WidgetValueAction::Update => {
let Some(update_value) = value.as_object() else {
error!("FontInput update was not of type: object");
return;
};
let Some(font_family_value) = update_value.get("fontFamily") else {
error!("FontInput update does not have a fontFamily");
return;
};
let Some(font_style_value) = update_value.get("fontStyle") else {
error!("FontInput update does not have a fontStyle");
return;
};
let Some(font_family) = font_family_value.as_str() else {
error!("FontInput update fontFamily was not of type: string");
return;
};
let Some(font_style) = font_style_value.as_str() else {
error!("FontInput update fontStyle was not of type: string");
return;
};
font_input.font_family = font_family.into();
font_input.font_style = font_style.into();
responses.add(PortfolioMessage::LoadFont {
font: Font::new(font_family.into(), font_style.into()),
});
(font_input.on_update.callback)(font_input)
}
};
responses.add(callback_message);
}
Widget::IconButton(icon_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (icon_button.on_commit.callback)(&()),
@@ -309,26 +279,15 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::ImageLabel(_) => {}
Widget::ShortcutLabel(_) => {}
Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = match action {
WidgetValueAction::Commit => (invisible.on_commit.callback)(&()),
WidgetValueAction::Update => (invisible.on_update.callback)(&()),
};
responses.add(callback_message);
}
Widget::NodeCatalog(node_type_input) => match action {
WidgetValueAction::Commit => {
let callback_message = (node_type_input.on_commit.callback)(&());
responses.add(callback_message);
}
WidgetValueAction::Update => {
let Some(value) = value.as_str().map(|s| s.to_string()) else {
error!("NodeCatalog update was not of type String");
return;
};
let callback_message = (node_type_input.on_update.callback)(&value);
let callback_message = (node_type_input.on_update.callback)(&value.into());
responses.add(callback_message);
}
},
@@ -412,7 +371,34 @@ impl LayoutMessageHandler {
Widget::TextButton(text_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (text_button.on_commit.callback)(&()),
WidgetValueAction::Update => (text_button.on_update.callback)(text_button),
WidgetValueAction::Update => {
let Some(value_path) = value.as_array() else {
error!("TextButton update was not of type: array");
return;
};
// Process the text button click, since no menu is involved if we're given an empty array.
if value_path.is_empty() {
(text_button.on_update.callback)(text_button)
}
// Process the text button's menu list entry click, since we have a path to the value of the contained menu entry.
else {
let mut current_submenu = &text_button.menu_list_children;
let mut final_entry: Option<&MenuListEntry> = None;
// Loop through all menu entry value strings in the path until we reach the final entry (which we store).
// Otherwise we exit early if we can't traverse the full path.
for value in value_path.iter().filter_map(|v| v.as_str().map(|s| s.to_string())) {
let Some(next_entry) = current_submenu.iter().flatten().find(|e| e.value == value) else { return };
current_submenu = &next_entry.children;
final_entry = Some(next_entry);
}
// If we've reached here without returning early, we have a final entry in the path and we should now execute its callback.
(final_entry.unwrap().on_commit.callback)(&())
}
}
};
responses.add(callback_message);
@@ -441,40 +427,37 @@ impl LayoutMessageHandler {
fn diff_and_send_layout_to_frontend(
&mut self,
layout_target: LayoutTarget,
new_layout: Layout,
mut new_layout: Layout,
responses: &mut VecDeque<Message>,
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>,
) {
match new_layout {
Layout::WidgetLayout(_) => {
let mut widget_diffs = Vec::new();
self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs);
// Step 1: Collect CheckboxId mappings from new layout
let mut checkbox_map = HashMap::new();
new_layout.collect_checkbox_ids(layout_target, &mut Vec::new(), &mut checkbox_map);
// Skip sending if no diff.
if widget_diffs.is_empty() {
return;
}
// Step 2: Replace all IDs in new layout with deterministic ones
new_layout.replace_widget_ids(layout_target, &mut Vec::new(), &checkbox_map);
self.send_diff(widget_diffs, layout_target, responses, action_input_mapping);
}
// We don't diff the menu bar layout yet.
Layout::MenuLayout(_) => {
// Skip update if the same
if self.layouts[layout_target as usize] == new_layout {
return;
}
// Step 3: Diff with deterministic IDs
let mut widget_diffs = Vec::new();
// Update the backend storage
self.layouts[layout_target as usize] = new_layout;
self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs);
// Update the UI
let Some(layout) = self.layouts[layout_target as usize].clone().as_menu_layout(action_input_mapping).map(|x| x.layout) else {
error!("Called unwrap_menu_layout on a widget layout");
return;
};
responses.add(FrontendMessage::UpdateMenuBarLayout { layout_target, layout });
}
// Skip sending if no diff
if widget_diffs.is_empty() {
return;
}
// On Mac we need the full MenuBar layout to construct the native menu
#[cfg(target_os = "macos")]
if layout_target == LayoutTarget::MenuBar {
widget_diffs = vec![WidgetDiff {
widget_path: Vec::new(),
new_value: DiffUpdate::Layout(self.layouts[LayoutTarget::MenuBar as usize].clone()),
}];
}
self.send_diff(widget_diffs, layout_target, responses, action_input_mapping);
}
/// Send a diff to the frontend based on the layout target.
@@ -482,24 +465,28 @@ 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::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { diff },
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { diff },
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { diff },
LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { diff },
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { diff },
LayoutTarget::StatusBarHints => FrontendMessage::UpdateStatusBarHintsLayout { diff },
LayoutTarget::StatusBarInfo => FrontendMessage::UpdateStatusBarInfoLayout { diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { diff },
LayoutTarget::WelcomeScreenButtons => FrontendMessage::UpdateWelcomeScreenButtonsLayout { diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { diff },
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
// KEEP THIS ENUM LAST
LayoutTarget::_LayoutTargetLength => panic!("`_LayoutTargetLength` is not a valid `LayoutTarget` and is used for array indexing"),
};
responses.add(message);
}
}
@@ -1,11 +1,12 @@
use super::widgets::button_widgets::*;
use super::widgets::input_widgets::*;
use super::widgets::label_widgets::*;
use super::widgets::menu_widgets::MenuLayout;
use crate::application::generate_uuid;
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::prelude::*;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[repr(transparent)]
@@ -21,6 +22,8 @@ impl core::fmt::Display for WidgetId {
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize, specta::Type)]
#[repr(u8)]
pub enum LayoutTarget {
/// The spreadsheet panel allows for the visualisation of data in the graph.
DataPanel,
/// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message.
DialogButtons,
/// Contains the contents of the dialog's primary column. Must be shown with the `FrontendMessage::DisplayDialog` message.
@@ -29,32 +32,34 @@ pub enum LayoutTarget {
DialogColumn2,
/// Contains the widgets located directly above the canvas to the right, for example the zoom in and out buttons.
DocumentBar,
/// Contains the dropdown for design / select / guide mode found on the top left of the canvas.
DocumentMode,
/// Controls for adding, grouping, and deleting layers at the bottom of the Layers panel.
LayersPanelBottomBar,
/// Blending options at the top of the Layers panel.
LayersPanelControlLeftBar,
/// Selected layer status (locked/hidden) at the top of the Layers panel.
LayersPanelControlRightBar,
/// Controls for adding, grouping, and deleting layers at the bottom of the Layers panel.
LayersPanelBottomBar,
/// The dropdown menu at the very top of the application: File, Edit, etc.
MenuBar,
/// 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.
PropertiesPanel,
/// The spredsheet panel allows for the visualisation of data in the graph.
DataPanel,
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
/// The contextual input key/mouse combination shortcuts shown in the status bar at the bottom of the window.
StatusBarHints,
/// The version information shown in the status bar at the bottom right of the window.
StatusBarInfo,
/// The left side of the control bar directly above the canvas.
ToolOptions,
/// The vertical buttons for all of the tools on the left of the canvas.
ToolShelf,
/// The quick access buttons found on the welcome screen, shown when no documents are open.
WelcomeScreenButtons,
/// The color swatch for the working colors and a flip and reset button found at the bottom of the tool shelf.
WorkingColors,
// KEEP THIS ENUM LAST
// This is a marker that is used to define an array that is used to hold widgets
LayoutTargetLength,
_LayoutTargetLength,
}
/// For use by structs that define a UI widget layout by implementing the layout() function belonging to this trait.
@@ -101,122 +106,123 @@ pub trait DialogLayoutHolder: LayoutHolder {
}
}
/// Wraps a choice of layout type. The chosen layout contains an arrangement of widgets mounted somewhere specific in the frontend.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum Layout {
WidgetLayout(WidgetLayout),
MenuLayout(MenuLayout),
/// Trait for types that can compute incremental diffs for UI updates.
///
/// This trait unifies the diffing behavior across Layout, LayoutGroup, and WidgetInstance,
/// allowing each type to specify how it should be represented in a DiffUpdate.
pub trait Diffable: Clone + PartialEq {
/// Converts this value into a DiffUpdate variant.
fn into_diff_update(self) -> DiffUpdate;
/// Computes the diff between self (old) and new, updating self and recording changes.
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>);
/// Collects all CheckboxIds currently in use in this layout, computing stable replacements.
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>);
/// Replaces all widget IDs with deterministic IDs based on position and type.
/// Also replaces CheckboxIds using the provided mapping.
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>);
}
/// Computes a deterministic WidgetId based on layout target, path, and widget type.
fn compute_widget_id(layout_target: LayoutTarget, widget_path: &[usize], widget: &Widget) -> WidgetId {
let mut hasher = DefaultHasher::new();
(layout_target as u8).hash(&mut hasher);
widget_path.hash(&mut hasher);
std::mem::discriminant(widget).hash(&mut hasher);
WidgetId(hasher.finish())
}
/// Computes a deterministic CheckboxId based on the same WidgetId algorithm.
fn compute_checkbox_id(layout_target: LayoutTarget, widget_path: &[usize], widget: &Widget) -> CheckboxId {
let mut hasher = DefaultHasher::new();
(layout_target as u8).hash(&mut hasher);
widget_path.hash(&mut hasher);
std::mem::discriminant(widget).hash(&mut hasher);
// Add extra salt for checkbox to differentiate from widget ID
"checkbox".hash(&mut hasher);
CheckboxId(hasher.finish())
}
/// Contains an arrangement of widgets mounted somewhere specific in the frontend.
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)]
pub struct Layout(pub Vec<LayoutGroup>);
impl Layout {
pub fn as_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) -> Option<MenuLayout> {
if let Self::MenuLayout(mut menu) = self {
menu.layout
.iter_mut()
.for_each(|menu_column| menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping));
Some(menu)
} else {
None
}
}
pub fn iter(&self) -> Box<dyn Iterator<Item = &WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter()),
}
}
pub fn iter_mut(&mut self) -> Box<dyn Iterator<Item = &mut WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter_mut()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()),
}
}
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
pub fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
match (self, new) {
// Simply diff the internal layout
(Self::WidgetLayout(current), Self::WidgetLayout(new)) => current.diff(new, widget_path, widget_diffs),
(current, Self::WidgetLayout(widget_layout)) => {
// Update current to the new value
*current = Self::WidgetLayout(widget_layout.clone());
// Push an update sublayout value
let new_value = DiffUpdate::SubLayout(widget_layout.layout);
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
}
(_, Self::MenuLayout(_)) => panic!("Cannot diff menu layout"),
}
}
}
impl Default for Layout {
fn default() -> Self {
Self::WidgetLayout(WidgetLayout::default())
}
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)]
pub struct WidgetLayout {
pub layout: SubLayout,
}
impl WidgetLayout {
pub fn new(layout: SubLayout) -> Self {
Self { layout }
}
pub fn iter(&self) -> WidgetIter<'_> {
WidgetIter {
stack: self.layout.iter().collect(),
stack: self.0.iter().collect(),
..Default::default()
}
}
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: self.layout.iter_mut().collect(),
stack: self.0.iter_mut().collect(),
..Default::default()
}
}
}
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
pub fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
impl Diffable for Layout {
fn into_diff_update(self) -> DiffUpdate {
DiffUpdate::Layout(self)
}
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
// Check if the length of items is different
// TODO: Diff insersion and deletion of items
if self.layout.len() != new.layout.len() {
if self.0.len() != new.0.len() {
// Update the layout to the new layout
self.layout.clone_from(&new.layout);
self.0.clone_from(&new.0);
// Push an update sublayout to the diff
let new = DiffUpdate::SubLayout(new.layout);
widget_diffs.push(WidgetDiff {
widget_path: widget_path.to_vec(),
new_value: new,
new_value: new.into_diff_update(),
});
return;
}
// Diff all of the children
for (index, (current_child, new_child)) in self.layout.iter_mut().zip(new.layout).enumerate() {
for (index, (current_child, new_child)) in self.0.iter_mut().zip(new.0).enumerate() {
widget_path.push(index);
current_child.diff(new_child, widget_path, widget_diffs);
widget_path.pop();
}
}
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
for (index, child) in self.0.iter().enumerate() {
widget_path.push(index);
child.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
for (index, child) in self.0.iter_mut().enumerate() {
widget_path.push(index);
child.replace_widget_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
}
#[derive(Debug, Default)]
pub struct WidgetIter<'a> {
pub stack: Vec<&'a LayoutGroup>,
pub table: Vec<&'a WidgetHolder>,
pub current_slice: Option<&'a [WidgetHolder]>,
pub table: Vec<&'a WidgetInstance>,
pub current_slice: Option<&'a [WidgetInstance]>,
}
impl<'a> Iterator for WidgetIter<'a> {
type Item = &'a WidgetHolder;
type Item = &'a WidgetInstance;
fn next(&mut self) -> Option<Self::Item> {
let widget = self.table.pop().or_else(|| {
@@ -225,13 +231,13 @@ impl<'a> Iterator for WidgetIter<'a> {
Some(first)
});
if let Some(item) = widget {
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = item {
self.stack.extend(p.popover_layout.iter());
if let Some(instance) = widget {
if let Widget::PopoverButton(popover_button) = &*instance.widget {
self.stack.extend(popover_button.popover_layout.0.iter());
return self.next();
}
return Some(item);
return Some(instance);
}
match self.stack.pop() {
@@ -243,12 +249,12 @@ impl<'a> Iterator for WidgetIter<'a> {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Table { rows }) => {
Some(LayoutGroup::Table { rows, .. }) => {
self.table.extend(rows.iter().flatten().rev());
self.next()
}
Some(LayoutGroup::Section { layout, .. }) => {
for layout_row in layout {
for layout_row in &layout.0 {
self.stack.push(layout_row);
}
self.next()
@@ -261,12 +267,12 @@ impl<'a> Iterator for WidgetIter<'a> {
#[derive(Debug, Default)]
pub struct WidgetIterMut<'a> {
pub stack: Vec<&'a mut LayoutGroup>,
pub table: Vec<&'a mut WidgetHolder>,
pub current_slice: Option<&'a mut [WidgetHolder]>,
pub table: Vec<&'a mut WidgetInstance>,
pub current_slice: Option<&'a mut [WidgetInstance]>,
}
impl<'a> Iterator for WidgetIterMut<'a> {
type Item = &'a mut WidgetHolder;
type Item = &'a mut WidgetInstance;
fn next(&mut self) -> Option<Self::Item> {
let widget = self.table.pop().or_else(|| {
@@ -275,13 +281,15 @@ impl<'a> Iterator for WidgetIterMut<'a> {
Some(first)
});
if let Some(widget) = widget {
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = widget {
self.stack.extend(p.popover_layout.iter_mut());
return self.next();
if let Some(instance) = widget {
// We have to check that we're not a popover and return first, then extract the popover with an unreachable else condition second, to satisfy the borrow checker.
// After Rust's Polonius is stable, we can reverse that order of steps to avoid the redundancy and unreachable statement.
if !matches!(*instance.widget, Widget::PopoverButton(_)) {
return Some(instance);
}
return Some(widget);
let Widget::PopoverButton(popover_button) = &mut *instance.widget else { unreachable!() };
self.stack.extend(popover_button.popover_layout.0.iter_mut());
return self.next();
}
match self.stack.pop() {
@@ -293,12 +301,12 @@ impl<'a> Iterator for WidgetIterMut<'a> {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Table { rows }) => {
Some(LayoutGroup::Table { rows, .. }) => {
self.table.extend(rows.iter_mut().flatten().rev());
self.next()
}
Some(LayoutGroup::Section { layout, .. }) => {
for layout_row in layout {
for layout_row in &mut layout.0 {
self.stack.push(layout_row);
}
self.next()
@@ -308,26 +316,24 @@ impl<'a> Iterator for WidgetIterMut<'a> {
}
}
pub type SubLayout = Vec<LayoutGroup>;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum LayoutGroup {
#[serde(rename = "column")]
Column {
#[serde(rename = "columnWidgets")]
widgets: Vec<WidgetHolder>,
widgets: Vec<WidgetInstance>,
},
#[serde(rename = "row")]
Row {
#[serde(rename = "rowWidgets")]
widgets: Vec<WidgetHolder>,
widgets: Vec<WidgetInstance>,
},
#[serde(rename = "table")]
Table {
#[serde(rename = "tableWidgets")]
rows: Vec<Vec<WidgetHolder>>,
rows: Vec<Vec<WidgetInstance>>,
unstyled: bool,
},
// TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout`
#[serde(rename = "section")]
Section {
name: String,
@@ -335,7 +341,7 @@ pub enum LayoutGroup {
visible: bool,
pinned: bool,
id: u64,
layout: SubLayout,
layout: Layout,
},
}
@@ -344,51 +350,67 @@ impl Default for LayoutGroup {
Self::Row { widgets: Vec::new() }
}
}
impl From<Vec<WidgetHolder>> for LayoutGroup {
fn from(widgets: Vec<WidgetHolder>) -> LayoutGroup {
impl From<Vec<WidgetInstance>> for LayoutGroup {
fn from(widgets: Vec<WidgetInstance>) -> LayoutGroup {
LayoutGroup::Row { widgets }
}
}
impl LayoutGroup {
/// Applies a tooltip to all widgets in this row or column without a tooltip.
pub fn with_tooltip(self, tooltip: impl Into<String>) -> Self {
/// Applies a tooltip description to all widgets without a tooltip in this row or column.
pub fn with_tooltip_description(self, description: impl Into<String>) -> Self {
let (is_col, mut widgets) = match self {
LayoutGroup::Column { widgets } => (true, widgets),
LayoutGroup::Row { widgets } => (false, widgets),
_ => unimplemented!(),
};
let tooltip = tooltip.into();
let description = description.into();
for widget in &mut widgets {
let val = match &mut widget.widget {
Widget::CheckboxInput(x) => &mut x.tooltip,
Widget::ColorInput(x) => &mut x.tooltip,
Widget::CurveInput(x) => &mut x.tooltip,
Widget::DropdownInput(x) => &mut x.tooltip,
Widget::FontInput(x) => &mut x.tooltip,
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,
Widget::TextAreaInput(x) => &mut x.tooltip,
Widget::TextButton(x) => &mut x.tooltip,
Widget::TextInput(x) => &mut x.tooltip,
Widget::TextLabel(x) => &mut x.tooltip,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip,
Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
let val = match &mut *widget.widget {
Widget::CheckboxInput(x) => &mut x.tooltip_description,
Widget::ColorInput(x) => &mut x.tooltip_description,
Widget::CurveInput(x) => &mut x.tooltip_description,
Widget::DropdownInput(x) => &mut x.tooltip_description,
Widget::IconButton(x) => &mut x.tooltip_description,
Widget::IconLabel(x) => &mut x.tooltip_description,
Widget::ImageButton(x) => &mut x.tooltip_description,
Widget::ImageLabel(x) => &mut x.tooltip_description,
Widget::NumberInput(x) => &mut x.tooltip_description,
Widget::PopoverButton(x) => &mut x.tooltip_description,
Widget::TextAreaInput(x) => &mut x.tooltip_description,
Widget::TextButton(x) => &mut x.tooltip_description,
Widget::TextInput(x) => &mut x.tooltip_description,
Widget::TextLabel(x) => &mut x.tooltip_description,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description,
Widget::ReferencePointInput(_)
| Widget::RadioInput(_)
| Widget::Separator(_)
| Widget::ShortcutLabel(_)
| Widget::WorkingColorsInput(_)
| Widget::NodeCatalog(_)
| Widget::ParameterExposeButton(_) => continue,
};
if val.is_empty() {
val.clone_from(&tooltip);
val.clone_from(&description);
}
}
if is_col { Self::Column { widgets } } else { Self::Row { widgets } }
}
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
pub fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: vec![self],
..Default::default()
}
}
}
impl Diffable for LayoutGroup {
fn into_diff_update(self) -> DiffUpdate {
DiffUpdate::LayoutGroup(self)
}
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
let is_column = matches!(new, Self::Column { .. });
match (self, new) {
(Self::Column { widgets: current_widgets }, Self::Column { widgets: new_widgets }) | (Self::Row { widgets: current_widgets }, Self::Row { widgets: new_widgets }) => {
@@ -399,7 +421,7 @@ impl LayoutGroup {
current_widgets.clone_from(&new_widgets);
// Push back a LayoutGroup update to the diff
let new_value = DiffUpdate::LayoutGroup(if is_column { Self::Column { widgets: new_widgets } } else { Self::Row { widgets: new_widgets } });
let new_value = (if is_column { Self::Column { widgets: new_widgets } } else { Self::Row { widgets: new_widgets } }).into_diff_update();
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
return;
@@ -431,7 +453,7 @@ impl LayoutGroup {
) => {
// Resend the entire panel if the lengths, names, visibility, or node IDs are different
// TODO: Diff insersion and deletion of items
if current_layout.len() != new_layout.len()
if current_layout.0.len() != new_layout.0.len()
|| *current_name != new_name
|| *current_description != new_description
|| *current_visible != new_visible
@@ -447,20 +469,21 @@ impl LayoutGroup {
current_layout.clone_from(&new_layout);
// Push an update layout group to the diff
let new_value = DiffUpdate::LayoutGroup(Self::Section {
let new_value = Self::Section {
name: new_name,
description: new_description,
visible: new_visible,
pinned: new_pinned,
id: new_id,
layout: new_layout,
});
}
.into_diff_update();
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
}
// Diff all of the children
else {
for (index, (current_child, new_child)) in current_layout.iter_mut().zip(new_layout).enumerate() {
for (index, (current_child, new_child)) in current_layout.0.iter_mut().zip(new_layout.0).enumerate() {
widget_path.push(index);
current_child.diff(new_child, widget_path, widget_diffs);
widget_path.pop();
@@ -469,51 +492,184 @@ impl LayoutGroup {
}
(current, new) => {
*current = new.clone();
let new_value = DiffUpdate::LayoutGroup(new);
let new_value = new.into_diff_update();
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
}
}
}
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: vec![self],
..Default::default()
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
match self {
Self::Column { widgets } | Self::Row { widgets } => {
for (index, widget) in widgets.iter().enumerate() {
widget_path.push(index);
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
Self::Table { rows, .. } => {
for (row_idx, row) in rows.iter().enumerate() {
for (col_idx, widget) in row.iter().enumerate() {
widget_path.push(row_idx);
widget_path.push(col_idx);
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
widget_path.pop();
}
}
}
Self::Section { layout, .. } => {
layout.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
}
}
}
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
match self {
Self::Column { widgets } | Self::Row { widgets } => {
for (index, widget) in widgets.iter_mut().enumerate() {
widget_path.push(index);
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
Self::Table { rows, .. } => {
for (row_idx, row) in rows.iter_mut().enumerate() {
for (col_idx, widget) in row.iter_mut().enumerate() {
widget_path.push(row_idx);
widget_path.push(col_idx);
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
widget_path.pop();
}
}
}
Self::Section { layout, .. } => {
layout.replace_widget_ids(layout_target, widget_path, checkbox_map);
}
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WidgetHolder {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WidgetInstance {
#[serde(rename = "widgetId")]
pub widget_id: WidgetId,
pub widget: Widget,
pub widget: Box<Widget>,
}
impl WidgetHolder {
#[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")]
impl PartialEq for WidgetInstance {
fn eq(&self, other: &Self) -> bool {
self.widget_id == other.widget_id && self.widget == other.widget
}
}
impl WidgetInstance {
#[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_instance()")]
pub fn new(widget: Widget) -> Self {
Self {
widget_id: WidgetId(generate_uuid()),
widget,
widget: Box::new(widget),
}
}
}
impl Diffable for WidgetInstance {
fn into_diff_update(self) -> DiffUpdate {
DiffUpdate::Widget(self)
}
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
if self == &new {
// Still need to update callbacks since PartialEq skips them
self.widget = new.widget;
return;
}
// Special handling for PopoverButton: recursively diff nested layout if only the layout changed
if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut *self.widget, &*new.widget) {
// Check if only the popover layout changed (all other fields are the same)
if self.widget_id == new.widget_id
&& button1.disabled == button2.disabled
&& button1.style == button2.style
&& button1.menu_direction == button2.menu_direction
&& button1.icon == button2.icon
&& button1.tooltip_label == button2.tooltip_label
&& button1.tooltip_description == button2.tooltip_description
&& button1.tooltip_shortcut == button2.tooltip_shortcut
&& button1.popover_min_width == button2.popover_min_width
{
// Only the popover layout differs, diff it recursively
for (i, (a, b)) in button1.popover_layout.0.iter_mut().zip(button2.popover_layout.0.iter()).enumerate() {
widget_path.push(i);
a.diff(b.clone(), widget_path, widget_diffs);
widget_path.pop();
}
return;
}
}
// Widget or ID changed, send full update
*self = new.clone();
let new_value = new.into_diff_update();
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
}
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
match &*self.widget {
Widget::CheckboxInput(checkbox) => {
// Compute stable ID based on position and insert mapping
let checkbox_id = checkbox.for_label;
let stable_id = compute_checkbox_id(layout_target, widget_path, &self.widget);
checkbox_map.entry(checkbox_id).or_insert(stable_id);
}
Widget::TextLabel(label) => {
// Compute stable ID based on position and insert mapping
let checkbox_id = label.for_checkbox;
let stable_id = compute_checkbox_id(layout_target, widget_path, &self.widget);
checkbox_map.entry(checkbox_id).or_insert(stable_id);
}
Widget::PopoverButton(button) => {
// Recursively collect from nested popover layout
for (index, child) in button.popover_layout.0.iter().enumerate() {
widget_path.push(index);
child.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
_ => {}
}
}
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec<WidgetDiff>) {
// If there have been changes to the actual widget (not just the id)
if self.widget != new.widget {
// We should update to the new widget value as well as a new widget id
*self = new.clone();
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
// 1. Generate deterministic WidgetId
self.widget_id = compute_widget_id(layout_target, widget_path, &self.widget);
// Push a widget update to the diff
let new_value = DiffUpdate::Widget(new);
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
} else {
// Required to update the callback function, which the PartialEq check above skips
self.widget = new.widget;
// 2. Replace CheckboxIds if present
match &mut *self.widget {
Widget::CheckboxInput(checkbox) => {
let old_id = checkbox.for_label;
if let Some(&new_id) = checkbox_map.get(&old_id) {
checkbox.for_label = new_id;
}
}
Widget::TextLabel(label) => {
let old_id = label.for_checkbox;
if let Some(&new_id) = checkbox_map.get(&old_id) {
label.for_checkbox = new_id;
}
}
Widget::PopoverButton(button) => {
// Recursively replace in nested popover layout
for (index, child) in button.popover_layout.0.iter_mut().enumerate() {
widget_path.push(index);
child.replace_widget_ids(layout_target, widget_path, checkbox_map);
widget_path.pop();
}
}
_ => {}
}
}
}
@@ -543,12 +699,11 @@ pub enum Widget {
ColorInput(ColorInput),
CurveInput(CurveInput),
DropdownInput(DropdownInput),
FontInput(FontInput),
IconButton(IconButton),
IconLabel(IconLabel),
ImageButton(ImageButton),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput),
ShortcutLabel(ShortcutLabel),
NodeCatalog(NodeCatalog),
NumberInput(NumberInput),
ParameterExposeButton(ParameterExposeButton),
@@ -577,56 +732,37 @@ pub struct WidgetDiff {
}
/// The new value of the UI, sent as part of a diff.
///
/// An update can represent a single widget or an entire SubLayout, or just a single layout group.
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum DiffUpdate {
#[serde(rename = "subLayout")]
SubLayout(SubLayout),
#[serde(rename = "layout")]
Layout(Layout),
#[serde(rename = "layoutGroup")]
LayoutGroup(LayoutGroup),
#[serde(rename = "widget")]
Widget(WidgetHolder),
Widget(WidgetInstance),
}
impl DiffUpdate {
/// Append the keyboard shortcut to the tooltip where applicable
pub fn apply_keyboard_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
// Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip
let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| {
let shortcut_text = tooltip_shortcut.to_keys(action_input_mapping);
if let ActionKeys::Keys(_keys) = tooltip_shortcut {
if !shortcut_text.is_empty() {
if !tooltip.is_empty() {
tooltip.push(' ');
}
tooltip.push('(');
tooltip.push_str(&shortcut_text);
tooltip.push(')');
}
}
};
// Go through each widget to convert `ActionKeys::Action` to `ActionKeys::Keys` and append the key combination to the widget tooltip
let convert_tooltip = |widget_holder: &mut WidgetHolder| {
// Go through each widget to convert `ActionShortcut::Action` to `ActionShortcut::Shortcut` and append the key combination to the widget tooltip
let convert_tooltip = |widget_instance: &mut WidgetInstance| {
// Handle all the widgets that have tooltips
let mut tooltip_shortcut = match &mut widget_holder.widget {
Widget::BreadcrumbTrailButtons(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ParameterExposeButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
let tooltip_shortcut = match &mut *widget_instance.widget {
Widget::BreadcrumbTrailButtons(widget) => widget.tooltip_shortcut.as_mut(),
Widget::CheckboxInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ColorInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::DropdownInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::IconButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::NumberInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ParameterExposeButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::PopoverButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::TextButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ImageButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ShortcutLabel(widget) => widget.shortcut.as_mut(),
Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_)
| Widget::NodeCatalog(_)
| Widget::ReferencePointInput(_)
| Widget::RadioInput(_)
@@ -636,29 +772,86 @@ impl DiffUpdate {
| Widget::TextLabel(_)
| Widget::WorkingColorsInput(_) => None,
};
if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut {
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
// Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut`
if let Some(tooltip_shortcut) = tooltip_shortcut {
tooltip_shortcut.realize_shortcut(action_input_mapping);
}
// Handle RadioInput separately because its tooltips are children of the widget
if let Widget::RadioInput(radio_input) = &mut widget_holder.widget {
if let Widget::RadioInput(radio_input) = &mut *widget_instance.widget {
for radio_entry_data in &mut radio_input.entries {
if let RadioEntryData {
tooltip,
tooltip_shortcut: Some(tooltip_shortcut),
..
} = radio_entry_data
{
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
// Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut`
if let Some(tooltip_shortcut) = radio_entry_data.tooltip_shortcut.as_mut() {
tooltip_shortcut.realize_shortcut(action_input_mapping);
}
}
}
};
// Recursively fill menu list entries with their realized shortcut keys specific to the current bindings and platform
fn apply_action_shortcut_to_menu_lists(entry_sections: &mut MenuListEntrySections, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
for entries in entry_sections {
for entry in entries {
// Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut`
if let Some(tooltip_shortcut) = &mut entry.tooltip_shortcut {
tooltip_shortcut.realize_shortcut(action_input_mapping);
}
// Recursively call this function on the menu's children
apply_action_shortcut_to_menu_lists(&mut entry.children, action_input_mapping);
}
}
}
// Hash the menu list entry sections for caching purposes
let hash_menu_list_entry_sections = |entry_sections: &MenuListEntrySections| {
struct RecursiveHasher<'a> {
hasher: DefaultHasher,
hash_fn: &'a dyn Fn(&mut RecursiveHasher, &MenuListEntrySections),
}
let mut recursive_hasher = RecursiveHasher {
hasher: DefaultHasher::new(),
hash_fn: &|recursive_hasher, entry_sections| {
for (index, entries) in entry_sections.iter().enumerate() {
index.hash(&mut recursive_hasher.hasher);
for entry in entries {
entry.hash(&mut recursive_hasher.hasher);
(recursive_hasher.hash_fn)(recursive_hasher, &entry.children);
}
}
},
};
(recursive_hasher.hash_fn)(&mut recursive_hasher, entry_sections);
recursive_hasher.hasher.finish()
};
// Apply shortcut conversions to all widgets that have menu lists
let convert_menu_lists = |widget_instance: &mut WidgetInstance| match &mut *widget_instance.widget {
Widget::DropdownInput(dropdown_input) => {
apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries, action_input_mapping);
dropdown_input.entries_hash = hash_menu_list_entry_sections(&dropdown_input.entries);
}
Widget::TextButton(text_button) => {
apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children, action_input_mapping);
text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children);
}
_ => {}
};
match self {
Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(convert_tooltip),
Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(convert_tooltip),
Self::Widget(widget_holder) => convert_tooltip(widget_holder),
Self::Layout(layout) => layout.0.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(|widget_instance| {
convert_tooltip(widget_instance);
convert_menu_lists(widget_instance);
}),
Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(|widget_instance| {
convert_tooltip(widget_instance);
convert_menu_lists(widget_instance);
}),
Self::Widget(widget_instance) => {
convert_tooltip(widget_instance);
convert_menu_lists(widget_instance);
}
}
}
}
@@ -6,5 +6,4 @@ pub mod widget_prelude {
pub use super::widgets::button_widgets::*;
pub use super::widgets::input_widgets::*;
pub use super::widgets::label_widgets::*;
pub use super::widgets::menu_widgets::*;
}
@@ -1,4 +1,4 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
@@ -9,29 +9,30 @@ use graphite_proc_macros::WidgetBuilder;
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct IconButton {
// Content
#[widget_builder(constructor)]
pub icon: String,
#[serde(rename = "hoverIcon")]
pub hover_icon: Option<String>,
#[widget_builder(constructor)]
pub size: u32, // TODO: Convert to an `IconSize` enum
pub disabled: bool,
pub active: bool,
// Styling
pub emphasized: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<IconButton>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -40,25 +41,26 @@ pub struct IconButton {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct PopoverButton {
// Content
pub style: Option<String>,
pub icon: Option<String>,
pub disabled: bool,
// Children
#[serde(rename = "popoverLayout")]
pub popover_layout: Layout,
#[serde(rename = "popoverMinWidth")]
pub popover_min_width: Option<u32>,
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub icon: Option<String>,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
#[serde(rename = "popoverLayout")]
pub popover_layout: SubLayout,
#[serde(rename = "popoverMinWidth")]
pub popover_min_width: Option<u32>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -78,21 +80,23 @@ pub enum MenuDirection {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ParameterExposeButton {
// Content
pub exposed: bool,
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<ParameterExposeButton>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -101,36 +105,42 @@ pub struct ParameterExposeButton {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct TextButton {
// Content
#[widget_builder(constructor)]
pub label: String,
pub icon: Option<String>,
#[serde(rename = "hoverIcon")]
pub hover_icon: Option<String>,
pub disabled: bool,
pub flush: bool,
// Children
#[serde(rename = "menuListChildren")]
pub menu_list_children: MenuListEntrySections,
#[serde(rename = "menuListChildrenHash")]
#[widget_builder(skip)]
pub menu_list_children_hash: u64,
// Styling
pub emphasized: bool,
pub flush: bool,
pub narrow: bool,
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
#[serde(rename = "menuListChildren")]
pub menu_list_children: MenuListEntrySections,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextButton>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -139,23 +149,24 @@ pub struct TextButton {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ImageButton {
// Content
#[widget_builder(constructor)]
pub image: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -164,34 +175,33 @@ pub struct ImageButton {
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct ColorInput {
// Content
/// WARNING: The colors are gamma, not linear!
#[widget_builder(constructor)]
pub value: FillChoice,
// TODO: Implement
// #[serde(rename = "allowTransparency")]
// #[derivative(Default(value = "false"))]
// pub allow_transparency: bool,
//
#[serde(rename = "allowNone")]
#[derivative(Default(value = "true"))]
pub allow_none: bool,
pub disabled: bool,
// #[serde(rename = "allowTransparency")] pub allow_transparency: bool, // TODO: Implement
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub disabled: bool,
pub tooltip: String,
// Styling
pub narrow: bool,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<ColorInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -200,21 +210,23 @@ pub struct ColorInput {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct BreadcrumbTrailButtons {
// Content
#[widget_builder(constructor)]
pub labels: Vec<String>,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<u64>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -1,5 +1,6 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use derivative::*;
use graphene_std::Color;
use graphene_std::raster::curve::Curve;
@@ -7,50 +8,36 @@ use graphene_std::transform::ReferencePoint;
use graphite_proc_macros::WidgetBuilder;
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
#[derivative(Debug, Default, PartialEq)]
pub struct CheckboxInput {
// Content
#[widget_builder(constructor)]
pub checked: bool,
pub disabled: bool,
#[derivative(Default(value = "\"Checkmark\".to_string()"))]
pub icon: String,
pub tooltip: String,
#[serde(rename = "forLabel")]
pub for_label: CheckboxId,
pub disabled: bool,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<CheckboxInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
}
impl Default for CheckboxInput {
fn default() -> Self {
Self {
checked: false,
disabled: false,
icon: "Checkmark".into(),
tooltip: Default::default(),
tooltip_shortcut: Default::default(),
for_label: CheckboxId::new(),
on_update: Default::default(),
on_commit: Default::default(),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CheckboxId(u64);
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CheckboxId(pub u64);
impl CheckboxId {
pub fn new() -> Self {
@@ -72,189 +59,156 @@ impl specta::Type for CheckboxId {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct DropdownInput {
#[widget_builder(constructor)]
pub entries: MenuListEntrySections,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
// Content
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (we can replace this with `usize` if we switch to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
#[serde(rename = "drawIcon")]
pub draw_icon: bool,
pub disabled: bool,
// Children
#[widget_builder(constructor)]
pub entries: MenuListEntrySections,
#[serde(rename = "entriesHash")]
#[widget_builder(skip)]
pub entries_hash: u64,
// Styling
pub narrow: bool,
// Behavior
#[serde(rename = "virtualScrolling")]
pub virtual_scrolling: bool,
#[derivative(Default(value = "true"))]
pub interactive: bool,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Styling
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
//
// Callbacks
// `on_update` exists on the `MenuListEntry`, not this parent `DropdownInput`
// Callbacks exists on the `MenuListEntry` children, not this parent `DropdownInput`
}
pub type MenuListEntrySections = Vec<Vec<MenuListEntry>>;
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
#[widget_builder(not_widget_holder)]
#[widget_builder(not_widget_instance)]
pub struct MenuListEntry {
// Content
#[widget_builder(constructor)]
pub value: String,
pub label: String,
pub icon: String,
pub shortcut: Vec<String>,
#[serde(rename = "shortcutRequiresLock")]
pub shortcut_requires_lock: bool,
pub disabled: bool,
// Children
pub children: MenuListEntrySections,
#[serde(rename = "childrenHash")]
#[widget_builder(skip)]
pub children_hash: u64,
// Styling
pub font: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct FontInput {
#[serde(rename = "fontFamily")]
#[widget_builder(constructor)]
pub font_family: String,
#[serde(rename = "fontStyle")]
#[widget_builder(constructor)]
pub font_style: String,
#[serde(rename = "isStyle")]
pub is_style_picker: bool,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<FontInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
}
/// This widget allows for the flexible use of the layout system.
/// In a custom layout, one can define a widget that is just used to trigger code on the backend.
/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend.
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct InvisibleStandinInput {
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
impl std::hash::Hash for MenuListEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
self.label.hash(state);
self.icon.hash(state);
self.disabled.hash(state);
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {
// Label
pub label: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Disabled
pub disabled: bool,
// Value
// Content
#[widget_builder(constructor)]
pub value: Option<f64>,
pub label: String,
pub disabled: bool,
// Styling
pub narrow: bool,
// Behavior
pub mode: NumberInputMode,
#[widget_builder(skip)]
pub min: Option<f64>,
#[widget_builder(skip)]
pub max: Option<f64>,
// TODO: Make this (and range_max) apply to both Range and Increment modes when dragging with the mouse
#[serde(rename = "rangeMin")]
pub range_min: Option<f64>,
#[serde(rename = "rangeMax")]
pub range_max: Option<f64>,
#[derivative(Default(value = "1."))]
pub step: f64,
#[serde(rename = "isInteger")]
pub is_integer: bool,
// Number presentation
#[serde(rename = "incrementBehavior")]
pub increment_behavior: NumberInputIncrementBehavior,
#[serde(rename = "displayDecimalPlaces")]
#[derivative(Default(value = "2"))]
pub display_decimal_places: u32,
pub unit: String,
#[serde(rename = "unitIsHiddenWhenEditing")]
#[derivative(Default(value = "true"))]
pub unit_is_hidden_when_editing: bool,
// Mode behavior
pub mode: NumberInputMode,
#[serde(rename = "incrementBehavior")]
pub increment_behavior: NumberInputIncrementBehavior,
#[derivative(Default(value = "1."))]
pub step: f64,
// TODO: Make this (and range_max) apply to both Range and Increment modes when dragging with the mouse
#[serde(rename = "rangeMin")]
pub range_min: Option<f64>,
#[serde(rename = "rangeMax")]
pub range_max: Option<f64>,
// Styling
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_increase: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_decrease: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -310,16 +264,17 @@ pub enum NumberInputMode {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct NodeCatalog {
// Content
pub disabled: bool,
// Behavior
#[serde(rename = "initialSearchTerm")]
pub intial_search: String,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<String>,
pub on_update: WidgetCallback<DefinitionIdentifier>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -328,40 +283,48 @@ pub struct NodeCatalog {
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct RadioInput {
#[widget_builder(constructor)]
pub entries: Vec<RadioEntryData>,
pub disabled: bool,
// Content
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
pub disabled: bool,
// Children
#[widget_builder(constructor)]
pub entries: Vec<RadioEntryData>,
// Styling
pub narrow: bool,
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
//
// Callbacks exists on the `RadioEntryData` children, not this parent `RadioInput`
}
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
#[widget_builder(not_widget_holder)]
#[widget_builder(not_widget_instance)]
pub struct RadioEntryData {
// Content
#[widget_builder(constructor)]
pub value: String,
pub label: String,
pub icon: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -370,9 +333,9 @@ pub struct RadioEntryData {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct WorkingColorsInput {
// Content
#[widget_builder(constructor)]
pub primary: Color,
#[widget_builder(constructor)]
pub secondary: Color,
}
@@ -380,20 +343,24 @@ pub struct WorkingColorsInput {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct TextAreaInput {
// Content
#[widget_builder(constructor)]
pub value: String,
pub label: Option<String>,
pub disabled: bool,
pub tooltip: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextAreaInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -402,28 +369,35 @@ pub struct TextAreaInput {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct TextInput {
// Content
#[widget_builder(constructor)]
pub value: String,
pub label: Option<String>,
pub placeholder: Option<String>,
pub disabled: bool,
pub tooltip: String,
// Styling
pub narrow: bool,
pub centered: bool,
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -432,18 +406,22 @@ pub struct TextInput {
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct CurveInput {
// Content
#[widget_builder(constructor)]
pub value: Curve,
pub disabled: bool,
pub tooltip: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<CurveInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -452,18 +430,23 @@ pub struct CurveInput {
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ReferencePointInput {
// Content
#[widget_builder(constructor)]
pub value: ReferencePoint,
pub disabled: bool,
pub tooltip: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<ReferencePointInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
@@ -1,24 +1,30 @@
use super::input_widgets::CheckboxId;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use derivative::*;
use graphite_proc_macros::WidgetBuilder;
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Default, PartialEq, Eq, WidgetBuilder, specta::Type)]
pub struct IconLabel {
// Content
#[widget_builder(constructor)]
pub icon: String,
pub disabled: bool,
pub tooltip: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
pub struct Separator {
// Content
pub direction: SeparatorDirection,
#[serde(rename = "type")]
#[widget_builder(constructor)]
pub separator_type: SeparatorType,
pub style: SeparatorStyle,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -29,55 +35,72 @@ pub enum SeparatorDirection {
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum SeparatorType {
pub enum SeparatorStyle {
Related,
#[default]
Unrelated,
Section,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, PartialEq, Eq, Default, WidgetBuilder, specta::Type)]
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Eq, Default, WidgetBuilder, specta::Type)]
#[derivative(PartialEq)]
pub struct TextLabel {
// Content
#[widget_builder(constructor)]
pub value: String,
pub disabled: bool,
pub bold: bool,
pub italic: bool,
pub monospace: bool,
pub multiline: bool,
#[serde(rename = "centerAlign")]
pub center_align: bool,
#[serde(rename = "tableAlign")]
pub table_align: bool,
#[serde(rename = "minWidth")]
pub min_width: String,
pub tooltip: String,
#[serde(rename = "forCheckbox")]
pub for_checkbox: CheckboxId,
// Body
#[widget_builder(constructor)]
pub value: String,
// Styling
pub narrow: bool,
pub bold: bool,
pub italic: bool,
pub monospace: bool,
pub multiline: bool,
#[serde(rename = "centerAlign")]
pub center_align: bool,
#[serde(rename = "tableAlign")]
pub table_align: bool,
// Sizing
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "minWidthCharacters")]
pub min_width_characters: u32,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ImageLabel {
// Content
#[widget_builder(constructor)]
pub url: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
}
// TODO: Add UserInputLabel
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ShortcutLabel {
// Content
// This is wrapped in an Option to satisfy the requirement that widgets implement Default
#[widget_builder(constructor)]
pub shortcut: Option<ActionShortcut>,
}
@@ -1,131 +0,0 @@
use super::input_widgets::InvisibleStandinInput;
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Default, specta::Type)]
pub struct MenuBarEntryChildren(pub Vec<Vec<MenuBarEntry>>);
impl MenuBarEntryChildren {
pub fn empty() -> Self {
Self(Vec::new())
}
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
let entries = self.0.iter_mut().flatten();
for entry in entries {
if let Some(action_keys) = &mut entry.shortcut {
action_keys.to_keys(action_input_mapping);
}
// Recursively do this for the children also
entry.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)]
pub struct MenuBarEntry {
pub label: String,
pub icon: Option<String>,
pub shortcut: Option<ActionKeys>,
pub action: WidgetHolder,
pub children: MenuBarEntryChildren,
pub disabled: bool,
}
impl MenuBarEntry {
pub fn new_root(label: String, disabled: bool, children: MenuBarEntryChildren) -> Self {
Self {
label,
disabled,
children,
..Default::default()
}
}
pub fn create_action(callback: impl Fn(&()) -> Message + 'static + Send + Sync) -> WidgetHolder {
InvisibleStandinInput::new().on_update(callback).widget_holder()
}
pub fn no_action() -> WidgetHolder {
MenuBarEntry::create_action(|_| Message::NoOp)
}
}
impl Default for MenuBarEntry {
fn default() -> Self {
Self {
label: "".into(),
icon: None,
shortcut: None,
action: MenuBarEntry::no_action(),
children: MenuBarEntryChildren::empty(),
disabled: false,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct MenuLayout {
pub layout: Vec<MenuBarEntry>,
}
impl MenuLayout {
pub fn new(layout: Vec<MenuBarEntry>) -> Self {
Self { layout }
}
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
MenuLayoutIter { stack: self.layout.iter().collect() }
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut WidgetHolder> + '_ {
MenuLayoutIterMut {
stack: self.layout.iter_mut().collect(),
}
}
}
#[derive(Debug, Default)]
pub struct MenuLayoutIter<'a> {
pub stack: Vec<&'a MenuBarEntry>,
}
impl<'a> Iterator for MenuLayoutIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter());
self.stack.extend(more_entries);
Some(&menu_entry.action)
}
None => None,
}
}
}
pub struct MenuLayoutIterMut<'a> {
pub stack: Vec<&'a mut MenuBarEntry>,
}
impl<'a> Iterator for MenuLayoutIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut());
self.stack.extend(more_entries);
Some(&mut menu_entry.action)
}
None => None,
}
}
}
@@ -1,4 +1,3 @@
pub mod button_widgets;
pub mod input_widgets;
pub mod label_widgets;
pub mod menu_widgets;
@@ -1,6 +1,6 @@
use crate::messages::prelude::*;
#[impl_message(Message, PortfolioMessage, MenuBar)]
#[impl_message(Message, MenuBar)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
pub enum MenuBarMessage {
// Messages
@@ -0,0 +1,755 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
use crate::messages::prelude::*;
use graphene_std::path_bool::BooleanOperation;
#[derive(Debug, Clone, Default, ExtractField)]
pub struct MenuBarMessageHandler {
pub has_active_document: bool,
pub canvas_tilted: bool,
pub canvas_flipped: bool,
pub rulers_visible: bool,
pub node_graph_open: bool,
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
pub has_selection_history: (bool, 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,
pub focus_document: bool,
}
#[message_handler_data]
impl MessageHandler<MenuBarMessage, ()> for MenuBarMessageHandler {
fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
MenuBarMessage::SendLayout => {
self.send_layout(responses, LayoutTarget::MenuBar);
}
}
}
fn actions(&self) -> ActionList {
actions!(MenuBarMessageDiscriminant;)
}
}
impl LayoutHolder for MenuBarMessageHandler {
fn layout(&self) -> Layout {
let no_active_document = !self.has_active_document;
let node_graph_open = self.node_graph_open;
let has_selected_nodes = self.has_selected_nodes;
let has_selected_layers = self.has_selected_layers;
let has_selection_history = self.has_selection_history;
let message_logging_verbosity_off = self.message_logging_verbosity == MessageLoggingVerbosity::Off;
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
let make_path_editable_is_allowed = self.make_path_editable_is_allowed;
let about = MenuListEntry::new("About Graphite…")
.label({
#[cfg(not(target_os = "macos"))]
{
"About Graphite…"
}
#[cfg(target_os = "macos")]
{
"About Graphite"
}
})
.icon("GraphiteLogo")
.on_commit(|_| DialogMessage::RequestAboutGraphiteDialog.into());
let preferences = MenuListEntry::new("Preferences…")
.label("Preferences…")
.icon("Settings")
.tooltip_shortcut(action_shortcut!(DialogMessageDiscriminant::RequestPreferencesDialog))
.on_commit(|_| DialogMessage::RequestPreferencesDialog.into());
let menu_bar_buttons = vec![
#[cfg(not(target_os = "macos"))]
TextButton::new("Graphite")
.label("")
.flush(true)
.icon(Some("GraphiteLogo".into()))
.on_commit(|_| FrontendMessage::TriggerVisitLink { url: "https://graphite.art".into() }.into())
.widget_instance(),
#[cfg(target_os = "macos")]
TextButton::new("Graphite")
.label("")
.flush(true)
.menu_list_children(vec![
vec![about],
vec![preferences],
vec![
MenuListEntry::new("Hide Graphite")
.label("Hide Graphite")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Hide))
.on_commit(|_| AppWindowMessage::Hide.into()),
MenuListEntry::new("Hide Others")
.label("Hide Others")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::HideOthers))
.on_commit(|_| AppWindowMessage::HideOthers.into()),
MenuListEntry::new("Show All")
.label("Show All")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::ShowAll))
.on_commit(|_| AppWindowMessage::ShowAll.into()),
],
vec![
MenuListEntry::new("Quit Graphite")
.label("Quit Graphite")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Close))
.on_commit(|_| AppWindowMessage::Close.into()),
],
])
.widget_instance(),
TextButton::new("File")
.label("File")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("New…")
.label("New…")
.icon("File")
.on_commit(|_| DialogMessage::RequestNewDocumentDialog.into())
.tooltip_shortcut(action_shortcut!(DialogMessageDiscriminant::RequestNewDocumentDialog)),
MenuListEntry::new("Open…")
.label("Open…")
.icon("Folder")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Open))
.on_commit(|_| PortfolioMessage::Open.into()),
MenuListEntry::new("Open Demo Artwork…")
.label("Open Demo Artwork…")
.icon("Image")
.on_commit(|_| DialogMessage::RequestDemoArtworkDialog.into()),
],
vec![
MenuListEntry::new("Close")
.label("Close")
.icon("Close")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation))
.on_commit(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into())
.disabled(no_active_document),
MenuListEntry::new("Close All")
.label("Close All")
.icon("CloseAll")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::CloseAllDocumentsWithConfirmation))
.on_commit(|_| PortfolioMessage::CloseAllDocumentsWithConfirmation.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Save")
.label("Save")
.icon("Save")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SaveDocument))
.on_commit(|_| DocumentMessage::SaveDocument.into())
.disabled(no_active_document),
#[cfg(not(target_family = "wasm"))]
MenuListEntry::new("Save As…")
.label("Save As…")
.icon("Save")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SaveDocumentAs))
.on_commit(|_| DocumentMessage::SaveDocumentAs.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Import…")
.label("Import…")
.icon("FileImport")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Import))
.on_commit(|_| PortfolioMessage::Import.into())
.disabled(no_active_document),
MenuListEntry::new("Export…")
.label("Export…")
.icon("FileExport")
.tooltip_shortcut(action_shortcut!(DialogMessageDiscriminant::RequestExportDialog))
.on_commit(|_| DialogMessage::RequestExportDialog.into())
.disabled(no_active_document),
],
#[cfg(not(target_os = "macos"))]
vec![preferences],
])
.widget_instance(),
TextButton::new("Edit")
.label("Edit")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("Undo")
.label("Undo")
.icon("HistoryUndo")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::Undo))
.on_commit(|_| DocumentMessage::Undo.into())
.disabled(no_active_document),
MenuListEntry::new("Redo")
.label("Redo")
.icon("HistoryRedo")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::Redo))
.on_commit(|_| DocumentMessage::Redo.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Cut")
.label("Cut")
.icon("Cut")
.tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Cut))
.on_commit(|_| ClipboardMessage::Cut.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Copy")
.label("Copy")
.icon("Copy")
.tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Copy))
.on_commit(|_| ClipboardMessage::Copy.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Paste")
.label("Paste")
.icon("Paste")
.tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Paste))
.on_commit(|_| ClipboardMessage::Paste.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Duplicate")
.label("Duplicate")
.icon("Copy")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DuplicateSelectedLayers))
.on_commit(|_| DocumentMessage::DuplicateSelectedLayers.into())
.disabled(no_active_document || !has_selected_nodes),
MenuListEntry::new("Delete")
.label("Delete")
.icon("Trash")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers))
.on_commit(|_| DocumentMessage::DeleteSelectedLayers.into())
.disabled(no_active_document || !has_selected_nodes),
],
vec![
MenuListEntry::new("Convert to Infinite Canvas")
.label("Convert to Infinite Canvas")
.icon("Artboard")
.on_commit(|_| DocumentMessage::RemoveArtboards.into())
.disabled(no_active_document),
],
])
.widget_instance(),
TextButton::new("Layer")
.label("Layer")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("New")
.label("New")
.icon("NewLayer")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder))
.on_commit(|_| DocumentMessage::CreateEmptyFolder.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Group")
.label("Group")
.icon("Folder")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::GroupSelectedLayers))
.on_commit(|_| {
DocumentMessage::GroupSelectedLayers {
group_folder_type: GroupFolderType::Layer,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Ungroup")
.label("Ungroup")
.icon("FolderOpen")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::UngroupSelectedLayers))
.on_commit(|_| DocumentMessage::UngroupSelectedLayers.into())
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Hide/Show")
.label("Hide/Show")
.icon("EyeHide")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ToggleSelectedVisibility))
.on_commit(|_| DocumentMessage::ToggleSelectedVisibility.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Lock/Unlock")
.label("Lock/Unlock")
.icon("PadlockLocked")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ToggleSelectedLocked))
.on_commit(|_| DocumentMessage::ToggleSelectedLocked.into())
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Grab")
.label("Grab")
.icon("TransformationGrab")
.tooltip_shortcut(action_shortcut!(TransformLayerMessageDiscriminant::BeginGrab))
.on_commit(|_| TransformLayerMessage::BeginGrab.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Rotate")
.label("Rotate")
.icon("TransformationRotate")
.tooltip_shortcut(action_shortcut!(TransformLayerMessageDiscriminant::BeginRotate))
.on_commit(|_| TransformLayerMessage::BeginRotate.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Scale")
.label("Scale")
.icon("TransformationScale")
.tooltip_shortcut(action_shortcut!(TransformLayerMessageDiscriminant::BeginScale))
.on_commit(|_| TransformLayerMessage::BeginScale.into())
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Arrange")
.label("Arrange")
.icon("StackHollow")
.disabled(no_active_document || !has_selected_layers)
.children(vec![
vec![
MenuListEntry::new("Raise To Front")
.label("Raise To Front")
.icon("Stack")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront))
.on_commit(|_| DocumentMessage::SelectedLayersRaiseToFront.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Raise")
.label("Raise")
.icon("StackRaise")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaise))
.on_commit(|_| DocumentMessage::SelectedLayersRaise.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Lower")
.label("Lower")
.icon("StackLower")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLower))
.on_commit(|_| DocumentMessage::SelectedLayersLower.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Lower to Back")
.label("Lower to Back")
.icon("StackBottom")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLowerToBack))
.on_commit(|_| DocumentMessage::SelectedLayersLowerToBack.into())
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Reverse")
.label("Reverse")
.icon("StackReverse")
.on_commit(|_| DocumentMessage::SelectedLayersReverse.into())
.disabled(no_active_document || !has_selected_layers),
],
]),
MenuListEntry::new("Align")
.label("Align")
.icon("AlignVerticalCenter")
.disabled(no_active_document || !has_selected_layers)
.children(vec![
vec![
MenuListEntry::new("Align Left")
.label("Align Left")
.icon("AlignLeft")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Min,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Align Horizontal Center")
.label("Align Horizontal Center")
.icon("AlignHorizontalCenter")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Center,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Align Right")
.label("Align Right")
.icon("AlignRight")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Max,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
],
vec![
MenuListEntry::new("Align Top")
.label("Align Top")
.icon("AlignTop")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Min,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Align Vertical Center")
.label("Align Vertical Center")
.icon("AlignVerticalCenter")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Center,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Align Bottom")
.label("Align Bottom")
.icon("AlignBottom")
.on_commit(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Max,
}
.into()
})
.disabled(no_active_document || !has_selected_layers),
],
]),
MenuListEntry::new("Flip")
.label("Flip")
.icon("FlipVertical")
.disabled(no_active_document || !has_selected_layers)
.children(vec![vec![
MenuListEntry::new("Flip Horizontal")
.label("Flip Horizontal")
.icon("FlipHorizontal")
.on_commit(|_| DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::X }.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Flip Vertical")
.label("Flip Vertical")
.icon("FlipVertical")
.on_commit(|_| DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::Y }.into())
.disabled(no_active_document || !has_selected_layers),
]]),
MenuListEntry::new("Turn")
.label("Turn")
.icon("TurnPositive90")
.disabled(no_active_document || !has_selected_layers)
.children(vec![vec![
MenuListEntry::new("Turn -90°")
.label("Turn -90°")
.icon("TurnNegative90")
.on_commit(|_| DocumentMessage::RotateSelectedLayers { degrees: -90. }.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Turn 90°")
.label("Turn 90°")
.icon("TurnPositive90")
.on_commit(|_| DocumentMessage::RotateSelectedLayers { degrees: 90. }.into())
.disabled(no_active_document || !has_selected_layers),
]]),
MenuListEntry::new("Boolean")
.label("Boolean")
.icon("BooleanSubtractFront")
.disabled(no_active_document || !has_selected_layers)
.children(vec![vec![
MenuListEntry::new("Union")
.label("Union")
.icon("BooleanUnion")
.on_commit(|_| {
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Union);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Subtract Front")
.label("Subtract Front")
.icon("BooleanSubtractFront")
.on_commit(|_| {
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::SubtractFront);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Subtract Back")
.label("Subtract Back")
.icon("BooleanSubtractBack")
.on_commit(|_| {
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::SubtractBack);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Intersect")
.label("Intersect")
.icon("BooleanIntersect")
.on_commit(|_| {
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Intersect);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Difference")
.label("Difference")
.icon("BooleanDifference")
.on_commit(|_| {
let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Difference);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
})
.disabled(no_active_document || !has_selected_layers),
]]),
],
vec![
MenuListEntry::new("Make Path Editable")
.label("Make Path Editable")
.icon("NodeShape")
.on_commit(|_| NodeGraphMessage::AddPathNode.into())
.disabled(!make_path_editable_is_allowed),
],
])
.widget_instance(),
TextButton::new("Select")
.label("Select")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("Select All")
.label("Select All")
.icon("SelectAll")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectAllLayers))
.on_commit(|_| DocumentMessage::SelectAllLayers.into())
.disabled(no_active_document),
MenuListEntry::new("Deselect All")
.label("Deselect All")
.icon("DeselectAll")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DeselectAllLayers))
.on_commit(|_| DocumentMessage::DeselectAllLayers.into())
.disabled(no_active_document || !has_selected_nodes),
MenuListEntry::new("Select Parent")
.label("Select Parent")
.icon("SelectParent")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectParentLayer))
.on_commit(|_| DocumentMessage::SelectParentLayer.into())
.disabled(no_active_document || !has_selected_nodes),
],
vec![
MenuListEntry::new("Previous Selection")
.label("Previous Selection")
.icon("HistoryUndo")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectionStepBack))
.on_commit(|_| DocumentMessage::SelectionStepBack.into())
.disabled(!has_selection_history.0),
MenuListEntry::new("Next Selection")
.label("Next Selection")
.icon("HistoryRedo")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::SelectionStepForward))
.on_commit(|_| DocumentMessage::SelectionStepForward.into())
.disabled(!has_selection_history.1),
],
])
.widget_instance(),
TextButton::new("View")
.label("View")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("Tilt")
.label("Tilt")
.icon("Tilt")
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::BeginCanvasTilt))
.on_commit(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into())
.disabled(no_active_document || node_graph_open),
MenuListEntry::new("Reset Tilt")
.label("Reset Tilt")
.icon("TiltReset")
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::CanvasTiltSet))
.on_commit(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into())
.disabled(no_active_document || node_graph_open || !self.canvas_tilted),
],
vec![
MenuListEntry::new("Zoom In")
.label("Zoom In")
.icon("ZoomIn")
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::CanvasZoomIncrease))
.on_commit(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
.disabled(no_active_document),
MenuListEntry::new("Zoom Out")
.label("Zoom Out")
.icon("ZoomOut")
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::CanvasZoomDecrease))
.on_commit(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
.disabled(no_active_document),
MenuListEntry::new("Zoom to Selection")
.label("Zoom to Selection")
.icon("FrameSelected")
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::FitViewportToSelection))
.on_commit(|_| NavigationMessage::FitViewportToSelection.into())
.disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Zoom to Fit")
.label("Zoom to Fit")
.icon("FrameAll")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ZoomCanvasToFitAll))
.on_commit(|_| DocumentMessage::ZoomCanvasToFitAll.into())
.disabled(no_active_document),
MenuListEntry::new("Zoom to 100%")
.label("Zoom to 100%")
.icon("Zoom1x")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent))
.on_commit(|_| DocumentMessage::ZoomCanvasTo100Percent.into())
.disabled(no_active_document),
MenuListEntry::new("Zoom to 200%")
.label("Zoom to 200%")
.icon("Zoom2x")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ZoomCanvasTo200Percent))
.on_commit(|_| DocumentMessage::ZoomCanvasTo200Percent.into())
.disabled(no_active_document),
],
vec![
MenuListEntry::new("Flip")
.label("Flip")
.icon(if self.canvas_flipped { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(NavigationMessageDiscriminant::CanvasFlip))
.on_commit(|_| NavigationMessage::CanvasFlip.into())
.disabled(no_active_document || node_graph_open),
],
vec![
MenuListEntry::new("Rulers")
.label("Rulers")
.icon(if self.rulers_visible { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleRulers))
.on_commit(|_| PortfolioMessage::ToggleRulers.into())
.disabled(no_active_document),
],
])
.widget_instance(),
TextButton::new("Window")
.label("Window")
.flush(true)
.menu_list_children(vec![
vec![
MenuListEntry::new("Fullscreen")
.label("Fullscreen")
.icon("FullscreenEnter")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Fullscreen))
.on_commit(|_| AppWindowMessage::Fullscreen.into()),
MenuListEntry::new("Focus Document")
.label("Focus Document")
.icon(if self.focus_document { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleFocusDocument))
.on_commit(|_| PortfolioMessage::ToggleFocusDocument.into()),
],
vec![
MenuListEntry::new("Properties")
.label("Properties")
.icon(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen))
.on_commit(|_| PortfolioMessage::TogglePropertiesPanelOpen.into())
.disabled(self.focus_document),
MenuListEntry::new("Layers")
.label("Layers")
.icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen))
.on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into())
.disabled(self.focus_document),
MenuListEntry::new("Data")
.label("Data")
.icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleDataPanelOpen))
.on_commit(|_| PortfolioMessage::ToggleDataPanelOpen.into())
.disabled(self.focus_document),
],
])
.widget_instance(),
TextButton::new("Help")
.label("Help")
.flush(true)
.menu_list_children(vec![
#[cfg(not(target_os = "macos"))]
vec![about],
vec![
MenuListEntry::new("Donate to Graphite").label("Donate to Graphite").icon("Heart").on_commit(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.art/donate/".into(),
}
.into()
}),
MenuListEntry::new("User Manual").label("User Manual").icon("UserManual").on_commit(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.art/learn/".into(),
}
.into()
}),
MenuListEntry::new("Report a Bug").label("Report a Bug").icon("Bug").on_commit(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
}),
MenuListEntry::new("Visit on GitHub").label("Visit on GitHub").icon("Website").on_commit(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite".into(),
}
.into()
}),
],
vec![MenuListEntry::new("Developer Debug").label("Developer Debug").icon("Code").children(vec![
vec![
MenuListEntry::new("Reset Nodes to Definitions on Open")
.label("Reset Nodes to Definitions on Open")
.icon(if reset_node_definitions_on_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.on_commit(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()),
],
vec![
MenuListEntry::new("Print Trace Logs")
.label("Print Trace Logs")
.icon(if log::max_level() == log::LevelFilter::Trace { "CheckboxChecked" } else { "CheckboxUnchecked" })
.on_commit(|_| DebugMessage::ToggleTraceLogs.into()),
MenuListEntry::new("Print Messages: Off")
.label("Print Messages: Off")
.icon(if message_logging_verbosity_off {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
}
#[cfg(target_os = "macos")]
{
"CheckboxChecked".to_string()
}
} else { Default::default() })
.tooltip_shortcut(action_shortcut!(DebugMessageDiscriminant::MessageOff))
.on_commit(|_| DebugMessage::MessageOff.into()),
MenuListEntry::new("Print Messages: Only Names")
.label("Print Messages: Only Names")
.icon(if message_logging_verbosity_names {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
}
#[cfg(target_os = "macos")]
{
"CheckboxChecked".to_string()
}
} else { Default::default() })
.tooltip_shortcut(action_shortcut!(DebugMessageDiscriminant::MessageNames))
.on_commit(|_| DebugMessage::MessageNames.into()),
MenuListEntry::new("Print Messages: Full Contents")
.label("Print Messages: Full Contents")
.icon(if message_logging_verbosity_contents {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
}
#[cfg(target_os = "macos")]
{
"CheckboxChecked".to_string()
}
} else { Default::default() })
.tooltip_shortcut(action_shortcut!(DebugMessageDiscriminant::MessageContents))
.on_commit(|_| DebugMessage::MessageContents.into()),
],
vec![MenuListEntry::new("Trigger a Crash").label("Trigger a Crash").icon("Warning").on_commit(|_| panic!())],
])],
])
.widget_instance(),
];
Layout(vec![LayoutGroup::Row { widgets: menu_bar_buttons }])
}
}
+9 -94
View File
@@ -12,6 +12,8 @@ pub enum Message {
#[child]
Broadcast(BroadcastMessage),
#[child]
Clipboard(ClipboardMessage),
#[child]
Debug(DebugMessage),
#[child]
Defer(DeferMessage),
@@ -20,19 +22,21 @@ pub enum Message {
#[child]
Frontend(FrontendMessage),
#[child]
Globals(GlobalsMessage),
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
KeyMapping(KeyMappingMessage),
#[child]
Layout(LayoutMessage),
#[child]
MenuBar(MenuBarMessage),
#[child]
Portfolio(PortfolioMessage),
#[child]
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage),
#[child]
Viewport(ViewportMessage),
// Messages
Batched {
@@ -49,97 +53,8 @@ impl specta::Type for MessageDiscriminant {
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io::Write;
#[test]
fn generate_message_tree() {
let result = Message::build_message_tree();
let mut file = std::fs::File::create("../hierarchical_message_system_tree.txt").unwrap();
file.write_all(format!("{} `{}`\n", result.name(), result.path()).as_bytes()).unwrap();
if let Some(variants) = result.variants() {
for (i, variant) in variants.iter().enumerate() {
let is_last = i == variants.len() - 1;
print_tree_node(variant, "", is_last, &mut file);
}
}
}
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
// Print the current node
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
("├── ", format!("{prefix}"))
} else if is_last {
("└── ", format!("{prefix} "))
} else {
("├── ", format!("{prefix}"))
};
if tree.path().is_empty() {
file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()).unwrap();
} else {
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, tree.name(), tree.path()).as_bytes()).unwrap();
}
// Print children if any
if let Some(variants) = tree.variants() {
let len = variants.len();
for (i, variant) in variants.iter().enumerate() {
let is_last_child = i == len - 1;
print_tree_node(variant, &child_prefix, is_last_child, file);
}
}
// Print message field if any
if let Some(fields) = tree.fields() {
let len = fields.len();
for (i, field) in fields.iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap();
}
}
// Print handler field if any
if let Some(data) = tree.message_handler_fields() {
let len = data.fields().len();
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
("├── ", format!("{prefix}"))
} else {
("└── ", format!("{prefix} "))
};
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
} else if tree.name() != FRONTEND_MESSAGE_STR {
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
for (i, field) in data.fields().iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
}
}
}
// Print data field if any
if let Some(data) = tree.message_handler_data_fields() {
let len = data.fields().len();
if data.path().is_empty() {
file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()).unwrap();
} else {
file.write_all(format!("{}{}{} `{}`\n", prefix, "└── ", data.name(), data.path()).as_bytes()).unwrap();
}
for (i, field) in data.fields().iter().enumerate() {
let is_last_field = i == len - 1;
let branch = if is_last_field { "└── " } else { "├── " };
file.write_all(format!("{}{}{}\n", format!("{} ", prefix), branch, field.0).as_bytes()).unwrap();
}
}
impl Message {
pub fn message_tree() -> DebugMessageTree {
Self::build_message_tree()
}
}
+3 -1
View File
@@ -3,16 +3,18 @@
pub mod animation;
pub mod app_window;
pub mod broadcast;
pub mod clipboard;
pub mod debug;
pub mod defer;
pub mod dialog;
pub mod frontend;
pub mod globals;
pub mod input_mapper;
pub mod input_preprocessor;
pub mod layout;
pub mod menu_bar;
pub mod message;
pub mod portfolio;
pub mod preferences;
pub mod prelude;
pub mod tool;
pub mod viewport;
@@ -1,9 +1,10 @@
use super::VectorTableTab;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget};
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 glam::{Affine2, Vec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
@@ -82,11 +83,12 @@ impl DataPanelMessageHandler {
};
// 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 layout = 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();
@@ -96,13 +98,13 @@ impl DataPanelMessageHandler {
widgets.extend([
if is_layer {
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder()
IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance()
} else {
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder()
IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance()
},
Separator::new(SeparatorType::Related).widget_holder(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextInput::new(network_interface.display_name(&node_id, &[]))
.tooltip(if is_layer { "Name of the selected layer" } else { "Name of the selected node" })
.tooltip_description(if is_layer { "Name of the selected layer." } else { "Name of the selected node." })
.on_update(move |text_input| {
NodeGraphMessage::SetDisplayName {
node_id,
@@ -112,8 +114,8 @@ impl DataPanelMessageHandler {
.into()
})
.max_width(200)
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
]);
}
@@ -121,16 +123,16 @@ impl DataPanelMessageHandler {
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();
.widget_instance();
widgets.push(breadcrumb);
}
if !widgets.is_empty() {
layout.insert(0, LayoutGroup::Row { widgets });
layout.0.insert(0, LayoutGroup::Row { widgets });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout,
layout_target: LayoutTarget::DataPanel,
});
}
@@ -164,6 +166,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Vec<String>,
f64,
u32,
u64,
@@ -175,12 +178,12 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
])
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
fn column_headings(value: &[&str]) -> Vec<WidgetInstance> {
value.iter().map(|text| TextLabel::new(*text).widget_instance()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
let error = vec![TextLabel::new(x).widget_instance()];
vec![LayoutGroup::Row { widgets: error }]
}
@@ -191,22 +194,55 @@ trait TableRowLayout {
data.breadcrumbs.push(self.identifier());
self.element_page(data)
}
fn element_widget(&self, index: usize) -> WidgetHolder {
fn element_widget(&self, index: usize) -> WidgetInstance {
TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.widget_holder()
.narrow(true)
.widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![]
}
}
impl<T: TableRowLayout> TableRowLayout for Vec<T> {
fn type_name() -> &'static str {
"Vec"
}
fn identifier(&self) -> String {
format!("Vec<{}> ({} element{})", 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.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}")).narrow(true).widget_instance(), row.element_widget(index)])
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element"]));
vec![LayoutGroup::Table { rows, unstyled: false }]
}
}
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" })
format!("Table<{}> ({} element{})", 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() {
@@ -226,18 +262,20 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
.enumerate()
.map(|(index, row)| {
vec![
TextLabel::new(format!("{index}")).widget_holder(),
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
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(),
TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_instance(),
TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_instance(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0)))
.narrow(true)
.widget_instance(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
vec![LayoutGroup::Table { rows, unstyled: false }]
}
}
@@ -305,7 +343,7 @@ impl TableRowLayout for Vector {
.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 table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_instance()];
let mut table_rows = Vec::new();
match data.vector_table_tab {
@@ -314,104 +352,130 @@ impl TableRowLayout for Vector {
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(),
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
]),
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(),
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::Solid(color))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder(),
.narrow(true)
.widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
TextLabel::new("Fill Gradient Type").narrow(true).widget_instance(),
TextLabel::new(gradient.gradient_type.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
TextLabel::new("Fill Gradient Start").narrow(true).widget_instance(),
TextLabel::new(format_dvec2(gradient.start)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient End").narrow(true).widget_instance(),
TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_instance(),
]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).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(),
TextLabel::new("Stroke").narrow(true).widget_instance(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
]);
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("Stroke Weight").narrow(true).widget_instance(),
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_instance(),
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(),
.narrow(true)
.widget_instance(),
]);
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(),
TextLabel::new("Stroke Dash Offset").narrow(true).widget_instance(),
TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
TextLabel::new("Stroke Cap").narrow(true).widget_instance(),
TextLabel::new(stroke.cap.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
TextLabel::new("Stroke Join").narrow(true).widget_instance(),
TextLabel::new(stroke.join.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_instance(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Align").narrow(true).widget_instance(),
TextLabel::new(stroke.align.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").narrow(true).widget_instance(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(),
TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_instance(),
]);
}
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("Colinear Handle IDs").narrow(true).widget_instance(),
TextLabel::new(colinear).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() {
TextLabel::new("Upstream Nested Layers").narrow(true).widget_instance(),
TextLabel::new(if self.upstream_data.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(),
.narrow(true)
.widget_instance(),
]);
}
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()]),
);
table_rows.extend(self.point_domain.iter().map(|(id, position)| {
vec![
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
TextLabel::new(format!("{position}")).narrow(true).widget_instance(),
]
}));
}
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(),
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
TextLabel::new(format!("{start}")).narrow(true).widget_instance(),
TextLabel::new(format!("{end}")).narrow(true).widget_instance(),
TextLabel::new(format!("{handles:?}")).narrow(true).widget_instance(),
]
}));
}
@@ -419,15 +483,15 @@ impl TableRowLayout for Vector {
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(),
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
TextLabel::new(format!("{segment_range:?}")).narrow(true).widget_instance(),
TextLabel::new(format!("{}", fill.inner())).narrow(true).widget_instance(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }]
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows, unstyled: false }]
}
}
@@ -439,10 +503,17 @@ impl TableRowLayout for Raster<CPU> {
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(|| {
let raster = self.data();
if raster.width == 0 || raster.height == 0 {
let widgets = vec![TextLabel::new("Image has no area").widget_instance()];
return vec![LayoutGroup::Row { widgets }];
}
let base64_string = raster.base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let output = raster.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);
@@ -450,7 +521,7 @@ impl TableRowLayout for Raster<CPU> {
base64_string
});
let widgets = vec![ImageLabel::new(base64_string).widget_holder()];
let widgets = vec![ImageLabel::new(base64_string).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -463,7 +534,7 @@ impl TableRowLayout for Raster<GPU> {
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()];
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -475,8 +546,12 @@ impl TableRowLayout for 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_widget(&self, _index: usize) -> WidgetInstance {
ColorInput::new(FillChoice::Solid(*self))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
@@ -491,11 +566,12 @@ impl TableRowLayout for GradientStops {
fn identifier(&self) -> String {
format!("Gradient ({} stops)", self.0.len())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
fn element_widget(&self, _index: usize) -> WidgetInstance {
ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder()
.disabled(true)
.narrow(true)
.widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
@@ -511,7 +587,7 @@ impl TableRowLayout for f64 {
"Number (f64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
let widgets = vec![TextLabel::new(self.to_string()).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -524,7 +600,7 @@ impl TableRowLayout for u32 {
"Number (u32)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
let widgets = vec![TextLabel::new(self.to_string()).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -537,7 +613,7 @@ impl TableRowLayout for u64 {
"Number (u64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
let widgets = vec![TextLabel::new(self.to_string()).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -550,7 +626,7 @@ impl TableRowLayout for bool {
"Bool".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
let widgets = vec![TextLabel::new(self.to_string()).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -560,10 +636,16 @@ impl TableRowLayout for String {
"String"
}
fn identifier(&self) -> String {
"String".to_string()
// Show the first line, and if there are more, indicate that with an ellipsis
let first_line = self.lines().next().unwrap_or("");
if self.lines().count() > 1 {
format!("\"{}\"", first_line)
} else {
format!("\"{}\"", first_line)
}
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_holder()];
let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -576,7 +658,7 @@ impl TableRowLayout for Option<f64> {
"Option<f64>".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()];
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -589,7 +671,20 @@ impl TableRowLayout for DVec2 {
"Vec2".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Vec2 {
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_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -602,7 +697,21 @@ impl TableRowLayout for DAffine2 {
"Transform".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()];
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Affine2 {
fn type_name() -> &'static str {
"Transform"
}
fn identifier(&self) -> String {
"Transform".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_instance()];
vec![LayoutGroup::Row { widgets }]
}
}
@@ -1,10 +1,10 @@
use std::path::PathBuf;
use std::sync::Arc;
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::overlays::utility_types::{OverlayContext, OverlaysType};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
use crate::messages::portfolio::utility_types::PanelType;
@@ -16,7 +16,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode;
use graphene_std::vector::style::RenderMode;
#[impl_message(Message, PortfolioMessage, Document)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
@@ -122,6 +122,7 @@ pub enum DocumentMessage {
SavedDocument {
path: Option<PathBuf>,
},
MarkAsSaved,
SelectParentLayer,
SelectAllLayers,
SelectedLayersLower,
@@ -176,13 +177,14 @@ pub enum DocumentMessage {
node_id: NodeId,
is_layer: bool,
},
SetViewMode {
view_mode: ViewMode,
SetRenderMode {
render_mode: RenderMode,
},
AddTransaction,
StartTransaction,
EndTransaction,
CommitTransaction,
CancelTransaction,
AbortTransaction,
RepeatedAbortTransaction {
undo_count: usize,
@@ -202,7 +204,7 @@ pub enum DocumentMessage {
first_element_source_id: HashMap<NodeId, Option<NodeId>>,
},
UpdateClickTargets {
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
},
UpdateClipTargets {
clip_targets: HashSet<NodeId>,
File diff suppressed because it is too large Load Diff
@@ -7,10 +7,12 @@ use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
@@ -113,6 +115,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
GraphOperationMessage::NewArtboard { id, artboard } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let artboard_location = artboard.location;
let artboard_layer = modify_inputs.create_artboard(id, artboard);
network_interface.move_layer_to_stack(artboard_layer, LayerNodeIdentifier::ROOT_PARENT, 0, &[]);
@@ -121,13 +124,41 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
log::error!("Artboard not created");
return;
};
let document_metadata = network_interface.document_metadata();
let primary_input = artboard.inputs.first().expect("Artboard should have a primary input").clone();
if let NodeInput::Node { node_id, .. } = &primary_input {
if network_interface.is_layer(node_id, &[]) && !network_interface.is_artboard(node_id, &[]) {
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(*node_id, network_interface), artboard_layer, 0, &[]);
if network_interface.is_artboard(node_id, &[]) {
// Nothing to do here: we have a stack full of artboards!
} else if network_interface.is_layer(node_id, &[]) {
// We have a stack of non-layer artboards.
for (insert_index, layer) in LayerNodeIdentifier::ROOT_PARENT.children(document_metadata).filter(|&layer| layer != artboard_layer).enumerate() {
// Parent the layer to our new artboard (retaining ordering)
responses.add(NodeGraphMessage::MoveLayerToStack {
layer,
parent: artboard_layer,
insert_index,
});
// Apply a translation to prevent the content from shifting
responses.add(GraphOperationMessage::TransformChange {
layer,
transform: DAffine2::from_translation(-artboard_location.as_dvec2()),
transform_in: TransformIn::Local,
skip_rerender: true,
});
}
// Set the bottom input of the artboard back to artboard
let bottom_input = NodeInput::value(TaggedValue::Artboard(Table::new()), true);
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
} else {
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
network_interface.set_input(&InputConnector::node(id, 0), primary_input, &[]);
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
// Set the bottom input of the artboard back to artboard
let bottom_input = NodeInput::value(TaggedValue::Artboard(Table::new()), true);
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
}
}
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
@@ -356,7 +387,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
modify_inputs.insert_vector(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
if let Some(transform_node_id) = modify_inputs.existing_node_id("Transform", true) {
if let Some(transform_node_id) = modify_inputs.existing_network_node_id("Transform", true) {
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
}
@@ -32,10 +32,13 @@ pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64
/// Update the inputs of the transform node to match a new transform
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
let (scale, angle, translation, shear) = compute_scale_angle_translation_shear(transform);
let (scale, rotation, translation, shear) = compute_scale_angle_translation_shear(transform);
let rotation = rotation.to_degrees();
let shear = DVec2::new(shear.x.atan().to_degrees(), shear.y.atan().to_degrees());
network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(translation), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(angle), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(rotation), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::DVec2(scale), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(shear), false), &[]);
}
@@ -76,14 +79,14 @@ pub fn get_current_transform(inputs: &[NodeInput]) -> DAffine2 {
} else {
DVec2::ZERO
};
let angle = if let Some(&TaggedValue::F64(angle)) = inputs[2].as_value() { angle } else { 0. };
let rotation = if let Some(&TaggedValue::F64(rotation)) = inputs[2].as_value() { rotation } else { 0. };
let scale = if let Some(&TaggedValue::DVec2(scale)) = inputs[3].as_value() { scale } else { DVec2::ONE };
let shear = if let Some(&TaggedValue::DVec2(shear)) = inputs[4].as_value() { shear } else { DVec2::ZERO };
DAffine2::from_scale_angle_translation(scale, angle, translation) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.])
let rotation = rotation.to_radians();
let shear = DVec2::new(shear.x.to_radians().tan(), shear.y.to_radians().tan());
DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.])
}
/// Extract the current normalized pivot from the layer
@@ -1,12 +1,12 @@
use super::transform_utils;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*;
use glam::{DAffine2, IVec2};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
@@ -123,14 +123,14 @@ impl<'a> ModifyInputsContext<'a> {
/// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
let new_merge_node = resolve_document_node_type("Merge").expect("Merge node").default_node_template();
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
self.network_interface.insert_node(new_id, new_merge_node, &[]);
LayerNodeIdentifier::new(new_id, self.network_interface)
}
/// Creates an artboard as the primary export for the document network
pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> LayerNodeIdentifier {
let artboard_node_template = resolve_document_node_type("Artboard").expect("Node").node_template_input_override([
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
Some(NodeInput::value(TaggedValue::Artboard(Default::default()), true)),
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)),
@@ -143,7 +143,7 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
let boolean = resolve_network_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
]);
@@ -156,7 +156,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector = Table::new_from_element(Vector::from_subpaths(subpaths, true));
let shape = resolve_document_node_type("Path")
let shape = resolve_network_node_type("Path")
.expect("Path node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::Vector(vector), false))]);
let shape_id = NodeId::new();
@@ -164,21 +164,25 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
if include_transform {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
}
if include_fill {
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let fill = resolve_proto_node_type(graphene_std::vector_nodes::fill::IDENTIFIER)
.expect("Fill node does not exist")
.default_node_template();
let fill_id = NodeId::new();
self.network_interface.insert_node(fill_id, fill, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
}
if include_stroke {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER)
.expect("Stroke node does not exist")
.default_node_template();
let stroke_id = NodeId::new();
self.network_interface.insert_node(stroke_id, stroke, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
@@ -186,21 +190,29 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let text = resolve_document_node_type("Text").expect("Text node does not exist").node_template_input_override([
Some(NodeInput::scope("editor-api")),
Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
]);
let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER)
.expect("Stroke node does not exist")
.default_node_template();
let fill = resolve_proto_node_type(graphene_std::vector_nodes::fill::IDENTIFIER)
.expect("Fill node does not exist")
.default_node_template();
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let text = resolve_proto_node_type(graphene_std::text::text::IDENTIFIER)
.expect("Text node does not exist")
.node_template_input_override([
Some(NodeInput::scope("editor-api")),
Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
Some(NodeInput::value(TaggedValue::Bool(typesetting.max_width.is_some()), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.max_width.unwrap_or(100.)), false)),
Some(NodeInput::value(TaggedValue::Bool(typesetting.max_width.is_some()), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.max_width.unwrap_or(100.)), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
]);
let text_id = NodeId::new();
self.network_interface.insert_node(text_id, text, &[]);
@@ -220,8 +232,8 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn insert_image_data(&mut self, image_frame: Table<Raster<CPU>>, layer: LayerNodeIdentifier) {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let image = resolve_document_node_type("Image Value")
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let image = resolve_proto_node_type(graphene_std::raster_nodes::std_nodes::image_value::IDENTIFIER)
.expect("ImageValue node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Raster(image_frame), false))]);
@@ -245,18 +257,26 @@ impl<'a> ModifyInputsContext<'a> {
})
}
/// Gets the node id of a node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
/// The returned node is based on the selection dots in the layer. The right most dot will always insert/access the path that flows directly into the layer.
/// Each dot after that represents an existing path node. If there is an existing upstream node, then it will always be returned first.
pub fn existing_node_id(&mut self, reference_name: &'static str, create_if_nonexistent: bool) -> Option<NodeId> {
/// Gets the node id of a network node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
pub fn existing_network_node_id(&mut self, reference: &str, create_if_nonexistent: bool) -> Option<NodeId> {
self.existing_node_id(&DefinitionIdentifier::Network(reference.into()), create_if_nonexistent)
}
/// Gets the node id of a proto node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
pub fn existing_proto_node_id(&mut self, reference: ProtoNodeIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent)
}
/// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
// Start from the layer node or export
let output_layer = self.get_output_layer()?;
let existing_node_id = Self::locate_node_in_layer_chain(reference_name, output_layer, self.network_interface);
let existing_node_id = Self::locate_node_in_layer_chain(reference, output_layer, self.network_interface);
// Create a new node if the node does not exist and update its inputs
if create_if_nonexistent {
return existing_node_id.or_else(|| self.create_node(reference_name));
return existing_node_id.or_else(|| self.create_node(reference));
}
existing_node_id
@@ -265,16 +285,13 @@ impl<'a> ModifyInputsContext<'a> {
/// Gets the node id of a node with a specific reference (name) that is upstream (leftward) from the layer node, but before reaching another upstream layer stack.
/// For example, if given a parent layer, this would find a requested "Transform" or "Boolean Operation" node in its chain, between the parent layer and its layer stack child contents.
/// It would also travel up an entire layer that's not fed by a stack until reaching the generator node, such as a "Rectangle" or "Path" layer.
pub fn locate_node_in_layer_chain(reference_name: &str, left_of_layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
pub fn locate_node_in_layer_chain(reference: &DefinitionIdentifier, left_of_layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let upstream = network_interface.upstream_flow_back_from_nodes(vec![left_of_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow);
// Look at all of the upstream nodes
for upstream_node in upstream {
// Check if this is the node we have been searching for.
if network_interface
.reference(&upstream_node, &[])
.is_some_and(|node_reference| *node_reference == Some(reference_name.to_string()))
{
if network_interface.reference(&upstream_node, &[]).is_some_and(|node_reference| node_reference == *reference) {
if !network_interface.is_visible(&upstream_node, &[]) {
continue;
}
@@ -293,19 +310,19 @@ impl<'a> ModifyInputsContext<'a> {
}
/// Create a new node inside the layer
pub fn create_node(&mut self, reference: &str) -> Option<NodeId> {
pub fn create_node(&mut self, reference: &DefinitionIdentifier) -> Option<NodeId> {
let output_layer = self.get_output_layer()?;
let Some(node_definition) = resolve_document_node_type(reference) else {
log::error!("Node type {reference} does not exist in ModifyInputsContext::existing_node_id");
log::error!("Node {reference:?} does not exist in ModifyInputsContext::existing_node_id");
return None;
};
// If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`.
// TODO: Allow the 'Path' node to operate on table data by utilizing the reference (index or ID?) for each row.
if node_definition.identifier == "Path" {
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone();
if layer_input_type == concrete!(Table<Graphic>) {
let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else {
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]);
if layer_input_type.compiled_nested_type() == Some(&concrete!(Table<Graphic>)) {
let Some(flatten_path_definition) = resolve_proto_node_type(graphene_std::vector_nodes::flatten_path::IDENTIFIER) else {
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
return None;
};
@@ -325,7 +342,9 @@ impl<'a> ModifyInputsContext<'a> {
let backup_color_index = 2;
let backup_gradient_index = 3;
let Some(fill_node_id) = self.existing_node_id("Fill", true) else { return };
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
match &fill {
Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
@@ -345,32 +364,42 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(blend_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}
pub fn opacity_set(&mut self, opacity: f64) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(blend_node_id, 2);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
}
pub fn blending_fill_set(&mut self, fill: f64) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(blend_node_id, 3);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
}
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.unwrap_or(false);
let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return };
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
return;
};
let input_connector = InputConnector::node(clip_node_id, 4);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
}
pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
return;
};
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
@@ -388,7 +417,7 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::<Vec<f64>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
@@ -410,7 +439,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) {
// Get the existing upstream Transform node and its transform, if present, otherwise use the identity transform
let (layer_transform, transform_node_id) = self
.existing_node_id("Transform", false)
.existing_network_node_id("Transform", false)
.and_then(|transform_node_id| {
let document_node = self.network_interface.document_network().nodes.get(&transform_node_id)?;
Some((transform_utils::get_current_transform(&document_node.inputs), transform_node_id))
@@ -434,7 +463,7 @@ impl<'a> ModifyInputsContext<'a> {
/// A new Transform node is created if one does not exist, unless it would be given the identity transform.
pub fn transform_set(&mut self, transform: DAffine2, transform_in: TransformIn, skip_rerender: bool) {
// Get the existing upstream Transform node, if present
let transform_node_id = self.existing_node_id("Transform", false);
let transform_node_id = self.existing_network_node_id("Transform", false);
// Get a transform appropriate for the requested space
let to_transform = match transform_in {
@@ -459,7 +488,7 @@ impl<'a> ModifyInputsContext<'a> {
}
// Create the Transform node
self.existing_node_id("Transform", true)
self.existing_network_node_id("Transform", true)
}) else {
return;
};
@@ -475,19 +504,23 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn vector_modify(&mut self, modification_type: VectorModificationType) {
let Some(path_node_id) = self.existing_node_id("Path", true) else { return };
let Some(path_node_id) = self.existing_network_node_id("Path", true) else {
return;
};
self.network_interface.vector_modify(&path_node_id, modification_type);
self.responses.add(PropertiesPanelMessage::Refresh);
self.responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
let Some(brush_node_id) = self.existing_node_id("Brush", true) else { return };
let Some(brush_node_id) = self.existing_network_node_id("Brush", true) else {
return;
};
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
}
pub fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) {
let Some(artboard_node_id) = self.existing_node_id("Artboard", true) else {
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
return;
};
@@ -1,3 +1,4 @@
use crate::application::Editor;
use crate::consts::{
VIEWPORT_ROTATE_SNAP_INTERVAL, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MIN_FRACTION_COVER, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN,
VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
@@ -21,6 +22,7 @@ pub struct NavigationMessageContext<'a> {
pub document_ptz: &'a mut PTZ,
pub graph_view_overlay_open: bool,
pub preferences: &'a PreferencesMessageHandler,
pub viewport: &'a ViewportMessageHandler,
}
#[derive(Debug, Clone, PartialEq, Default, ExtractField)]
@@ -41,6 +43,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
document_ptz,
graph_view_overlay_open,
preferences,
viewport,
} = context;
fn get_ptz<'a>(document_ptz: &'a PTZ, network_interface: &'a NodeNetworkInterface, graph_view_overlay_open: bool, breadcrumb_network_path: &[NodeId]) -> Option<&'a PTZ> {
@@ -74,9 +77,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
NavigationMessage::BeginCanvasPan => {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Grabbing });
responses.add(FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
});
HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]).send_layout(responses);
self.mouse_position = ipp.mouse.position;
let Some(ptz) = get_ptz(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
@@ -93,12 +94,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
responses.add(NavigationMessage::BeginCanvasPan);
} else {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
responses.add(FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
]),
});
HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
])
.send_layout(responses);
self.navigation_operation = NavigationOperation::Tilt {
tilt_original_for_abort: ptz.tilt(),
@@ -116,12 +116,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
};
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::ZoomIn });
responses.add(FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Increments")]),
]),
});
HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Increments")]),
])
.send_layout(responses);
self.navigation_operation = NavigationOperation::Zoom {
zoom_raw_not_snapped: ptz.zoom(),
@@ -135,7 +134,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
log::error!("Could not get PTZ in CanvasPan");
return;
};
let document_to_viewport = self.calculate_offset_transform(ipp.viewport_bounds.center(), ptz);
let document_to_viewport = self.calculate_offset_transform(viewport.center_in_viewport_space().into_dvec2(), ptz);
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta);
ptz.pan += transformed_delta;
@@ -169,16 +168,20 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
log::error!("Could not get node graph PTZ in CanvasPanByViewportFraction");
return;
};
let document_to_viewport = self.calculate_offset_transform(ipp.viewport_bounds.center(), ptz);
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta * ipp.viewport_bounds.size());
let document_to_viewport = self.calculate_offset_transform(viewport.center_in_viewport_space().into_dvec2(), ptz);
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta * viewport.size().into_dvec2());
ptz.pan += transformed_delta;
responses.add(DocumentMessage::PTZUpdate);
}
NavigationMessage::CanvasPanMouseWheel { use_y_as_x } => {
let delta = if use_y_as_x { (-ipp.mouse.scroll_delta.y, 0.).into() } else { -ipp.mouse.scroll_delta.as_dvec2() } * VIEWPORT_SCROLL_RATE;
// On Mac, the OS already converts Shift+scroll into horizontal scrolling
let delta = if use_y_as_x && !Editor::environment().is_mac() {
(ipp.mouse.scroll_delta.y, 0.).into()
} else {
ipp.mouse.scroll_delta.as_dvec2()
} * -VIEWPORT_SCROLL_RATE;
responses.add(NavigationMessage::CanvasPan { delta });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
NavigationMessage::CanvasTiltResetAndZoomTo100Percent => {
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
@@ -193,7 +196,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
NavigationMessage::CanvasTiltSet { angle_radians } => {
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
@@ -214,7 +216,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < ptz.zoom()).unwrap_or(&ptz.zoom());
if center_on_mouse {
responses.add(self.center_zoom(ipp.viewport_bounds.size(), new_scale / ptz.zoom(), ipp.mouse.position));
responses.add(self.center_zoom(viewport.size().into(), new_scale / ptz.zoom(), ipp.mouse.position));
}
responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: new_scale });
}
@@ -225,7 +227,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > ptz.zoom()).unwrap_or(&ptz.zoom());
if center_on_mouse {
responses.add(self.center_zoom(ipp.viewport_bounds.size(), new_scale / ptz.zoom(), ipp.mouse.position));
responses.add(self.center_zoom(viewport.size().into(), new_scale / ptz.zoom(), ipp.mouse.position));
}
responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: new_scale });
}
@@ -245,9 +247,9 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
return;
};
zoom_factor *= Self::clamp_zoom(ptz.zoom() * zoom_factor, document_bounds, old_zoom, ipp);
zoom_factor *= Self::clamp_zoom(ptz.zoom() * zoom_factor, document_bounds, old_zoom, viewport);
responses.add(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position));
responses.add(self.center_zoom(viewport.size().into(), zoom_factor, ipp.mouse.position));
responses.add(NavigationMessage::CanvasZoomSet {
zoom_factor: ptz.zoom() * zoom_factor,
});
@@ -264,7 +266,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
return;
};
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);
let zoom = zoom * Self::clamp_zoom(zoom, document_bounds, old_zoom, viewport);
ptz.set_zoom(zoom);
if graph_view_overlay_open {
responses.add(NodeGraphMessage::UpdateGraphBarRight);
@@ -272,7 +274,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
NavigationMessage::CanvasFlip => {
if graph_view_overlay_open {
@@ -320,7 +321,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
} else {
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(NodeGraphMessage::SetGridAlignedEdges);
// Reset the navigation operation now that it's done
self.navigation_operation = NavigationOperation::None;
@@ -343,7 +343,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let (pos1, pos2) = (pos1.min(pos2), pos1.max(pos2));
let diagonal = pos2 - pos1;
if diagonal.length() < f64::EPSILON * 1000. || ipp.viewport_bounds.size() == DVec2::ZERO {
if diagonal.length() < f64::EPSILON * 1000. || viewport.size().into_dvec2() == DVec2::ZERO {
warn!("Cannot center since the viewport size is 0");
return;
}
@@ -352,10 +352,10 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
log::error!("Could not get node graph PTZ in CanvasPanByViewportFraction");
return;
};
let document_to_viewport = self.calculate_offset_transform(ipp.viewport_bounds.center(), ptz);
let document_to_viewport = self.calculate_offset_transform(viewport.center_in_viewport_space().into_dvec2(), ptz);
let v1 = document_to_viewport.inverse().transform_point2(DVec2::ZERO);
let v2 = document_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size());
let v2 = document_to_viewport.inverse().transform_point2(viewport.size().into_dvec2());
let center = ((v2 + v1) - (pos2 + pos1)) / 2.;
let size = (v2 - v1) / diagonal;
@@ -382,7 +382,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
responses.add(DocumentMessage::PTZUpdate);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
// Fully zooms in on the selected
NavigationMessage::FitViewportToSelection => {
@@ -397,7 +396,8 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
log::error!("Could not get node graph PTZ in FitViewportToSelection");
return;
};
let document_to_viewport = self.calculate_offset_transform(ipp.viewport_bounds.center(), ptz);
let document_to_viewport = self.calculate_offset_transform(viewport.center_in_viewport_space().into_dvec2(), ptz);
responses.add(NavigationMessage::FitViewportToBounds {
bounds: [document_to_viewport.inverse().transform_point2(bounds[0]), document_to_viewport.inverse().transform_point2(bounds[1])],
prevent_zoom_past_100: false,
@@ -419,7 +419,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
let tilt_raw_not_snapped = {
// Compute the angle in document space to counter for the canvas being flipped
let viewport_to_document = network_interface.document_metadata().document_to_viewport.inverse();
let half_viewport = ipp.viewport_bounds.size() / 2.;
let half_viewport = viewport.center_in_viewport_space().into_dvec2();
let start_offset = viewport_to_document.transform_vector2(self.mouse_position - half_viewport);
let end_offset = viewport_to_document.transform_vector2(ipp.mouse.position - half_viewport);
let angle = start_offset.angle_to(end_offset);
@@ -459,7 +459,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
network_interface.graph_bounds_viewport_space(breadcrumb_network_path)
};
updated_zoom * Self::clamp_zoom(updated_zoom, document_bounds, old_zoom, ipp)
updated_zoom * Self::clamp_zoom(updated_zoom, document_bounds, old_zoom, viewport)
};
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
log::error!("Could not get mutable PTZ in Zoom");
@@ -476,7 +476,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
};
responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: ptz.zoom() });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
}
@@ -563,9 +562,9 @@ impl NavigationMessageHandler {
NavigationMessage::CanvasPan { delta }.into()
}
pub fn clamp_zoom(zoom: f64, document_bounds: Option<[DVec2; 2]>, old_zoom: f64, ipp: &InputPreprocessorMessageHandler) -> f64 {
pub fn clamp_zoom(zoom: f64, document_bounds: Option<[DVec2; 2]>, old_zoom: f64, viewport: &ViewportMessageHandler) -> f64 {
let document_size = (document_bounds.map(|[min, max]| max - min).unwrap_or_default() / old_zoom) * zoom;
let scale_factor = (document_size / ipp.viewport_bounds.size()).max_element();
let scale_factor = (document_size / viewport.size().into_dvec2()).max_element();
if scale_factor <= f64::EPSILON * 100. || !scale_factor.is_finite() || scale_factor >= VIEWPORT_ZOOM_MIN_FRACTION_COVER {
return 1.;
File diff suppressed because it is too large Load Diff
@@ -1,94 +1,135 @@
use super::DocumentNodeDefinition;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, WidgetOverride};
use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::*;
use graphene_std::registry::*;
use graphene_std::*;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec<DocumentNodeDefinition> {
// Remove struct generics
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
let NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
} = node_template;
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
*name = Cow::Owned(new_name.to_string())
}
};
}
pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap<DefinitionIdentifier, DocumentNodeDefinition> {
// Create hashmap for the protonodes added by the macro.
let mut definitions_map = HashMap::new();
// First remove the custom protonodes and add them to the definitions map since they contain different metadata
// from the macro and must be inserted first so that network nodes which reference them use the correct metadata.
let network_nodes = custom
.into_iter()
.filter_map(|definition| {
if let DocumentNodeImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.document_node.implementation {
definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition);
return None;
};
Some(definition)
})
.collect::<Vec<_>>();
// Add the rest of the protonodes from the macro
let node_registry = NODE_REGISTRY.lock().unwrap();
'outer: for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
for node in custom.iter() {
let DocumentNodeDefinition {
node_template: NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
},
..
} = node;
match implementation {
DocumentNodeImplementation::ProtoNode(name) if name == id => continue 'outer,
_ => (),
}
}
for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
let identifier = DefinitionIdentifier::ProtoNode(id.clone());
if definitions_map.contains_key(&identifier) {
continue;
};
let NodeMetadata {
display_name,
category,
fields,
description,
properties,
context_features,
} = metadata;
let Some(implementations) = &node_registry.get(id) else { continue };
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let input_type = if valid_inputs.len() > 1 { &const { generic!(D) } } else { &first_node_io.call_argument };
let output_type = &first_node_io.return_value;
let inputs = preprocessor::node_inputs(fields, first_node_io);
let node = DocumentNodeDefinition {
identifier: display_name,
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
call_argument: (input_type.clone()),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
skip_deduplication: false,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
// TODO: Store information for input overrides in the node macro
input_metadata: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
definitions_map.insert(
identifier,
DocumentNodeDefinition {
identifier: display_name,
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
call_argument: input_type.clone(),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
skip_deduplication: false,
context_features: ContextDependencies::from(context_features.as_slice()),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
// TODO: Store information for input overrides in the node macro
input_metadata: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
})
.collect(),
locked: false,
..Default::default()
},
},
category,
description: Cow::Borrowed(description),
properties: *properties,
},
category: category.unwrap_or("UNCATEGORIZED"),
description: Cow::Borrowed(description),
properties: *properties,
};
custom.push(node);
);
}
custom
// If any protonode does not have metadata then set its display name to its identifier string
for definition in definitions_map.values_mut() {
let metadata = NODE_METADATA.lock().unwrap();
if let DocumentNodeImplementation::ProtoNode(id) = &definition.node_template.document_node.implementation
&& !metadata.contains_key(id)
{
definition.node_template.persistent_node_metadata.display_name = definition.identifier.to_string();
}
}
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes
for mut network_node in network_nodes {
traverse_node(&network_node.node_template.document_node, &mut network_node.node_template.persistent_node_metadata, &definitions_map);
// Set the reference to the node identifier
if let Some(nested_metadata) = network_node.node_template.persistent_node_metadata.network_metadata.as_mut() {
nested_metadata.persistent_metadata.reference = Some(network_node.identifier.to_string());
// If it is not a merge node, then set the display name to the identifier/reference
if network_node.identifier != "Merge" {
network_node.node_template.persistent_node_metadata.display_name = network_node.identifier.to_string();
}
}
definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node);
}
definitions_map
}
/// Traverses a document node template and metadata in parallel to add metadata to the protonodes
fn traverse_node(node: &DocumentNode, node_metadata: &mut DocumentNodePersistentMetadata, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
match &node.implementation {
DocumentNodeImplementation::Network(node_network) => {
for (nested_node_id, nested_node) in node_network.nodes.iter() {
let nested_metadata = node_metadata.network_metadata.as_mut().unwrap().persistent_metadata.node_metadata.get_mut(nested_node_id).unwrap();
traverse_node(nested_node, &mut nested_metadata.persistent_metadata, definitions_map);
}
}
DocumentNodeImplementation::ProtoNode(id) => {
// Set all the metadata except the position to the proto node information from the macro
// TODO: Use options in the template to specify what you want to default and what you want to override
// If this fails then the proto node id in the definition doesn't match what is generated by the macro
let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else {
// log::error!("Could not get definition for id {} when filling in protonode metadata for a custom node", id.clone());
return;
};
let mut new_metadata = definition.node_template.persistent_node_metadata.clone();
new_metadata.node_type_metadata = node_metadata.node_type_metadata.clone();
*node_metadata = new_metadata
}
DocumentNodeImplementation::Extract => {}
}
}
@@ -1,5 +1,6 @@
use super::utility_types::Direction;
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::*;
@@ -18,21 +19,25 @@ pub enum NodeGraphMessage {
},
AddPathNode,
AddImport,
AddPrimaryImport,
AddSecondaryImport,
AddExport,
AddPrimaryExport,
AddSecondaryExport,
Init,
SelectedNodesUpdated,
Copy,
CreateNodeInLayerNoTransaction {
node_type: String,
node_type: DefinitionIdentifier,
layer: LayerNodeIdentifier,
},
CreateNodeInLayerWithTransaction {
node_type: String,
node_type: DefinitionIdentifier,
layer: LayerNodeIdentifier,
},
CreateNodeFromContextMenu {
node_id: Option<NodeId>,
node_type: String,
node_type: DefinitionIdentifier,
xy: Option<(i32, i32)>,
add_transaction: bool,
},
@@ -63,9 +68,16 @@ pub enum NodeGraphMessage {
set_to_exposed: bool,
start_transaction: bool,
},
ExposeEncapsulatingPrimaryInput {
exposed: bool,
},
ExposePrimaryExport {
exposed: bool,
},
InsertNode {
node_id: NodeId,
node_template: NodeTemplate,
// Boxed to reduce size of enum (1120 bytes to 8 bytes)
node_template: Box<NodeTemplate>,
},
InsertNodeBetween {
node_id: NodeId,
@@ -102,6 +114,7 @@ pub enum NodeGraphMessage {
shift: Key,
},
ShakeNode,
UpdateNodeGraphWidth,
RemoveImport {
import_index: usize,
},
@@ -133,7 +146,6 @@ pub enum NodeGraphMessage {
SendWires,
UpdateVisibleNodes,
SendGraph,
SetGridAlignedEdges,
SetInputValue {
node_id: NodeId,
input_index: usize,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,7 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource};
use glam::{DVec2, IVec2};
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_std::Type;
use std::borrow::Cow;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum FrontendGraphDataType {
@@ -16,6 +15,7 @@ pub enum FrontendGraphDataType {
Color,
Gradient,
Typography,
Invalid,
}
impl FrontendGraphDataType {
@@ -23,6 +23,7 @@ impl FrontendGraphDataType {
match TaggedValue::from_type_or_none(input) {
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F32(_)
| TaggedValue::F64(_)
| TaggedValue::DVec2(_)
| TaggedValue::F64Array4(_)
@@ -35,17 +36,10 @@ impl FrontendGraphDataType {
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
TaggedValue::String(_) | TaggedValue::VecString(_) => Self::Typography,
_ => Self::General,
}
}
pub fn displayed_type(input: &Type, type_source: &TypeSource) -> Self {
match type_source {
TypeSource::Error(_) | TypeSource::RandomProtonodeImplementation => Self::General,
_ => Self::from_type(input),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -59,7 +53,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "validTypes")]
pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")]
pub connected_to: Option<OutputConnector>,
/// Either "nothing", "import #{index}", or "{node name} #{output_index}".
pub connected_to: String,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -67,11 +62,13 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
pub resolved_type: String,
pub description: String,
/// If connected to an export, it is "export index {index}".
/// If connected to a node, it is "{node name} input {input_index}".
#[serde(rename = "connectedTo")]
pub connected_to: Vec<InputConnector>,
pub connected_to: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -84,6 +81,8 @@ pub struct FrontendNode {
pub reference: Option<String>,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "implementationName")]
pub implementation_name: String,
#[serde(rename = "primaryInput")]
pub primary_input: Option<FrontendGraphInput>,
#[serde(rename = "exposedInputs")]
@@ -92,40 +91,25 @@ pub struct FrontendNode {
pub primary_output: Option<FrontendGraphOutput>,
#[serde(rename = "exposedOutputs")]
pub exposed_outputs: Vec<FrontendGraphOutput>,
pub position: (i32, i32),
#[serde(rename = "primaryInputConnectedToLayer")]
pub primary_input_connected_to_layer: bool,
#[serde(rename = "primaryOutputConnectedToLayer")]
pub primary_output_connected_to_layer: bool,
pub position: IVec2,
pub previewed: bool,
pub visible: bool,
pub locked: bool,
pub previewed: bool,
pub errors: Option<String>,
#[serde(rename = "uiOnly")]
pub ui_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendNodeType {
pub name: Cow<'static, str>,
pub category: Cow<'static, str>,
pub identifier: String,
pub name: String,
pub category: String,
#[serde(rename = "inputTypes")]
pub input_types: Option<Vec<Cow<'static, str>>>,
pub input_types: Vec<String>,
}
impl FrontendNodeType {
pub fn new(name: impl Into<Cow<'static, str>>, category: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
category: category.into(),
input_types: None,
}
}
pub fn with_input_types(name: impl Into<Cow<'static, str>>, category: impl Into<Cow<'static, str>>, input_types: Vec<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
category: category.into(),
input_types: Some(input_types),
}
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct DragStart {
pub start_x: f64,
@@ -154,16 +138,18 @@ pub struct BoxSelection {
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(tag = "type", content = "data")]
pub enum ContextMenuData {
ToggleLayer {
ModifyNode {
#[serde(rename = "nodeId")]
node_id: NodeId,
#[serde(rename = "canBeLayer")]
can_be_layer: bool,
#[serde(rename = "currentlyIsNode")]
currently_is_node: bool,
},
CreateNode {
#[serde(rename = "compatibleType")]
#[serde(default)]
compatible_type: Option<String>,
},
}
@@ -172,11 +158,17 @@ pub enum ContextMenuData {
pub struct ContextMenuInformation {
// Stores whether the context menu is open and its position in graph coordinates
#[serde(rename = "contextMenuCoordinates")]
pub context_menu_coordinates: (i32, i32),
pub context_menu_coordinates: FrontendXY,
#[serde(rename = "contextMenuData")]
pub context_menu_data: ContextMenuData,
}
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct NodeGraphErrorDiagnostic {
pub position: FrontendXY,
pub error: String,
}
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendClickTargets {
#[serde(rename = "nodeClickTargets")]
@@ -189,8 +181,6 @@ pub struct FrontendClickTargets {
pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")]
pub all_nodes_bounding_box: String,
#[serde(rename = "importExportsBoundingBox")]
pub import_exports_bounding_box: String,
#[serde(rename = "modifyImportExport")]
pub modify_import_export: Vec<String>,
}
@@ -202,3 +192,22 @@ pub enum Direction {
Left,
Right,
}
/// Stores node graph coordinates which are then transformed in Svelte based on the node graph transform
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendXY {
pub x: i32,
pub y: i32,
}
impl From<DVec2> for FrontendXY {
fn from(v: DVec2) -> Self {
FrontendXY { x: v.x as i32, y: v.y as i32 }
}
}
impl From<IVec2> for FrontendXY {
fn from(v: IVec2) -> Self {
FrontendXY { x: v.x, y: v.y }
}
}
@@ -13,9 +13,11 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else {
return;
};
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]);
for primary in 0..2 {
let secondary = 1 - primary;
@@ -52,9 +54,11 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else {
return;
};
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]);
let min = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
let max = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
@@ -85,9 +89,11 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]);
let tan_a = angle_a.to_radians().tan();
let tan_b = angle_b.to_radians().tan();
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
@@ -128,9 +134,11 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]);
let tan_a = angle_a.to_radians().tan();
let tan_b = angle_b.to_radians().tan();
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
@@ -205,10 +213,10 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
}
let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| {
update_val::<NumberInput, _>(grid, move |grid, val| {
if let Some(val) = val.value {
if let Some(update) = update(grid) {
*update = val;
}
if let Some(val) = val.value
&& let Some(update) = update(grid)
{
*update = val;
}
})
};
@@ -228,13 +236,13 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
};
widgets.push(LayoutGroup::Row {
widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()],
widgets: vec![TextLabel::new("Grid").bold(true).widget_instance()],
});
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Type").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Type").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(vec![
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type {
@@ -260,98 +268,101 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
GridType::Rectangular { .. } => 0,
GridType::Isometric { .. } => 1,
}))
.widget_holder(),
.widget_instance(),
],
});
let mut color_widgets = vec![TextLabel::new("Display").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
let mut color_widgets = vec![
TextLabel::new("Display").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
];
color_widgets.extend([
CheckboxInput::new(grid.dot_display)
.icon("GridDotted")
.tooltip("Display as dotted grid")
.tooltip_label("Display as Dotted Grid")
.on_update(update_display(grid, |grid| Some(&mut grid.dot_display)))
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
]);
color_widgets.push(
ColorInput::new(FillChoice::Solid(grid.grid_color.to_gamma_srgb()))
.tooltip("Grid display color")
.tooltip_label("Grid Display Color")
.allow_none(false)
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))
.widget_holder(),
.widget_instance(),
);
widgets.push(LayoutGroup::Row { widgets: color_widgets });
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Origin").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Origin").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(grid.origin.x))
.label("X")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.x)))
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(grid.origin.y))
.label("Y")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.y)))
.widget_holder(),
.widget_instance(),
],
});
match grid.grid_type {
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Spacing").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Spacing").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(spacing.x))
.label("X")
.unit(" px")
.min(0.)
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x)))
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(spacing.y))
.label("Y")
.unit(" px")
.min(0.)
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y)))
.widget_holder(),
.widget_instance(),
],
}),
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Y Spacing").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Y Spacing").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(y_axis_spacing))
.unit(" px")
.min(0.)
.min_width(200)
.on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing()))
.widget_holder(),
.widget_instance(),
],
});
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Angles").table_align(true).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Angles").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(angle_a))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.angle_a()))
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(angle_b))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.angle_b()))
.widget_holder(),
.widget_instance(),
],
});
}
@@ -2,8 +2,17 @@ pub mod grid_overlays;
mod overlays_message;
mod overlays_message_handler;
pub mod utility_functions;
#[cfg_attr(not(target_family = "wasm"), path = "utility_types_vello.rs")]
pub mod utility_types;
// Native (nonwasm)
#[cfg(not(target_family = "wasm"))]
pub mod utility_types_native;
#[cfg(not(target_family = "wasm"))]
pub use utility_types_native as utility_types;
// WebAssembly
#[cfg(target_family = "wasm")]
pub mod utility_types_web;
#[cfg(target_family = "wasm")]
pub use utility_types_web as utility_types;
#[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
@@ -4,8 +4,7 @@ use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct OverlaysMessageContext<'a> {
pub visibility_settings: OverlaysVisibilitySettings,
pub ipp: &'a InputPreprocessorMessageHandler,
pub device_pixel_ratio: f64,
pub viewport: &'a ViewportMessageHandler,
}
#[derive(Debug, Clone, Default, ExtractField)]
@@ -20,19 +19,14 @@ pub struct OverlaysMessageHandler {
#[message_handler_data]
impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMessageHandler {
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, context: OverlaysMessageContext) {
let OverlaysMessageContext {
visibility_settings,
ipp,
device_pixel_ratio,
..
} = context;
let OverlaysMessageContext { visibility_settings, viewport, .. } = context;
match message {
#[cfg(target_family = "wasm")]
OverlaysMessage::Draw => {
use super::utility_functions::overlay_canvas_element;
use super::utility_types::OverlayContext;
use glam::{DAffine2, DVec2};
use crate::messages::viewport::{Position, ToPhysical};
use wasm_bindgen::JsCast;
let canvas = match &self.canvas {
@@ -48,28 +42,26 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
canvas_context.dyn_into().expect("Context should be a canvas 2d context")
});
let size = ipp.viewport_bounds.size().as_uvec2();
let size_logical = viewport.size();
let size_physical = size_logical.to_physical();
let width = size_logical.x().max(size_physical.x());
let height = size_logical.y().max(size_physical.y());
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array();
let _ = canvas_context.set_transform(a, b, c, d, e, f);
canvas_context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
let _ = canvas_context.reset_transform();
canvas_context.clear_rect(0., 0., width, height);
if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays {
context: OverlayContext {
render_context: canvas_context.clone(),
size: size.as_dvec2(),
device_pixel_ratio,
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
},
});
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: canvas_context.clone(),
size: size.as_dvec2(),
device_pixel_ratio,
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
}));
}
}
@@ -78,9 +70,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
OverlaysMessage::Draw => {
use super::utility_types::OverlayContext;
let size = ipp.viewport_bounds.size();
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
let overlay_context = OverlayContext::new(*viewport, visibility_settings);
if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
@@ -93,7 +83,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
}
#[cfg(all(not(target_family = "wasm"), test))]
OverlaysMessage::Draw => {
let _ = (responses, visibility_settings, ipp, device_pixel_ratio);
let _ = (responses, visibility_settings, viewport);
}
OverlaysMessage::AddProvider { provider: message } => {
self.overlay_providers.insert(message);
@@ -1,12 +1,16 @@
use super::utility_types::{DrawHandles, OverlayContext};
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::text::{Font, FontCache, TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId};
use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use wasm_bindgen::JsCast;
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
@@ -24,33 +28,40 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
create_context().expect("Failed to get canvas context")
}
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> Vec<SegmentId> {
let selected_points = shape_editor.selected_points();
let selected_anchors = selected_points
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> HashMap<LayerNodeIdentifier, Vec<SegmentId>> {
let mut map = HashMap::new();
for (layer, state) in &shape_editor.selected_shape_state {
let Some(vector) = network_interface.compute_modified_vector(*layer) else { continue };
let selected_segments = selected_segments_for_layer(&vector, state);
map.insert(*layer, selected_segments);
}
map
}
pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState) -> Vec<SegmentId> {
let selected_anchors = state
.selected_points()
.filter_map(|point| if let ManipulatorPointId::Anchor(p) = point { Some(p) } else { None })
.collect::<Vec<_>>();
// Collect the segments whose handles are selected
let mut selected_segments = shape_editor
let mut selected_segments = state
.selected_points()
.filter_map(|point_id| match point_id {
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(*segment_id),
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(segment_id),
ManipulatorPointId::Anchor(_) => None,
})
.collect::<Vec<_>>();
// TODO: Currently if there are two duplicate layers, both of their segments get overlays
// Adding segments which are are connected to selected anchors
for layer in network_interface.selected_nodes().selected_layers(network_interface.document_metadata()) {
let Some(vector) = network_interface.compute_modified_vector(layer) else { continue };
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
selected_segments.push(segment_id);
}
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
selected_segments.push(segment_id);
}
}
selected_segments
}
@@ -124,22 +135,18 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
overlay_context.outline_vector(&vector, transform);
}
let selected_shape_state = shape_editor.selected_shape_state.entry(layer).or_default();
// Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector.segment_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue;
};
if selected_shape_state.is_segment_selected(segment_id) {
overlay_context.outline_select_bezier(bezier, transform);
}
}
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
let is_selected = |point: ManipulatorPointId| selected_shape_state.is_point_selected(point);
if display_handles {
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector.adjacent_segment(point_id)).collect();
let opposite_handles_data = selected_shape_state.selected_points().filter_map(|point_id| vector.adjacent_segment(&point_id)).collect::<Vec<_>>();
match draw_handles {
DrawHandles::All => {
@@ -148,9 +155,11 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
});
}
DrawHandles::SelectedAnchors(ref selected_segments) => {
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
vector
.segment_bezier_iter()
.filter(|(segment_id, ..)| selected_segments.contains(segment_id))
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
.for_each(|(segment_id, bezier, _start, _end)| {
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
});
@@ -161,7 +170,9 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
}
}
}
DrawHandles::FrontierHandles(ref segment_endpoints) => {
DrawHandles::FrontierHandles(ref segment_endpoints_by_layer) => {
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
vector
.segment_bezier_iter()
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
@@ -186,7 +197,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
}
}
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext, preferences: &PreferencesMessageHandler) {
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
if !overlay_context.visibility_settings.anchors() {
return;
}
@@ -195,15 +206,46 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
let Some(vector) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
//let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
for point in vector.extendable_points(preferences.vector_meshes) {
for point in vector.anchor_endpoints() {
let Some(position) = vector.point_domain.position_from_id(point) else { continue };
let position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
}
}
}
// Global lazy initialized font cache and text context
pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
let mut font_cache = FontCache::default();
// Initialize with the hardcoded font used by overlay text
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
font_cache.insert(font, FONT_DATA.to_vec());
font_cache
});
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
pub fn text_width(text: &str, font_size: f64) -> f64 {
let typesetting = TypesettingConfig {
font_size,
line_height_ratio: 1.2,
character_spacing: 0.0,
max_width: None,
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
};
// Load Source Sans Pro font data
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
bounds.x
}
@@ -1,9 +1,12 @@
use crate::consts::{
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::Message;
use crate::messages::prelude::ViewportMessageHandler;
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
@@ -11,7 +14,7 @@ use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector};
@@ -22,12 +25,15 @@ use std::sync::{Arc, Mutex, MutexGuard};
use vello::Scene;
use vello::peniko;
// TODO Remove duplicated definition of this in `utility_types_web.rs`
pub type OverlayProvider = fn(OverlayContext) -> Message;
// TODO Remove duplicated definition of this in `utility_types_web.rs`
pub fn empty_provider() -> OverlayProvider {
|_| Message::NoOp
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum OverlaysType {
@@ -38,6 +44,7 @@ pub enum OverlaysType {
TransformCage,
HoverOutline,
SelectionOutline,
LayerOriginCross,
Pivot,
Origin,
Path,
@@ -45,6 +52,7 @@ pub enum OverlaysType {
Handles,
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct OverlaysVisibilitySettings {
@@ -56,6 +64,7 @@ pub struct OverlaysVisibilitySettings {
pub transform_cage: bool,
pub hover_outline: bool,
pub selection_outline: bool,
pub layer_origin_cross: bool,
pub pivot: bool,
pub origin: bool,
pub path: bool,
@@ -63,6 +72,7 @@ pub struct OverlaysVisibilitySettings {
pub handles: bool,
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
impl Default for OverlaysVisibilitySettings {
fn default() -> Self {
Self {
@@ -74,6 +84,7 @@ impl Default for OverlaysVisibilitySettings {
transform_cage: true,
hover_outline: true,
selection_outline: true,
layer_origin_cross: true,
pivot: true,
origin: true,
path: true,
@@ -83,6 +94,7 @@ impl Default for OverlaysVisibilitySettings {
}
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
impl OverlaysVisibilitySettings {
pub fn all(&self) -> bool {
self.all
@@ -116,6 +128,10 @@ impl OverlaysVisibilitySettings {
self.all && self.selection_outline
}
pub fn layer_origin_cross(&self) -> bool {
self.all && self.layer_origin_cross
}
pub fn pivot(&self) -> bool {
self.all && self.pivot
}
@@ -143,24 +159,18 @@ pub struct OverlayContext {
#[serde(skip)]
#[specta(skip)]
internal: Arc<Mutex<OverlayContextInternal>>,
pub size: DVec2,
// The device pixel ratio is a property provided by the browser window and is the CSS pixel size divided by the physical monitor's pixel size.
// It allows better pixel density of visualizations on high-DPI displays where the OS display scaling is not 100%, or where the browser is zoomed.
pub device_pixel_ratio: f64,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,
}
impl Clone for OverlayContext {
fn clone(&self) -> Self {
let internal = self.internal.lock().expect("Failed to lock internal overlay context");
let size = internal.size;
let device_pixel_ratio = internal.device_pixel_ratio;
let visibility_settings = internal.visibility_settings;
drop(internal); // Explicitly release the lock before cloning the Arc<Mutex<_>>
Self {
internal: self.internal.clone(),
size,
device_pixel_ratio,
viewport: self.viewport,
visibility_settings,
}
}
@@ -169,7 +179,7 @@ impl Clone for OverlayContext {
// Manual implementations since Scene doesn't implement PartialEq or Debug
impl PartialEq for OverlayContext {
fn eq(&self, other: &Self) -> bool {
self.size == other.size && self.device_pixel_ratio == other.device_pixel_ratio && self.visibility_settings == other.visibility_settings
self.viewport == other.viewport && self.visibility_settings == other.visibility_settings
}
}
@@ -177,8 +187,7 @@ impl std::fmt::Debug for OverlayContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OverlayContext")
.field("scene", &"Scene { ... }")
.field("size", &self.size)
.field("device_pixel_ratio", &self.device_pixel_ratio)
.field("viewport", &self.viewport)
.field("visibility_settings", &self.visibility_settings)
.finish()
}
@@ -189,8 +198,7 @@ impl Default for OverlayContext {
fn default() -> Self {
Self {
internal: Mutex::new(OverlayContextInternal::default()).into(),
size: DVec2::ZERO,
device_pixel_ratio: 1.0,
viewport: ViewportMessageHandler::default(),
visibility_settings: OverlaysVisibilitySettings::default(),
}
}
@@ -203,18 +211,17 @@ impl core::hash::Hash for OverlayContext {
impl OverlayContext {
#[allow(dead_code)]
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
pub(super) fn new(viewport: ViewportMessageHandler, visibility_settings: OverlaysVisibilitySettings) -> Self {
Self {
internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))),
size,
device_pixel_ratio,
internal: Arc::new(Mutex::new(OverlayContextInternal::new(viewport, visibility_settings))),
viewport,
visibility_settings,
}
}
pub fn take_scene(self) -> Scene {
let mut internal = self.internal.lock().expect("Failed to lock internal overlay context");
std::mem::take(&mut *internal).scene
std::mem::take(&mut internal.scene)
}
fn internal(&'_ self) -> MutexGuard<'_, OverlayContextInternal> {
@@ -266,6 +273,14 @@ impl OverlayContext {
self.internal().manipulator_anchor(position, selected, color);
}
pub fn resize_handle(&mut self, position: DVec2, rotation: f64) {
self.internal().resize_handle(position, rotation);
}
pub fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) {
self.internal().skew_handles(edge_start, edge_end);
}
pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
self.internal().square(position, size, color_fill, color_stroke);
}
@@ -278,6 +293,7 @@ impl OverlayContext {
self.internal().circle(position, radius, color_fill, color_stroke);
}
#[allow(clippy::too_many_arguments)]
pub fn dashed_ellipse(
&mut self,
center: DVec2,
@@ -378,10 +394,6 @@ impl OverlayContext {
self.internal().fill_path_pattern(subpaths, transform, color);
}
pub fn get_width(&self, text: &str) -> f64 {
self.internal().get_width(text)
}
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
let mut internal = self.internal();
internal.text(text, font_color, background_color, transform, padding, pivot);
@@ -392,43 +404,38 @@ impl OverlayContext {
}
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
pub enum Pivot {
Start,
Middle,
End,
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
pub enum DrawHandles {
All,
SelectedAnchors(Vec<SegmentId>),
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
None,
}
pub(super) struct OverlayContextInternal {
scene: Scene,
size: DVec2,
device_pixel_ratio: f64,
viewport: ViewportMessageHandler,
visibility_settings: OverlaysVisibilitySettings,
}
impl Default for OverlayContextInternal {
fn default() -> Self {
Self {
scene: Scene::new(),
size: DVec2::ZERO,
device_pixel_ratio: 1.0,
visibility_settings: OverlaysVisibilitySettings::default(),
}
Self::new(ViewportMessageHandler::default(), OverlaysVisibilitySettings::default())
}
}
impl OverlayContextInternal {
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
pub(super) fn new(viewport: ViewportMessageHandler, visibility_settings: OverlaysVisibilitySettings) -> Self {
Self {
scene: Scene::new(),
size,
device_pixel_ratio,
viewport,
visibility_settings,
}
}
@@ -464,7 +471,7 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color_stroke), None, &path);
}
fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
@@ -484,11 +491,13 @@ impl OverlayContextInternal {
let mut path = BezPath::new();
if let Some(first) = polygon.last() {
path.move_to(kurbo::Point::new(first.x.round() - 0.5, first.y.round() - 0.5));
let p = self.snap_to_physical_pixel_center(*first);
path.move_to(kurbo::Point::new(p.x, p.y));
}
for point in polygon {
path.line_to(kurbo::Point::new(point.x.round() - 0.5, point.y.round() - 0.5));
let p = self.snap_to_physical_pixel_center(*point);
path.line_to(kurbo::Point::new(p.x, p.y));
}
path.close_path();
@@ -497,7 +506,7 @@ impl OverlayContextInternal {
}
let stroke_color = stroke_color.unwrap_or(COLOR_OVERLAY_BLUE);
let mut stroke = kurbo::Stroke::new(1.0);
let mut stroke = kurbo::Stroke::new(1.);
if let Some(dash_width) = dash_width {
let dash_gap = dash_gap_width.unwrap_or(1.);
@@ -515,8 +524,8 @@ impl OverlayContextInternal {
fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
let transform = self.get_transform();
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
let start = self.snap_to_physical_pixel_center(start);
let end = self.snap_to_physical_pixel_center(end);
let mut path = BezPath::new();
path.move_to(kurbo::Point::new(start.x, start.y));
@@ -534,7 +543,7 @@ impl OverlayContextInternal {
fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
let transform = self.get_transform();
let position = position.round() - DVec2::splat(0.5);
let position = self.snap_to_physical_pixel_center(position);
let circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
@@ -542,25 +551,24 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
self.scene
.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
}
fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
let transform = self.get_transform();
let position = position.round() - DVec2::splat(0.5);
let position = self.snap_to_physical_pixel_center(position);
let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2.);
let fill = COLOR_OVERLAY_BLUE_50;
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle);
let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle);
}
fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
@@ -575,8 +583,22 @@ impl OverlayContextInternal {
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
}
fn resize_handle(&mut self, position: DVec2, rotation: f64) {
let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);
self.quad(quad, None, Some(COLOR_OVERLAY_WHITE));
}
fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) {
let edge_dir = (edge_end - edge_start).normalize();
let mid = edge_end.midpoint(edge_start);
for edge in [edge_dir, -edge_dir] {
self.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET), edge, SKEW_TRIANGLE_SIZE, None, None);
}
}
fn get_transform(&self) -> kurbo::Affine {
kurbo::Affine::scale(self.device_pixel_ratio)
kurbo::Affine::scale(self.viewport.scale())
}
fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
@@ -584,7 +606,7 @@ impl OverlayContextInternal {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round() - DVec2::splat(0.5);
let position = self.snap_to_physical_pixel_center(position);
let corner = position - DVec2::splat(size) / 2.;
let transform = self.get_transform();
@@ -592,14 +614,14 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &rect);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color_stroke), None, &rect);
}
fn pixel(&mut self, position: DVec2, color: Option<&str>) {
let size = 1.;
let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE);
let position = position.round() - DVec2::splat(0.5);
let position = self.snap_to_physical_pixel_center(position);
let corner = position - DVec2::splat(size) / 2.;
let transform = self.get_transform();
@@ -611,16 +633,17 @@ impl OverlayContextInternal {
fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
let position = self.snap_to_physical_pixel(position);
let transform = self.get_transform();
let circle = kurbo::Circle::new((position.x, position.y), radius);
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color_stroke), None, &circle);
}
#[allow(clippy::too_many_arguments)]
fn dashed_ellipse(
&mut self,
_center: DVec2,
@@ -669,7 +692,7 @@ impl OverlayContextInternal {
);
}
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
}
fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
@@ -686,12 +709,12 @@ impl OverlayContextInternal {
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
}
fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
let sign = scale.signum();
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb();
fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str());
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None);
self.line(start + DVec2::X * radius * sign, start + DVec2::X * radius * scale.abs(), None, None);
self.circle(start, radius, fill_color, None);
self.circle(start, radius * scale.abs(), fill_color, None);
self.text(
@@ -705,28 +728,28 @@ impl OverlayContextInternal {
}
fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option<bool>) {
const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.;
const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.;
const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.;
const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
let hover_ring_outer_radius: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.;
let main_ring_outer_radius: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
let main_ring_inner_radius: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.;
let arrow_radius: f64 = COMPASS_ROSE_ARROW_SIZE / 2.;
let hover_ring_stroke_width: f64 = hover_ring_outer_radius - main_ring_inner_radius;
let hover_ring_centerline_radius: f64 = (hover_ring_outer_radius + main_ring_inner_radius) / 2.;
let main_ring_stroke_width: f64 = main_ring_outer_radius - main_ring_inner_radius;
let main_ring_centerline_radius: f64 = (main_ring_outer_radius + main_ring_inner_radius) / 2.;
let Some(show_hover_ring) = show_compass_with_hover_ring else { return };
let transform = self.get_transform();
let center = compass_center.round() - DVec2::splat(0.5);
let center = self.snap_to_physical_pixel_center(compass_center);
// Hover ring
if show_hover_ring {
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).to_rgba_hex_srgb();
fill_color.insert(0, '#');
let circle = kurbo::Circle::new((center.x, center.y), HOVER_RING_CENTERLINE_RADIUS);
let circle = kurbo::Circle::new((center.x, center.y), hover_ring_centerline_radius);
self.scene
.stroke(&kurbo::Stroke::new(HOVER_RING_STROKE_WIDTH), transform, Self::parse_color(&fill_color), None, &circle);
.stroke(&kurbo::Stroke::new(hover_ring_stroke_width), transform, Self::parse_color(&fill_color), None, &circle);
}
// Arrows
@@ -734,11 +757,11 @@ impl OverlayContextInternal {
let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle);
let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN };
let tip = center + direction * HOVER_RING_OUTER_RADIUS;
let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.;
let tip = center + direction * hover_ring_outer_radius;
let base = center + direction * (main_ring_inner_radius + main_ring_outer_radius) / 2.;
let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt();
let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r);
let r = (arrow_radius.powi(2) + main_ring_inner_radius.powi(2)).sqrt();
let (cos, sin) = (main_ring_inner_radius / r, arrow_radius / r);
let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos);
let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos);
@@ -755,14 +778,14 @@ impl OverlayContextInternal {
}
// Main ring
let circle = kurbo::Circle::new((center.x, center.y), MAIN_RING_CENTERLINE_RADIUS);
let circle = kurbo::Circle::new((center.x, center.y), main_ring_centerline_radius);
self.scene
.stroke(&kurbo::Stroke::new(MAIN_RING_STROKE_WIDTH), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle);
.stroke(&kurbo::Stroke::new(main_ring_stroke_width), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle);
}
fn pivot(&mut self, position: DVec2, angle: f64) {
let uv = DVec2::from_angle(angle);
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
let (x, y) = self.snap_to_physical_pixel_center(position).into();
let transform = self.get_transform();
@@ -771,60 +794,58 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &circle);
// Crosshair
const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
let crosshair_radius: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
let mut stroke = kurbo::Stroke::new(PIVOT_CROSSHAIR_THICKNESS);
stroke = stroke.with_caps(kurbo::Cap::Round);
// Horizontal line
let mut path = BezPath::new();
path.move_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y));
path.line_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y));
path.move_to(kurbo::Point::new(x + crosshair_radius * uv.x, y + crosshair_radius * uv.y));
path.line_to(kurbo::Point::new(x - crosshair_radius * uv.x, y - crosshair_radius * uv.y));
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
// Vertical line
let mut path = BezPath::new();
path.move_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x));
path.line_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x));
path.move_to(kurbo::Point::new(x - crosshair_radius * uv.y, y + crosshair_radius * uv.x));
path.line_to(kurbo::Point::new(x + crosshair_radius * uv.y, y - crosshair_radius * uv.x));
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
}
fn dowel_pin(&mut self, position: DVec2, angle: f64, color: Option<&str>) {
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
let (x, y) = self.snap_to_physical_pixel_center(position).into();
let color = color.unwrap_or(COLOR_OVERLAY_YELLOW_DULL);
let transform = self.get_transform();
// Draw the background circle with a white fill and colored outline
let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS);
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color), None, &circle);
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color), None, &circle);
// Draw the two filled sectors using paths
let mut path = BezPath::new();
// Top-left sector
let start1 = FRAC_PI_2 + angle;
let start1_x = x + DOWEL_PIN_RADIUS * start1.cos();
let start1_y = y + DOWEL_PIN_RADIUS * start1.sin();
path.move_to(kurbo::Point::new(x, y));
let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).cos();
let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).sin();
path.line_to(kurbo::Point::new(end_x, end_y));
// Draw arc manually
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
arc.to_cubic_beziers(0.1, |p1, p2, p| {
path.line_to(kurbo::Point::new(start1_x, start1_y));
let arc1 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start1, FRAC_PI_2, 0.0);
arc1.to_cubic_beziers(0.1, |p1, p2, p| {
path.curve_to(p1, p2, p);
});
path.close_path();
// Bottom-right sector
let start2 = PI + FRAC_PI_2 + angle;
let start2_x = x + DOWEL_PIN_RADIUS * start2.cos();
let start2_y = y + DOWEL_PIN_RADIUS * start2.sin();
path.move_to(kurbo::Point::new(x, y));
let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).cos();
let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).sin();
path.line_to(kurbo::Point::new(end_x, end_y));
// Draw arc manually
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
arc.to_cubic_beziers(0.1, |p1, p2, p| {
path.line_to(kurbo::Point::new(start2_x, start2_y));
let arc2 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start2, FRAC_PI_2, 0.0);
arc2.to_cubic_beziers(0.1, |p1, p2, p| {
path.curve_to(p1, p2, p);
});
path.close_path();
@@ -852,7 +873,7 @@ impl OverlayContextInternal {
self.bezier_to_path(bezier, transform, move_to, &mut path);
}
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
}
/// Used by the Pen tool in order to show how the bezier curve would look like.
@@ -861,7 +882,7 @@ impl OverlayContextInternal {
let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path);
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
}
/// Used by the path tool segment mode in order to show the selected segments.
@@ -901,29 +922,30 @@ impl OverlayContextInternal {
};
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
let start_point = self.snap_to_physical_pixel(start_point);
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
for curve in curves {
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5);
let a = self.snap_to_physical_pixel_center(a);
path.line_to(kurbo::Point::new(a.x, a.y));
}
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
}
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
let c = self.snap_to_physical_pixel_center(c);
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
}
}
@@ -954,7 +976,7 @@ impl OverlayContextInternal {
let path = self.push_path(subpaths.iter(), transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(color), None, &path);
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
}
}
@@ -989,15 +1011,20 @@ impl OverlayContextInternal {
data[index..index + 4].copy_from_slice(&rgba);
}
let image = peniko::Image {
data: data.into(),
format: peniko::ImageFormat::Rgba8,
width: PATTERN_WIDTH,
height: PATTERN_HEIGHT,
x_extend: peniko::Extend::Repeat,
y_extend: peniko::Extend::Repeat,
alpha: 1.0,
quality: peniko::ImageQuality::default(),
let image = peniko::ImageBrush {
image: peniko::ImageData {
data: data.into(),
format: peniko::ImageFormat::Rgba8,
width: PATTERN_WIDTH,
height: PATTERN_HEIGHT,
alpha_type: peniko::ImageAlphaType::Alpha,
},
sampler: peniko::ImageSampler {
x_extend: peniko::Extend::Repeat,
y_extend: peniko::Extend::Repeat,
quality: peniko::ImageQuality::default(),
alpha: 1.,
},
};
let path = self.push_path(subpaths, transform);
@@ -1006,32 +1033,6 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
}
fn get_width(&self, text: &str) -> f64 {
// Use the actual text-to-path system to get precise text width
const FONT_SIZE: f64 = 12.0;
let typesetting = TypesettingConfig {
font_size: FONT_SIZE,
line_height_ratio: 1.2,
character_spacing: 0.0,
max_width: None,
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
};
// Load Source Sans Pro font data
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font_blob = Some(load_font(FONT_DATA));
// Convert text to paths and calculate actual bounds
let text_table = to_path(text, font_blob, typesetting, false);
let text_bounds = self.calculate_text_bounds(&text_table);
text_bounds.width()
}
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
// Use the proper text-to-path system for accurate text rendering
const FONT_SIZE: f64 = 12.0;
@@ -1050,15 +1051,18 @@ impl OverlayContextInternal {
// Load Source Sans Pro font data
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font_blob = Some(load_font(FONT_DATA));
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
// Convert text to vector paths using the existing text system
let text_table = to_path(text, font_blob, typesetting, false);
// Calculate text bounds from the generated paths
let text_bounds = self.calculate_text_bounds(&text_table);
let text_width = text_bounds.width();
let text_height = text_bounds.height();
// Get text dimensions directly from layout
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let text_size = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
let text_width = text_size.x;
let text_height = text_size.y;
// Create a rect from the size (assuming text starts at origin)
let text_bounds = kurbo::Rect::new(0.0, 0.0, text_width, text_height);
// Convert text to vector paths for rendering
let text_table = text_context.to_path(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
// Calculate position based on pivot
let mut position = DVec2::ZERO;
@@ -1093,56 +1097,6 @@ impl OverlayContextInternal {
self.render_text_paths(&text_table, font_color, vello_transform);
}
// Calculate bounds of text from vector table
fn calculate_text_bounds(&self, text_table: &Table<Vector>) -> kurbo::Rect {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for row in text_table.iter() {
// Use the existing segment_bezier_iter to get all bezier curves
for (_, bezier, _, _) in row.element.segment_bezier_iter() {
let transformed_bezier = bezier.apply_transformation(|point| row.transform.transform_point2(point));
// Add start and end points to bounds
let points = [transformed_bezier.start, transformed_bezier.end];
for point in points {
min_x = min_x.min(point.x);
min_y = min_y.min(point.y);
max_x = max_x.max(point.x);
max_y = max_y.max(point.y);
}
// Add handle points if they exist
match transformed_bezier.handles {
subpath::BezierHandles::Quadratic { handle } => {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y);
}
subpath::BezierHandles::Cubic { handle_start, handle_end } => {
for handle in [handle_start, handle_end] {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y);
}
}
_ => {}
}
}
}
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
kurbo::Rect::new(min_x, min_y, max_x, max_y)
} else {
// Fallback for empty text
kurbo::Rect::new(0.0, 0.0, 0.0, 12.0)
}
}
// Render text paths to the vello scene using existing infrastructure
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
let color = Self::parse_color(font_color);
@@ -1193,4 +1147,20 @@ impl OverlayContextInternal {
self.line(quad.bottom_left(), quad.bottom_right(), None, None);
}
}
fn snap_to_physical_pixel(&self, p: DVec2) -> DVec2 {
let s = self.viewport.scale();
if !s.is_finite() || s <= 0.0 {
return p.round();
}
(p * s).round() / s
}
fn snap_to_physical_pixel_center(&self, p: DVec2) -> DVec2 {
let s = self.viewport.scale();
if !s.is_finite() || s <= 0.0 {
return p.round() - DVec2::splat(0.5);
}
self.snap_to_physical_pixel(p) - DVec2::splat(0.5 / s)
}
}
@@ -2,9 +2,11 @@ use super::utility_functions::overlay_canvas_context;
use crate::consts::{
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::Message;
use crate::messages::viewport::ViewportMessageHandler;
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
@@ -35,6 +37,7 @@ pub enum OverlaysType {
TransformCage,
HoverOutline,
SelectionOutline,
LayerOriginCross,
Pivot,
Origin,
Path,
@@ -53,6 +56,7 @@ pub struct OverlaysVisibilitySettings {
pub transform_cage: bool,
pub hover_outline: bool,
pub selection_outline: bool,
pub layer_origin_cross: bool,
pub pivot: bool,
pub origin: bool,
pub path: bool,
@@ -71,6 +75,7 @@ impl Default for OverlaysVisibilitySettings {
transform_cage: true,
hover_outline: true,
selection_outline: true,
layer_origin_cross: true,
pivot: true,
origin: true,
path: true,
@@ -113,6 +118,10 @@ impl OverlaysVisibilitySettings {
self.all && self.selection_outline
}
pub fn layer_origin_cross(&self) -> bool {
self.all && self.layer_origin_cross
}
pub fn pivot(&self) -> bool {
self.all && self.pivot
}
@@ -140,10 +149,7 @@ pub struct OverlayContext {
#[serde(skip, default = "overlay_canvas_context")]
#[specta(skip)]
pub render_context: web_sys::CanvasRenderingContext2d,
pub size: DVec2,
// The device pixel ratio is a property provided by the browser window and is the CSS pixel size divided by the physical monitor's pixel size.
// It allows better pixel density of visualizations on high-DPI displays where the OS display scaling is not 100%, or where the browser is zoomed.
pub device_pixel_ratio: f64,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,
}
// Message hashing isn't used but is required by the message system macros
@@ -498,11 +504,25 @@ impl OverlayContext {
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
}
pub fn resize_handle(&mut self, position: DVec2, rotation: f64) {
let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);
self.quad(quad, None, Some(COLOR_OVERLAY_WHITE));
}
pub fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) {
let edge_dir = (edge_end - edge_start).normalize();
let mid = edge_end.midpoint(edge_start);
for edge in [edge_dir, -edge_dir] {
self.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET), edge, SKEW_TRIANGLE_SIZE, None, None);
}
}
/// Transforms the canvas context to adjust for DPI scaling
///
/// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`].
fn start_dpi_aware_transform(&self) {
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(self.device_pixel_ratio)).to_cols_array();
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(self.viewport.scale())).to_cols_array();
self.render_context
.set_transform(a, b, c, d, e, f)
.expect("transform should be able to be set to be able to account for DPI");
@@ -949,10 +969,6 @@ impl OverlayContext {
self.render_context.fill();
}
pub fn get_width(&self, text: &str) -> f64 {
self.render_context.measure_text(text).expect("Failed to measure text dimensions").width()
}
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
let x = match pivot[0] {
@@ -966,7 +982,7 @@ impl OverlayContext {
Pivot::End => -padding,
};
let [a, b, c, d, e, f] = (DAffine2::from_scale(DVec2::splat(self.device_pixel_ratio)) * transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array();
let [a, b, c, d, e, f] = (DAffine2::from_scale(DVec2::splat(self.viewport.scale())) * transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array();
self.render_context.set_transform(a, b, c, d, e, f).expect("Failed to rotate the render context to the specified angle");
if let Some(background) = background_color {
@@ -1024,7 +1040,7 @@ pub enum Pivot {
pub enum DrawHandles {
All,
SelectedAnchors(Vec<SegmentId>),
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
None,
}
@@ -35,7 +35,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
match message {
PropertiesPanelMessage::Clear => {
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout: Layout::default(),
layout_target: LayoutTarget::PropertiesPanel,
});
}
@@ -53,10 +53,10 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
document_name,
executor,
};
let properties_sections = NodeGraphMessageHandler::collate_properties(&mut node_properties_context);
let layout = Layout(NodeGraphMessageHandler::collate_properties(&mut node_properties_context));
node_properties_context.responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
layout,
layout_target: LayoutTarget::PropertiesPanel,
});
}
@@ -5,10 +5,9 @@ use graph_craft::document::NodeId;
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug, specta::Type)]
pub enum Clipboard {
Internal,
Device,
_InternalClipboardCount, // Keep this as the last entry of **internal** clipboards since it is used for counting the number of enum variants
Device,
}
pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;
@@ -1,6 +1,7 @@
use super::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2};
@@ -12,6 +13,7 @@ use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, Vector};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
use std::sync::Arc;
// ================
// DocumentMetadata
@@ -25,7 +27,7 @@ pub struct DocumentMetadata {
pub local_transforms: HashMap<NodeId, DAffine2>,
pub first_element_source_ids: HashMap<NodeId, Option<NodeId>>,
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>,
pub clip_targets: HashSet<NodeId>,
pub vector_modify: HashMap<NodeId, Vector>,
/// Transform from document space to viewport space.
@@ -45,8 +47,8 @@ impl DocumentMetadata {
self.structure.contains_key(&layer)
}
pub fn click_targets(&self, layer: LayerNodeIdentifier) -> Option<&Vec<ClickTarget>> {
self.click_targets.get(&layer)
pub fn click_targets(&self, layer: LayerNodeIdentifier) -> Option<&[Arc<ClickTarget>]> {
self.click_targets.get(&layer).map(|x| x.as_slice())
}
/// Access the [`NodeRelations`] of a layer.
@@ -91,16 +93,15 @@ impl DocumentMetadata {
let mut use_local = true;
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
if let Some(path_node) = graph_layer.upstream_visible_node_id_from_name_in_layer("Path") {
if let Some(&source) = self.first_element_source_ids.get(&layer.to_node()) {
if !network_interface
.upstream_flow_back_from_nodes(vec![path_node], &[], FlowType::HorizontalFlow)
.any(|upstream| Some(upstream) == source)
{
use_local = false;
info!("Local transform is invalid — using the identity for the local transform instead")
}
}
let identifier = DefinitionIdentifier::Network("Path".into());
if let Some(path_node) = graph_layer.upstream_visible_node_id_from_name_in_layer(&identifier)
&& let Some(&source) = self.first_element_source_ids.get(&layer.to_node())
&& !network_interface
.upstream_flow_back_from_nodes(vec![path_node], &[], FlowType::HorizontalFlow)
.any(|upstream| Some(upstream) == source)
{
use_local = false;
info!("Local transform is invalid — using the identity for the local transform instead")
}
let local_transform = use_local.then(|| self.local_transforms.get(&layer.to_node()).copied()).flatten().unwrap_or_default();
@@ -116,7 +117,7 @@ impl DocumentMetadata {
let local_transform = self.local_transforms.get(&layer.to_node()).copied();
let transform = local_transform.unwrap_or_else(|| {
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain("Transform", layer, network_interface);
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain(&DefinitionIdentifier::Network("Transform".into()), layer, network_interface);
let transform_node = transform_node_id.and_then(|id| network_interface.document_node(&id, &[]));
transform_node.map(|node| transform_utils::get_current_transform(node.inputs.as_slice())).unwrap_or_default()
});
@@ -154,10 +155,7 @@ impl DocumentMetadata {
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
self.click_targets(layer)?
.iter()
.filter_map(|click_target| match click_target.target_type() {
ClickTargetType::Subpath(subpath) => subpath.bounding_box_with_transform(transform),
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
})
.filter_map(|click_target| click_target.bounding_box_with_transform(transform))
.reduce(Quad::combine_bounds)
}
@@ -209,7 +207,7 @@ impl DocumentMetadata {
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
static EMPTY: Vec<Arc<ClickTarget>> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().filter_map(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => Some(subpath),
@@ -218,7 +216,7 @@ impl DocumentMetadata {
}
pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
static EMPTY: Vec<ClickTarget> = Vec::new();
static EMPTY: Vec<Arc<ClickTarget>> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(|target| target.target_type())
}
@@ -16,7 +16,13 @@ pub enum EditorError {
#[error("The operation caused a document error:\n{0:?}")]
Document(String),
#[error("This document was created in an older version of the editor.\n\nBackwards compatibility is, regrettably, not present in the current alpha release.\n\nTechnical details:\n{0:?}")]
#[error(
"This document was created in an older version of the editor.\n\
\n\
Full backwards compatibility is not guaranteed in the current alpha release.\n\
\n\
If this document is critical, ask for support in Graphite's Discord community."
)]
DocumentDeserialization(String),
#[error("{0}")]
@@ -26,33 +26,33 @@ pub enum AlignAggregate {
Center,
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum DocumentMode {
#[default]
DesignMode,
SelectMode,
GuideMode,
}
// #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
// pub enum DocumentMode {
// #[default]
// DesignMode,
// SelectMode,
// GuideMode,
// }
impl fmt::Display for DocumentMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DocumentMode::DesignMode => write!(f, "Design Mode"),
DocumentMode::SelectMode => write!(f, "Select Mode"),
DocumentMode::GuideMode => write!(f, "Guide Mode"),
}
}
}
// impl fmt::Display for DocumentMode {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// match self {
// DocumentMode::DesignMode => write!(f, "Design Mode"),
// DocumentMode::SelectMode => write!(f, "Select Mode"),
// DocumentMode::GuideMode => write!(f, "Guide Mode"),
// }
// }
// }
impl DocumentMode {
pub fn icon_name(&self) -> String {
match self {
DocumentMode::DesignMode => "ViewportDesignMode".to_string(),
DocumentMode::SelectMode => "ViewportSelectMode".to_string(),
DocumentMode::GuideMode => "ViewportGuideMode".to_string(),
}
}
}
// impl DocumentMode {
// pub fn icon_name(&self) -> String {
// match self {
// DocumentMode::DesignMode => "ViewportDesignMode".to_string(),
// DocumentMode::SelectMode => "ViewportSelectMode".to_string(),
// DocumentMode::GuideMode => "ViewportGuideMode".to_string(),
// }
// }
// }
/// SnappingState determines the current individual snapping states
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -386,55 +386,55 @@ pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 5] = [
(
"Align with Edges",
(|snapping_state| &mut snapping_state.bounding_box.align_with_edges) as GetSnapState,
"Snaps to horizontal/vertical alignment with the edges of any layer's bounding box",
"Snaps to horizontal/vertical alignment with the edges of any layer's bounding box.",
),
(
"Corner Points",
(|snapping_state| &mut snapping_state.bounding_box.corner_point) as GetSnapState,
"Snaps to the four corners of any layer's bounding box",
"Snaps to the four corners of any layer's bounding box.",
),
(
"Center Points",
(|snapping_state| &mut snapping_state.bounding_box.center_point) as GetSnapState,
"Snaps to the center point of any layer's bounding box",
"Snaps to the center point of any layer's bounding box.",
),
(
"Edge Midpoints",
(|snapping_state| &mut snapping_state.bounding_box.edge_midpoint) as GetSnapState,
"Snaps to any of the four points at the middle of the edges of any layer's bounding box",
"Snaps to any of the four points at the middle of the edges of any layer's bounding box.",
),
(
"Distribute Evenly",
(|snapping_state| &mut snapping_state.bounding_box.distribute_evenly) as GetSnapState,
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers",
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers.",
),
];
pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
(
"Align with Anchor Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.align_with_anchor_point) as GetSnapState,
"Snaps to horizontal/vertical alignment with the anchor points of any vector path",
"Snaps to horizontal/vertical alignment with the anchor points of any vector path.",
),
(
"Anchor Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.anchor_point) as GetSnapState,
"Snaps to the anchor point of any vector path",
"Snaps to the anchor point of any vector path.",
),
(
// TODO: Extend to the midpoints of curved segments and rename to "Segment Midpoint"
"Line Midpoints",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.line_midpoint) as GetSnapState,
"Snaps to the point at the middle of any straight line segment of a vector path",
"Snaps to the point at the middle of any straight line segment of a vector path.",
),
(
"Path Intersection Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.path_intersection_point) as GetSnapState,
"Snaps to any points where vector paths intersect",
"Snaps to any points where vector paths intersect.",
),
(
"Along Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.along_path) as GetSnapState,
"Snaps along the length of any vector path",
"Snaps along the length of any vector path.",
),
(
// TODO: This works correctly for line segments, but not curved segments.
@@ -442,7 +442,7 @@ pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
"Normal to Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.normal_to_path) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Intersections of Paths' to be enabled
"Snaps a line to a point perpendicular to a vector path\n(due to a bug, 'Intersections of Paths' must be enabled)",
"Snaps a line to a point perpendicular to a vector path.\n(Due to a bug, 'Intersections of Paths' must be enabled.)",
),
(
// TODO: This works correctly for line segments, but not curved segments.
@@ -450,7 +450,7 @@ pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
"Tangent to Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.tangent_to_path) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Intersections of Paths' to be enabled
"Snaps a line to a point tangent to a vector path\n(due to a bug, 'Intersections of Paths' must be enabled)",
"Snaps a line to a point tangent to a vector path.\n(Due to a bug, 'Intersections of Paths' must be enabled.)",
),
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,201 @@
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, InputPersistentMetadata, NodeNetworkMetadata, NodeTypePersistentMetadata};
use serde_json::Value;
use std::collections::HashMap;
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataInputNames {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_names: Vec<String>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataInputNames> for DocumentNodePersistentMetadataPropertiesRow {
fn from(old: DocumentNodePersistentMetadataInputNames) -> Self {
DocumentNodePersistentMetadataPropertiesRow {
reference: old.reference,
input_properties: Vec::new(),
display_name: old.display_name,
output_names: old.output_names,
has_primary_output: old.has_primary_output,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataPropertiesRow {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_properties: Vec<PropertiesRow>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PropertiesRow {
pub input_data: HashMap<String, Value>,
pub widget_override: Option<String>,
#[serde(skip)]
pub input_name: String,
#[serde(skip)]
pub input_description: String,
}
impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersistentMetadataHasPrimaryOutput {
fn from(old: DocumentNodePersistentMetadataPropertiesRow) -> Self {
let mut input_metadata = Vec::new();
for properties_row in old.input_properties {
input_metadata.push(InputMetadata {
persistent_metadata: InputPersistentMetadata {
input_data: properties_row.input_data,
widget_override: properties_row.widget_override,
input_name: properties_row.input_name,
input_description: properties_row.input_description,
},
..Default::default()
})
}
DocumentNodePersistentMetadataHasPrimaryOutput {
reference: old.reference,
display_name: old.display_name,
input_metadata: Vec::new(),
output_names: old.output_names,
has_primary_output: old.has_primary_output,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataHasPrimaryOutput {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_metadata: Vec<InputMetadata>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataHasPrimaryOutput> for DocumentNodePersistentMetadataStringReference {
fn from(old: DocumentNodePersistentMetadataHasPrimaryOutput) -> Self {
DocumentNodePersistentMetadataStringReference {
reference: old.reference,
display_name: old.display_name,
input_metadata: old.input_metadata,
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
struct DocumentNodePersistentMetadataStringReference {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_metadata: Vec<InputMetadata>,
pub output_names: Vec<String>,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataStringReference> for DocumentNodePersistentMetadata {
fn from(mut old: DocumentNodePersistentMetadataStringReference) -> Self {
if let Some(metadata) = old.network_metadata.as_mut() {
metadata.persistent_metadata.reference = old.reference;
}
DocumentNodePersistentMetadata {
display_name: old.display_name,
input_metadata: old.input_metadata,
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum DocumentNodePersistentMetadataVersioned {
// Newest first
Current(DocumentNodePersistentMetadata),
StringReference(DocumentNodePersistentMetadataStringReference),
HasPrimaryOutput(DocumentNodePersistentMetadataHasPrimaryOutput),
PropertiesRow(DocumentNodePersistentMetadataPropertiesRow),
InputNames(DocumentNodePersistentMetadataInputNames),
}
pub fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let value = Value::deserialize(deserializer)?;
let versioned_document = serde_json::from_value::<DocumentNodePersistentMetadataVersioned>(value).map_err(serde::de::Error::custom)?;
let current: DocumentNodePersistentMetadata = match versioned_document {
DocumentNodePersistentMetadataVersioned::Current(v) => v,
DocumentNodePersistentMetadataVersioned::StringReference(v) => {
let v: DocumentNodePersistentMetadataStringReference = v;
v.into()
}
DocumentNodePersistentMetadataVersioned::HasPrimaryOutput(v) => {
let v: DocumentNodePersistentMetadataStringReference = v.into();
v.into()
}
DocumentNodePersistentMetadataVersioned::PropertiesRow(v) => {
let v: DocumentNodePersistentMetadataHasPrimaryOutput = v.into();
let v: DocumentNodePersistentMetadataStringReference = v.into();
v.into()
}
DocumentNodePersistentMetadataVersioned::InputNames(v) => {
let v: DocumentNodePersistentMetadataPropertiesRow = v.into();
let v: DocumentNodePersistentMetadataHasPrimaryOutput = v.into();
let v: DocumentNodePersistentMetadataStringReference = v.into();
v.into()
}
};
Ok(current)
}
@@ -0,0 +1,57 @@
use graph_craft::document::NodeNetwork;
use std::cell::Cell;
use std::hash::{Hash, Hasher};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct MemoNetwork {
network: NodeNetwork,
hash_code: Cell<Option<u64>>,
}
impl<'de> serde::Deserialize<'de> for MemoNetwork {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Self::new(NodeNetwork::deserialize(deserializer)?))
}
}
impl serde::Serialize for MemoNetwork {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.network.serialize(serializer)
}
}
impl Hash for MemoNetwork {
fn hash<H: Hasher>(&self, state: &mut H) {
self.current_hash().hash(state);
}
}
impl MemoNetwork {
pub fn network(&self) -> &NodeNetwork {
&self.network
}
pub fn network_mut(&mut self) -> &mut NodeNetwork {
self.hash_code.set(None);
&mut self.network
}
pub fn new(network: NodeNetwork) -> Self {
Self { network, hash_code: None.into() }
}
pub fn current_hash(&self) -> u64 {
let mut hash_code = self.hash_code.get();
if hash_code.is_none() {
hash_code = Some(self.network.current_hash());
self.hash_code.set(hash_code);
}
hash_code.unwrap()
}
}
@@ -0,0 +1,379 @@
use std::collections::{HashMap, HashSet};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
use graph_craft::proto::{GraphErrorType, GraphErrors};
use graph_craft::{Type, concrete};
use graphene_std::uuid::NodeId;
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::node_registry::NODE_REGISTRY;
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
// This file contains utility methods for interfacing with the resolved types returned from the compiler
#[derive(Debug, Default)]
pub struct ResolvedDocumentNodeTypes {
pub types: HashMap<Vec<NodeId>, NodeTypes>,
pub node_graph_errors: GraphErrors,
}
impl ResolvedDocumentNodeTypes {
pub fn update(&mut self, delta: ResolvedDocumentNodeTypesDelta, errors: GraphErrors) {
for (path, node_type) in delta.add {
self.types.insert(path.to_vec(), node_type);
}
for path in delta.remove {
self.types.remove(&path.to_vec());
}
self.node_graph_errors = errors;
}
}
/// Represents the result of a type query for an input or output connector.
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSource {
/// A type that has been compiled based on all upstream types.
Compiled(Type),
/// The type of value inputs.
TaggedValue(Type),
/// When the input/output is not compiled. The Type is from the document node definition, or () if it doesn't exist.
Unknown,
/// When there is a node graph error for the inputs to a node. The Type is from the document node definition, or () if it doesn't exist.
Invalid,
/// When there is an error in the algorithm for determining the input/output type (indicates a bug in the editor).
Error(&'static str),
}
impl TypeSource {
/// The reduced set of frontend types for displaying color.
pub fn displayed_type(&self) -> FrontendGraphDataType {
if matches!(self, TypeSource::Invalid) {
return FrontendGraphDataType::Invalid;
};
match self.compiled_nested_type() {
Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) {
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F32(_)
| TaggedValue::F64(_)
| TaggedValue::DVec2(_)
| TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => FrontendGraphDataType::Number,
TaggedValue::Artboard(_) => FrontendGraphDataType::Artboard,
TaggedValue::Graphic(_) => FrontendGraphDataType::Graphic,
TaggedValue::Raster(_) => FrontendGraphDataType::Raster,
TaggedValue::Vector(_) => FrontendGraphDataType::Vector,
TaggedValue::Color(_) => FrontendGraphDataType::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => FrontendGraphDataType::Gradient,
TaggedValue::String(_) => FrontendGraphDataType::Typography,
_ => FrontendGraphDataType::General,
},
None => FrontendGraphDataType::General,
}
}
pub fn compiled_nested_type(&self) -> Option<&Type> {
match self {
TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()),
TypeSource::TaggedValue(value_type) => Some(value_type.nested_type()),
_ => None,
}
}
/// Used when searching for nodes in the add Node popup.
pub fn add_node_string(self) -> Option<String> {
self.compiled_nested_type().map(|ty| format!("type:{ty}"))
}
/// The type to display in the tooltip label.
pub fn resolved_type_tooltip_string(&self) -> String {
match self {
TypeSource::Compiled(compiled_type) => format!("Data Type: {}", compiled_type.nested_type()),
TypeSource::TaggedValue(value_type) => format!("Data Type: {}", value_type.nested_type()),
TypeSource::Unknown => "Unknown Data Type".to_string(),
TypeSource::Invalid => "Invalid Type Combination".to_string(),
TypeSource::Error(_) => "Error Getting Data Type".to_string(),
}
}
/// The type to display in the node row.
pub fn resolved_type_node_string(&self) -> String {
match self {
TypeSource::Compiled(compiled_type) => compiled_type.nested_type().to_string(),
TypeSource::TaggedValue(value_type) => value_type.nested_type().to_string(),
TypeSource::Unknown => "Unknown".to_string(),
TypeSource::Invalid => "Invalid".to_string(),
TypeSource::Error(_) => "Error".to_string(),
}
}
}
impl NodeNetworkInterface {
fn input_has_error(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> bool {
match input_connector {
InputConnector::Node { node_id, input_index } => {
let Some(implementation) = self.implementation(node_id, network_path) else {
log::error!("Could not get implementation in input_has_error");
return false;
};
let node_path = [network_path, &[*node_id]].concat();
match implementation {
DocumentNodeImplementation::Network(_) => {
let Some(map) = self.outward_wires(&node_path) else { return false };
let Some(outward_wires) = map.get(&OutputConnector::Import(*input_index)) else { return false };
outward_wires.clone().iter().any(|connector| match connector {
InputConnector::Node { node_id, input_index } => self.input_has_error(&InputConnector::node(*node_id, *input_index), &node_path),
InputConnector::Export(_) => false,
})
}
DocumentNodeImplementation::ProtoNode(_) => self.resolved_types.node_graph_errors.iter().any(|error| {
error.node_path == node_path
&& match &error.error {
GraphErrorType::InvalidImplementations { error_inputs, .. } => error_inputs.iter().any(|solution| solution.iter().any(|(index, _)| index == input_index)),
_ => true,
}
}),
DocumentNodeImplementation::Extract => false,
}
}
InputConnector::Export(_) => false,
}
}
pub fn input_type_not_invalid(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
let Some(input) = self.input_from_connector(input_connector, network_path) else {
return TypeSource::Error("Could not get input from connector");
};
match input {
NodeInput::Node { node_id, output_index } => {
let output_connector = OutputConnector::node(*node_id, *output_index);
self.output_type(&output_connector, network_path)
}
NodeInput::Value { tagged_value, .. } => TypeSource::TaggedValue(tagged_value.ty()),
NodeInput::Import { import_index, .. } => {
// Get the input type of the encapsulating node input
let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else {
return TypeSource::Error("Could not get type of import in document network since it has no imports");
};
self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path)
}
NodeInput::Scope(_) => TypeSource::Compiled(concrete!(())),
NodeInput::Reflection(document_node_metadata) => TypeSource::Compiled(document_node_metadata.ty()),
NodeInput::Inline(_) => TypeSource::Compiled(concrete!(InlineRust)),
}
}
/// Get the [`TypeSource`] for any InputConnector.
/// If the input is not compiled, then an Unknown or default from the definition is returned.
pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
// First check if there is an error with this node or any protonodes it is connected to
if self.input_has_error(input_connector, network_path) {
return TypeSource::Invalid;
}
self.input_type_not_invalid(input_connector, network_path)
}
/// Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation.
pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue {
let guaranteed_type = match self.input_type(input_connector, network_path) {
TypeSource::Compiled(compiled) => compiled,
TypeSource::TaggedValue(value) => value,
TypeSource::Unknown | TypeSource::Invalid => {
// Pick a random type from the complete valid types
// TODO: Add a NodeInput::Indeterminate which can be resolved at compile time to be any type that prevents an error. This may require bidirectional typing.
self.complete_valid_input_types(input_connector, network_path)
.into_iter()
.min_by_key(|ty| ty.nested_type().identifier_name())
// Pick a random type from the potential valid types
.or_else(|| {
self.potential_valid_input_types(input_connector, network_path)
.into_iter()
.min_by_key(|ty| ty.nested_type().identifier_name())
}).unwrap_or(concrete!(()))
}
TypeSource::Error(e) => {
log::error!("Error getting tagged_value_from_input for {input_connector:?} {e}");
concrete!(())
}
};
TaggedValue::from_type_or_none(&guaranteed_type)
}
/// A list of all valid input types for this specific node.
pub fn potential_valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
let InputConnector::Node { node_id, input_index } = input_connector else {
// An export can have any type connected to it
return vec![graph_craft::generic!(T)];
};
let Some(implementation) = self.implementation(node_id, network_path) else {
log::error!("Could not get node implementation in potential_valid_input_types");
return Vec::new();
};
match implementation {
DocumentNodeImplementation::Network(_) => {
let nested_path = [network_path, &[*node_id]].concat();
let Some(outward_wires) = self.outward_wires(&nested_path) else {
log::error!("Could not get outward wires in potential_valid_input_types");
return Vec::new();
};
let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else {
log::error!("Could not get inputs from import in potential_valid_input_types");
return Vec::new();
};
let intersection: HashSet<Type> = inputs_from_import
.clone()
.iter()
.map(|input_connector| self.potential_valid_input_types(input_connector, &nested_path).into_iter().collect::<HashSet<_>>())
.fold(None, |acc: Option<HashSet<Type>>, set| match acc {
Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()),
None => Some(set),
})
.unwrap_or_default();
intersection.into_iter().collect::<Vec<_>>()
}
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => {
let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else {
log::error!("Protonode {proto_node_identifier:?} not found in registry in potential_valid_input_types");
return Vec::new();
};
let number_of_inputs = self.number_of_inputs(node_id, network_path);
implementations
.iter()
.filter_map(|(node_io, _)| {
// Check if this NodeIOTypes implementation is valid for the other inputs
let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| {
let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path);
// TODO: Fix type checking for different call arguments
// For example a node input of (Footprint) -> Vector would not be compatible with a node that is called with () and returns Vector
node_io.inputs.get(iterator_index).map(|ty| ty.nested_type()) == input_type.compiled_nested_type()
});
// If so, then return the input at the chosen index
if valid_implementation { node_io.inputs.get(*input_index).cloned() } else { None }
})
.collect::<Vec<_>>()
}
DocumentNodeImplementation::Extract => {
log::error!("Input types for extract node not supported");
Vec::new()
}
}
}
/// Performs a downstream traversal to ensure input type will work in the full context of the graph.
pub fn complete_valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
match input_connector {
InputConnector::Node { node_id, input_index } => {
let Some(implementation) = self.implementation(node_id, network_path) else {
log::error!("Could not get node implementation for {:?} {} in complete_valid_input_types", network_path, *node_id);
return Vec::new();
};
match implementation {
DocumentNodeImplementation::Network(_) => self.valid_output_types(&OutputConnector::Import(input_connector.input_index()), &[network_path, &[*node_id]].concat()),
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => {
let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else {
log::error!("Protonode {proto_node_identifier:?} not found in registry in complete_valid_input_types");
return Vec::new();
};
let valid_output_types = self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path);
implementations
.iter()
.filter_map(|(node_io, _)| {
if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) {
return None;
}
let valid_inputs = (0..node_io.inputs.len()).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| {
let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path);
match input_type.compiled_nested_type() {
Some(input_type) => node_io.inputs.get(iterator_index).is_some_and(|node_io_input_type| node_io_input_type.nested_type() == input_type),
None => true,
}
});
if valid_inputs { node_io.inputs.get(*input_index).cloned() } else { None }
})
.collect::<Vec<_>>()
}
DocumentNodeImplementation::Extract => Vec::new(),
}
}
InputConnector::Export(export_index) => {
match network_path.split_last() {
Some((encapsulating_node, encapsulating_path)) => self.valid_output_types(&OutputConnector::node(*encapsulating_node, *export_index), encapsulating_path),
None => {
// Valid types for the export are all types that can be fed into the render node
let render_node = graphene_std::render_node::render::IDENTIFIER;
let Some(implementations) = NODE_REGISTRY.get(&render_node) else {
log::error!("Protonode {render_node:?} not found in registry");
return Vec::new();
};
implementations.keys().map(|types| types.inputs[1].clone()).collect()
}
}
}
}
}
pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource {
match output_connector {
OutputConnector::Node { node_id, output_index } => {
// First try iterating upstream to the first protonode and try get its compiled type
let Some(implementation) = self.implementation(node_id, network_path) else {
return TypeSource::Error("Could not get implementation");
};
match implementation {
DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()),
DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) {
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
None => TypeSource::Unknown,
},
DocumentNodeImplementation::Extract => TypeSource::Compiled(concrete!(())),
}
}
OutputConnector::Import(import_index) => {
let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else {
return TypeSource::Error("Cannot get import type in document network since it has no imports");
};
let mut input_type = self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path);
if matches!(input_type, TypeSource::Invalid) {
input_type = TypeSource::Unknown
}
input_type
}
}
}
/// The valid output types are all types that are valid for each downstream connection.
fn valid_output_types(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Vec<Type> {
let Some(outward_wires) = self.outward_wires(network_path) else {
log::error!("Could not get outward wires in valid_output_types");
return Vec::new();
};
let Some(inputs_from_import) = outward_wires.get(output_connector) else {
log::error!("Could not get inputs from import in valid_output_types");
return Vec::new();
};
let intersection = inputs_from_import
.clone()
.iter()
.map(|input_connector| self.potential_valid_input_types(input_connector, network_path).into_iter().collect::<HashSet<_>>())
.fold(None, |acc: Option<HashSet<Type>>, set| match acc {
Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()),
None => Some(set),
})
.unwrap_or_default();
intersection.into_iter().collect::<Vec<_>>()
}
}
@@ -34,8 +34,11 @@ impl serde::Serialize for JsRawBuffer {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
pub struct LayerPanelEntry {
pub id: NodeId,
#[serde(rename = "implementationName")]
pub implementation_name: String,
#[serde(rename = "iconName")]
pub icon_name: Option<String>,
pub alias: String,
pub tooltip: String,
#[serde(rename = "inSelectedNetwork")]
pub in_selected_network: bool,
#[serde(rename = "childrenAllowed")]
@@ -167,8 +170,8 @@ impl SelectedNodes {
std::mem::replace(&mut self.0, new)
}
pub fn filtered_selected_nodes(&self, node_ids: std::collections::HashSet<NodeId>) -> SelectedNodes {
SelectedNodes(self.0.iter().filter(|node_id| node_ids.contains(node_id)).cloned().collect())
pub fn filtered_selected_nodes(&self, filter: impl Fn(&NodeId) -> bool) -> SelectedNodes {
SelectedNodes(self.0.iter().copied().filter(filter).collect())
}
}
@@ -2,16 +2,17 @@ use super::network_interface::NodeNetworkInterface;
use crate::consts::{ROTATE_INCREMENT, SCALE_INCREMENT};
use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn};
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformationState;
use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::vector::{HandleExt, PointId, VectorModificationType};
use std::collections::{HashMap, VecDeque};
use std::f64::consts::PI;
#[derive(Debug, PartialEq, Clone, Copy)]
struct AnchorPoint {
@@ -53,7 +54,7 @@ impl OriginalTransforms {
/// Gets the transform from the most downstream transform node
fn get_layer_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<DAffine2> {
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain("Transform", layer, network_interface)?;
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain(&DefinitionIdentifier::Network("Transform".into()), layer, network_interface)?;
let document_node = network_interface.document_network().nodes.get(&transform_node_id)?;
Some(transform_utils::get_current_transform(&document_node.inputs))
@@ -156,22 +157,25 @@ pub struct Translation {
}
impl Translation {
pub fn to_dvec(self, transform: DAffine2, increment_mode: bool) -> DVec2 {
pub fn to_dvec(self, state: &TransformationState, document: &DocumentMessageHandler) -> DVec2 {
let document_to_viewport = document.metadata().document_to_viewport;
let displacement = if let Some(value) = self.typed_distance {
match self.constraint {
Axis::X => transform.transform_vector2(DVec2::new(value, 0.)),
Axis::Y => transform.transform_vector2(DVec2::new(0., value)),
Axis::X => DVec2::X * value,
Axis::Y => DVec2::Y * value,
Axis::Both => self.dragged_distance,
}
} else {
match self.constraint {
Axis::Both => self.dragged_distance,
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
Axis::Y => DVec2::new(0., self.dragged_distance.y),
Axis::X => DVec2::X * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()),
Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()),
}
};
let displacement = transform.inverse().transform_vector2(displacement);
if increment_mode { displacement.round() } else { displacement }
let displacement_viewport = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale.
let displacement_document = document_to_viewport.inverse().transform_vector2(displacement_viewport);
let displacement_document = if state.is_rounded_to_intervals { displacement_document.round() } else { displacement_document }; // It rounds in document space?
document_to_viewport.transform_vector2(displacement_document)
}
#[must_use]
@@ -327,36 +331,19 @@ impl TransformType {
impl TransformOperation {
#[allow(clippy::too_many_arguments)]
pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
let local_axis_transform_angle = (quad.top_left() - quad.top_right()).to_angle();
pub fn apply_transform_operation(&self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
if self != &TransformOperation::None {
let transformation = match self {
TransformOperation::Grabbing(translation) => {
let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(local_transform, increment_mode)));
if local {
let resolved_angle = if local_axis_transform_angle > 0. {
local_axis_transform_angle
} else {
local_axis_transform_angle - PI
};
DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
} else {
translate
}
}
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(increment_mode)),
TransformOperation::Scaling(scale) => {
if local {
DAffine2::from_angle(local_axis_transform_angle) * DAffine2::from_scale(scale.to_dvec(increment_mode)) * DAffine2::from_angle(-local_axis_transform_angle)
} else {
DAffine2::from_scale(scale.to_dvec(increment_mode))
}
}
let mut transformation = match self {
TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state, document)),
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(state.is_rounded_to_intervals)),
TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(state.is_rounded_to_intervals)),
TransformOperation::None => unreachable!(),
};
let normalized_transform = state.local_to_viewport_transform();
transformation = normalized_transform * transformation * normalized_transform.inverse();
selected.update_transforms(transformation, Some(pivot), Some(*self));
self.hints(selected.responses, local);
selected.update_transforms(transformation, Some(state.pivot_viewport(document)), Some(*self));
self.hints(selected.responses, state.is_transforming_in_local_space);
}
}
@@ -373,24 +360,27 @@ impl TransformOperation {
}
#[allow(clippy::too_many_arguments)]
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) -> bool {
(*self, local) = match self {
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) -> bool {
let resulting_local;
(*self, resulting_local) = match self {
TransformOperation::Grabbing(translation) => {
let (translation, local) = translation.with_constraint(axis, local);
(TransformOperation::Grabbing(translation), local)
let (translation, resulting_local) = translation.with_constraint(axis, state.is_transforming_in_local_space);
(TransformOperation::Grabbing(translation), resulting_local)
}
TransformOperation::Scaling(scale) => {
let (scale, local) = scale.with_constraint(axis, local);
(TransformOperation::Scaling(scale), local)
let (scale, resulting_local) = scale.with_constraint(axis, state.is_transforming_in_local_space);
(TransformOperation::Scaling(scale), resulting_local)
}
_ => (*self, false),
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
local
self.apply_transform_operation(selected, state, document);
resulting_local
}
#[allow(clippy::too_many_arguments)]
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
@@ -398,7 +388,7 @@ impl TransformOperation {
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
self.apply_transform_operation(selected, state, document);
}
pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
@@ -456,15 +446,14 @@ impl TransformOperation {
}
let mut typing_hints = vec![HintInfo::keys([Key::Minus], "Negate Direction")];
if self.can_begin_typing() {
typing_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
typing_hints.push(HintInfo::keys([Key::FakeKeyNumbers], "Enter Number"));
if self.is_typing() {
typing_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
}
}
hint_groups.push(HintGroup(typing_hints));
let hint_data = HintData(hint_groups);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
HintData(hint_groups).send_layout(responses);
}
pub fn is_constraint_to_axis(&self) -> bool {
@@ -481,7 +470,7 @@ impl TransformOperation {
}
#[allow(clippy::too_many_arguments)]
pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
pub fn negate(&mut self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
if *self != TransformOperation::None {
*self = match self {
TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
@@ -489,7 +478,8 @@ impl TransformOperation {
TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
_ => *self,
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
self.apply_transform_operation(selected, state, document);
}
}
}
@@ -42,8 +42,8 @@ impl std::fmt::Display for GraphWireStyle {
impl GraphWireStyle {
pub fn tooltip_description(&self) -> &'static str {
match self {
GraphWireStyle::GridAligned => "Wires follow the grid, running in straight lines between nodes",
GraphWireStyle::Direct => "Wires bend to run at an angle directly between nodes",
GraphWireStyle::GridAligned => "Wires follow the grid, running in straight lines between nodes.",
GraphWireStyle::Direct => "Wires bend to run at an angle directly between nodes.",
}
}
File diff suppressed because it is too large Load Diff
@@ -1,742 +0,0 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
use crate::messages::prelude::*;
use graphene_std::path_bool::BooleanOperation;
#[derive(Debug, Clone, Default, ExtractField)]
pub struct MenuBarMessageHandler {
pub has_active_document: bool,
pub canvas_tilted: bool,
pub canvas_flipped: bool,
pub rulers_visible: bool,
pub node_graph_open: bool,
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
pub has_selection_history: (bool, 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]
impl MessageHandler<MenuBarMessage, ()> for MenuBarMessageHandler {
fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar),
}
}
fn actions(&self) -> ActionList {
actions!(MenuBarMessageDiscriminant;)
}
}
impl LayoutHolder for MenuBarMessageHandler {
fn layout(&self) -> Layout {
let no_active_document = !self.has_active_document;
let node_graph_open = self.node_graph_open;
let has_selected_nodes = self.has_selected_nodes;
let has_selected_layers = self.has_selected_layers;
let has_selection_history = self.has_selection_history;
let message_logging_verbosity_off = self.message_logging_verbosity == MessageLoggingVerbosity::Off;
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
let make_path_editable_is_allowed = self.make_path_editable_is_allowed;
let menu_bar_entries = vec![
MenuBarEntry {
icon: Some("GraphiteLogo".into()),
action: MenuBarEntry::create_action(|_| FrontendMessage::TriggerVisitLink { url: "https://graphite.rs".into() }.into()),
..Default::default()
},
MenuBarEntry::new_root(
"File".into(),
false,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "New…".into(),
icon: Some("File".into()),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestNewDocumentDialog),
children: MenuBarEntryChildren::empty(),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Open…".into(),
icon: Some("Folder".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuBarEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Open Demo Artwork…".into(),
icon: Some("Image".into()),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestDemoArtworkDialog.into()),
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Close".into(),
icon: Some("Close".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation),
action: MenuBarEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Close All".into(),
icon: Some("CloseAll".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::CloseAllDocumentsWithConfirmation),
action: MenuBarEntry::create_action(|_| PortfolioMessage::CloseAllDocumentsWithConfirmation.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Save".into(),
icon: Some("Save".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
#[cfg(not(target_family = "wasm"))]
MenuBarEntry {
label: "Save As…".into(),
icon: Some("Save".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs),
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Import…".into(),
icon: Some("FileImport".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
action: MenuBarEntry::create_action(|_| PortfolioMessage::Import.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Export…".into(),
icon: Some("FileExport".into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestExportDialog),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestExportDialog.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Preferences…".into(),
icon: Some("Settings".into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestPreferencesDialog),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestPreferencesDialog.into()),
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Edit".into(),
false,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Undo".into(),
icon: Some("HistoryUndo".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::Undo),
action: MenuBarEntry::create_action(|_| DocumentMessage::Undo.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Redo".into(),
icon: Some("HistoryRedo".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::Redo),
action: MenuBarEntry::create_action(|_| DocumentMessage::Redo.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Cut".into(),
icon: Some("Cut".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::Cut),
action: MenuBarEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Copy".into(),
icon: Some("Copy".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::Copy),
action: MenuBarEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Paste".into(),
icon: Some("Paste".into()),
shortcut: action_keys!(FrontendMessageDiscriminant::TriggerPaste),
action: MenuBarEntry::create_action(|_| FrontendMessage::TriggerPaste.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Duplicate".into(),
icon: Some("Copy".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::DuplicateSelectedLayers),
action: MenuBarEntry::create_action(|_| DocumentMessage::DuplicateSelectedLayers.into()),
disabled: no_active_document || !has_selected_nodes,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Delete".into(),
icon: Some("Trash".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers),
action: MenuBarEntry::create_action(|_| DocumentMessage::DeleteSelectedLayers.into()),
disabled: no_active_document || !has_selected_nodes,
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Convert to Infinite Canvas".into(),
icon: Some("Artboard".into()),
action: MenuBarEntry::create_action(|_| DocumentMessage::RemoveArtboards.into()),
disabled: no_active_document,
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Layer".into(),
no_active_document,
MenuBarEntryChildren(vec![
vec![MenuBarEntry {
label: "New".into(),
icon: Some("NewLayer".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder),
action: MenuBarEntry::create_action(|_| DocumentMessage::CreateEmptyFolder.into()),
disabled: no_active_document,
..MenuBarEntry::default()
}],
vec![
MenuBarEntry {
label: "Group".into(),
icon: Some("Folder".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers),
action: MenuBarEntry::create_action(|_| {
DocumentMessage::GroupSelectedLayers {
group_folder_type: GroupFolderType::Layer,
}
.into()
}),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Ungroup".into(),
icon: Some("FolderOpen".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::UngroupSelectedLayers),
action: MenuBarEntry::create_action(|_| DocumentMessage::UngroupSelectedLayers.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Hide/Show".into(),
icon: Some("EyeHide".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::ToggleSelectedVisibility),
action: MenuBarEntry::create_action(|_| DocumentMessage::ToggleSelectedVisibility.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Lock/Unlock".into(),
icon: Some("PadlockLocked".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::ToggleSelectedLocked),
action: MenuBarEntry::create_action(|_| DocumentMessage::ToggleSelectedLocked.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Grab".into(),
icon: Some("TransformationGrab".into()),
shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginGrab),
action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginGrab.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Rotate".into(),
icon: Some("TransformationRotate".into()),
shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginRotate),
action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginRotate.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Scale".into(),
icon: Some("TransformationScale".into()),
shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginScale),
action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginScale.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Arrange".into(),
icon: Some("StackHollow".into()),
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Raise To Front".into(),
icon: Some("Stack".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Raise".into(),
icon: Some("StackRaise".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Lower".into(),
icon: Some("StackLower".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLower),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Lower to Back".into(),
icon: Some("StackBottom".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Reverse".into(),
icon: Some("StackReverse".into()),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersReverse.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
}],
]),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Align".into(),
icon: Some("AlignVerticalCenter".into()),
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren({
let choices = [
[
(AlignAxis::X, AlignAggregate::Min, "AlignLeft", "Align Left"),
(AlignAxis::X, AlignAggregate::Center, "AlignHorizontalCenter", "Align Horizontal Center"),
(AlignAxis::X, AlignAggregate::Max, "AlignRight", "Align Right"),
],
[
(AlignAxis::Y, AlignAggregate::Min, "AlignTop", "Align Top"),
(AlignAxis::Y, AlignAggregate::Center, "AlignVerticalCenter", "Align Vertical Center"),
(AlignAxis::Y, AlignAggregate::Max, "AlignBottom", "Align Bottom"),
],
];
choices
.into_iter()
.map(|section| {
section
.into_iter()
.map(|(axis, aggregate, icon, name)| MenuBarEntry {
label: name.into(),
icon: Some(icon.into()),
action: MenuBarEntry::create_action(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
})
.collect()
})
.collect()
}),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Flip".into(),
icon: Some("FlipVertical".into()),
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{
[(FlipAxis::X, "FlipHorizontal", "Horizontal"), (FlipAxis::Y, "FlipVertical", "Vertical")]
.into_iter()
.map(|(flip_axis, icon, name)| MenuBarEntry {
label: name.into(),
icon: Some(icon.into()),
action: MenuBarEntry::create_action(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
})
.collect()
}]),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Turn".into(),
icon: Some("TurnPositive90".into()),
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{
[(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")]
.into_iter()
.map(|(degrees, icon, name)| MenuBarEntry {
label: name.into(),
icon: Some(icon.into()),
action: MenuBarEntry::create_action(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
})
.collect()
}]),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Boolean".into(),
icon: Some("BooleanSubtractFront".into()),
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
list.iter()
.flat_map(|i| i.iter())
.map(move |(operation, info)| MenuBarEntry {
label: info.label.to_string(),
icon: info.icon.as_ref().map(|i| i.to_string()),
action: MenuBarEntry::create_action(move |_| {
let group_folder_type = GroupFolderType::BooleanOperation(*operation);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
}),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
})
.collect()
}]),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Make Path Editable".into(),
icon: Some("NodeShape".into()),
shortcut: None,
action: MenuBarEntry::create_action(|_| NodeGraphMessage::AddPathNode.into()),
disabled: !make_path_editable_is_allowed,
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Select".into(),
no_active_document,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Select All".into(),
icon: Some("SelectAll".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectAllLayers),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Deselect All".into(),
icon: Some("DeselectAll".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::DeselectAllLayers),
action: MenuBarEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()),
disabled: no_active_document || !has_selected_nodes,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Select Parent".into(),
icon: Some("SelectParent".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectParentLayer),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectParentLayer.into()),
disabled: no_active_document || !has_selected_nodes,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Previous Selection".into(),
icon: Some("HistoryUndo".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectionStepBack),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectionStepBack.into()),
disabled: !has_selection_history.0,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Next Selection".into(),
icon: Some("HistoryRedo".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectionStepForward),
action: MenuBarEntry::create_action(|_| DocumentMessage::SelectionStepForward.into()),
disabled: !has_selection_history.1,
..MenuBarEntry::default()
},
],
]),
),
MenuBarEntry::new_root(
"View".into(),
no_active_document,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Tilt".into(),
icon: Some("Tilt".into()),
shortcut: action_keys!(NavigationMessageDiscriminant::BeginCanvasTilt),
action: MenuBarEntry::create_action(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into()),
disabled: no_active_document || node_graph_open,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Reset Tilt".into(),
icon: Some("TiltReset".into()),
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasTiltSet),
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()),
disabled: no_active_document || node_graph_open || !self.canvas_tilted,
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {
label: "Zoom In".into(),
icon: Some("ZoomIn".into()),
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease),
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Zoom Out".into(),
icon: Some("ZoomOut".into()),
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasZoomDecrease),
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Zoom to Selection".into(),
icon: Some("FrameSelected".into()),
shortcut: action_keys!(NavigationMessageDiscriminant::FitViewportToSelection),
action: MenuBarEntry::create_action(|_| NavigationMessage::FitViewportToSelection.into()),
disabled: no_active_document || !has_selected_layers,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Zoom to Fit".into(),
icon: Some("FrameAll".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasToFitAll),
action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasToFitAll.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Zoom to 100%".into(),
icon: Some("Zoom1x".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent),
action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasTo100Percent.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Zoom to 200%".into(),
icon: Some("Zoom2x".into()),
shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo200Percent),
action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasTo200Percent.into()),
disabled: no_active_document,
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Flip".into(),
icon: Some(if self.canvas_flipped { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasFlip),
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasFlip.into()),
disabled: no_active_document || node_graph_open,
..MenuBarEntry::default()
}],
vec![MenuBarEntry {
label: "Rulers".into(),
icon: Some(if self.rulers_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleRulers),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleRulers.into()),
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: "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(),
false,
MenuBarEntryChildren(vec![
vec![MenuBarEntry {
label: "About Graphite…".into(),
icon: Some("GraphiteLogo".into()),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()),
..MenuBarEntry::default()
}],
vec![
MenuBarEntry {
label: "Donate to Graphite".into(),
icon: Some("Heart".into()),
action: MenuBarEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.rs/donate/".into(),
}
.into()
}),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "User Manual".into(),
icon: Some("UserManual".into()),
action: MenuBarEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://graphite.rs/learn/".into(),
}
.into()
}),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Report a Bug".into(),
icon: Some("Bug".into()),
action: MenuBarEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
}),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Visit on GitHub".into(),
icon: Some("Website".into()),
action: MenuBarEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite".into(),
}
.into()
}),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Developer Debug".into(),
icon: Some("Code".into()),
action: MenuBarEntry::no_action(),
children: MenuBarEntryChildren(vec![
vec![MenuBarEntry {
label: "Reset Nodes to Definitions on Open".into(),
icon: Some(if reset_node_definitions_on_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()),
..MenuBarEntry::default()
}],
vec![
MenuBarEntry {
label: "Print Trace Logs".into(),
icon: Some(if log::max_level() == log::LevelFilter::Trace { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Print Messages: Off".into(),
icon: message_logging_verbosity_off.then_some("SmallDot".into()),
shortcut: action_keys!(DebugMessageDiscriminant::MessageOff),
action: MenuBarEntry::create_action(|_| DebugMessage::MessageOff.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Print Messages: Only Names".into(),
icon: message_logging_verbosity_names.then_some("SmallDot".into()),
shortcut: action_keys!(DebugMessageDiscriminant::MessageNames),
action: MenuBarEntry::create_action(|_| DebugMessage::MessageNames.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Print Messages: Full Contents".into(),
icon: message_logging_verbosity_contents.then_some("SmallDot".into()),
shortcut: action_keys!(DebugMessageDiscriminant::MessageContents),
action: MenuBarEntry::create_action(|_| DebugMessage::MessageContents.into()),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Trigger a Crash".into(),
icon: Some("Warning".into()),
action: MenuBarEntry::create_action(|_| panic!()),
..MenuBarEntry::default()
}],
]),
..MenuBarEntry::default()
}],
]),
),
];
Layout::MenuLayout(MenuLayout::new(menu_bar_entries))
}
}
-1
View File
@@ -3,7 +3,6 @@ mod portfolio_message_handler;
pub mod document;
pub mod document_migration;
pub mod menu_bar;
pub mod utility_types;
#[doc(inline)]
@@ -2,6 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::utility_types::PanelType;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::utility_types::FontCatalog;
use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::raster::Image;
@@ -13,8 +14,6 @@ use std::path::PathBuf;
pub enum PortfolioMessage {
// Sub-messages
#[child]
MenuBar(MenuBarMessage),
#[child]
Document(DocumentMessage),
// Messages
@@ -48,24 +47,34 @@ pub enum PortfolioMessage {
},
DestroyAllDocuments,
EditorPreferences,
FontCatalogLoaded {
catalog: FontCatalog,
},
LoadFontData {
font: Font,
},
FontLoaded {
font_family: String,
font_style: String,
preview_url: String,
data: Vec<u8>,
},
Import,
LoadDocumentResources {
document_id: DocumentId,
},
LoadFont {
font: Font,
},
NewDocumentWithName {
name: String,
},
NextDocument,
OpenDocument,
Open,
Import,
OpenFile {
path: PathBuf,
content: Vec<u8>,
},
ImportFile {
path: PathBuf,
content: Vec<u8>,
},
OpenDocumentFile {
document_name: Option<String>,
document_path: Option<PathBuf>,
@@ -79,12 +88,15 @@ pub enum PortfolioMessage {
document_is_saved: bool,
document_serialized_content: String,
to_front: bool,
select_after_open: bool,
},
ToggleResetNodesToDefinitionsOnOpen,
PasteIntoFolder {
clipboard: Clipboard,
parent: LayerNodeIdentifier,
insert_index: usize,
OpenImage {
name: Option<String>,
image: Image<Color>,
},
OpenSvg {
name: Option<String>,
svg: String,
},
PasteSerializedData {
data: String,
@@ -92,9 +104,6 @@ pub enum PortfolioMessage {
PasteSerializedVector {
data: String,
},
CenterPastedLayers {
layers: Vec<LayerNodeIdentifier>,
},
PasteImage {
name: Option<String>,
image: Image<Color>,
@@ -107,13 +116,21 @@ pub enum PortfolioMessage {
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
// TODO: Unused except by tests, remove?
PasteIntoFolder {
clipboard: Clipboard,
parent: LayerNodeIdentifier,
insert_index: usize,
},
CenterPastedLayers {
layers: Vec<LayerNodeIdentifier>,
},
PrevDocument,
RequestWelcomeScreenButtonsLayout,
RequestStatusBarInfoLayout,
SetActivePanel {
panel: PanelType,
},
SetDevicePixelRatio {
ratio: f64,
},
SelectDocument {
document_id: DocumentId,
},
@@ -123,12 +140,17 @@ pub enum PortfolioMessage {
scale_factor: f64,
bounds: ExportBounds,
transparent_background: bool,
artboard_name: Option<String>,
artboard_count: usize,
},
SubmitActiveGraphRender,
SubmitGraphRender {
document_id: DocumentId,
ignore_hash: bool,
},
SubmitEyedropperPreviewRender,
ToggleResetNodesToDefinitionsOnOpen,
ToggleFocusDocument,
ToggleDataPanelOpen,
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,
File diff suppressed because it is too large Load Diff
+85 -25
View File
@@ -1,46 +1,94 @@
use graphene_std::text::FontCache;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::text::{Font, FontCache};
#[derive(Debug, Default)]
pub struct PersistentData {
pub font_cache: FontCache,
pub font_catalog: FontCatalog,
pub use_vello: bool,
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum Platform {
#[default]
Unknown,
Windows,
Mac,
Linux,
}
// TODO: Should this be a BTreeMap instead?
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalog(pub Vec<FontCatalogFamily>);
impl Platform {
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
match self {
Platform::Mac => KeyboardPlatformLayout::Mac,
Platform::Windows | Platform::Linux => KeyboardPlatformLayout::Standard,
Platform::Unknown => {
warn!("The platform has not been set, remember to send `GlobalsMessage::SetPlatform` during editor initialization.");
KeyboardPlatformLayout::Standard
}
impl FontCatalog {
pub fn find_font_style_in_catalog(&self, font: &Font) -> Option<FontCatalogStyle> {
let family = self.0.iter().find(|family| family.name == font.font_family);
let found_style = family.map(|family| {
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
family.closest_style(weight, italic).clone()
});
if found_style.is_none() {
log::warn!("Font not found in catalog: {:?}", font);
}
found_style
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum KeyboardPlatformLayout {
/// Standard keyboard mapping used by Windows and Linux
#[default]
Standard,
/// Keyboard mapping used by Macs where Command is sometimes used in favor of Control
Mac,
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogFamily {
/// The font family name.
pub name: String,
/// The font styles (variants) available for the font family.
pub styles: Vec<FontCatalogStyle>,
}
impl FontCatalogFamily {
/// Finds the closest style to the given weight and italic setting.
/// Aims to find the nearest weight while maintaining the italic setting if possible, but italic may change if no other option is available.
pub fn closest_style(&self, weight: u32, italic: bool) -> &FontCatalogStyle {
self.styles
.iter()
.map(|style| ((style.weight as i32 - weight as i32).unsigned_abs() + 10000 * (style.italic != italic) as u32, style))
.min_by_key(|(distance, _)| *distance)
.map(|(_, style)| style)
.unwrap_or(&self.styles[0])
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogStyle {
pub weight: u32,
pub italic: bool,
pub url: String,
}
impl FontCatalogStyle {
pub fn to_named_style(&self) -> String {
let weight = self.weight;
let italic = self.italic;
let named_weight = Font::named_weight(weight);
let maybe_italic = if italic { " Italic" } else { "" };
format!("{named_weight}{maybe_italic} ({weight})")
}
pub fn from_named_style(named_style: &str, url: impl Into<String>) -> FontCatalogStyle {
let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::<u32>().ok()).unwrap_or(400);
let italic = named_style.contains("Italic (");
FontCatalogStyle { weight, italic, url: url.into() }
}
/// Get the URL for the stylesheet for loading a font preview for this style of the given family name, subsetted to only the letters in the family name.
pub fn preview_url(&self, family: impl Into<String>) -> String {
let name = family.into().replace(' ', "+");
let italic = if self.italic { "ital," } else { "" };
let weight = self.weight;
format!("https://fonts.googleapis.com/css2?display=swap&family={name}:{italic}wght@{weight}&text={name}")
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum PanelType {
#[default]
Document,
Welcome,
Layers,
Properties,
DataPanel,
@@ -50,6 +98,7 @@ impl From<String> for PanelType {
fn from(value: String) -> Self {
match value.as_str() {
"Document" => PanelType::Document,
"Welcome" => PanelType::Welcome,
"Layers" => PanelType::Layers,
"Properties" => PanelType::Properties,
"Data" => PanelType::DataPanel,
@@ -57,3 +106,14 @@ impl From<String> for PanelType {
}
}
}
pub enum FileContent {
/// A Graphite document.
Document(String),
/// A bitmap image.
Image(Image<Color>),
/// An SVG file string.
Svg(String),
/// Any other unsupported/unrecognized file type.
Unsupported,
}
+1 -1
View File
@@ -1,5 +1,5 @@
mod preferences_message;
mod preferences_message_handler;
pub mod preferences_message_handler;
pub mod utility_types;
#[doc(inline)]
@@ -6,14 +6,15 @@ use crate::messages::prelude::*;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum PreferencesMessage {
// Management messages
Load { preferences: String },
Load { preferences: Option<PreferencesMessageHandler> },
ResetToDefaults,
// Per-preference messages
UseVello { use_vello: bool },
SelectionMode { selection_mode: SelectionMode },
VectorMeshes { enabled: bool },
BrushTool { enabled: bool },
ModifyLayout { zoom_with_scroll: bool },
GraphWireStyle { style: GraphWireStyle },
ViewportZoomWheelRate { rate: f64 },
UIScale { scale: f64 },
}
@@ -1,18 +1,26 @@
use crate::consts::VIEWPORT_ZOOM_WHEEL_RATE;
use crate::consts::{UI_SCALE_DEFAULT, VIEWPORT_ZOOM_WHEEL_RATE};
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType;
use graph_craft::wasm_application_io::EditorPreferences;
#[derive(ExtractField)]
pub struct PreferencesMessageContext<'a> {
pub tool_message_handler: &'a ToolMessageHandler,
}
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)]
#[serde(default)]
pub struct PreferencesMessageHandler {
pub selection_mode: SelectionMode,
pub zoom_with_scroll: bool,
pub use_vello: bool,
pub vector_meshes: bool,
pub brush_tool: bool,
pub graph_wire_style: GraphWireStyle,
pub viewport_zoom_wheel_rate: f64,
pub ui_scale: f64,
}
impl PreferencesMessageHandler {
@@ -37,28 +45,32 @@ impl Default for PreferencesMessageHandler {
selection_mode: SelectionMode::Touched,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello: EditorPreferences::default().use_vello,
vector_meshes: false,
brush_tool: false,
graph_wire_style: GraphWireStyle::default(),
viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE,
ui_scale: UI_SCALE_DEFAULT,
}
}
}
#[message_handler_data]
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _: ()) {
impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for PreferencesMessageHandler {
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, context: PreferencesMessageContext) {
let PreferencesMessageContext { tool_message_handler } = context;
match message {
// Management messages
PreferencesMessage::Load { preferences } => {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences;
responses.add(PortfolioMessage::EditorPreferences);
responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PreferencesMessage::ModifyLayout {
zoom_with_scroll: self.zoom_with_scroll,
});
if let Some(preferences) = preferences {
*self = preferences;
}
responses.add(PortfolioMessage::EditorPreferences);
responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PreferencesMessage::ModifyLayout {
zoom_with_scroll: self.zoom_with_scroll,
});
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
}
PreferencesMessage::ResetToDefaults => {
refresh_dialog(responses);
@@ -73,8 +85,14 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PortfolioMessage::EditorPreferences);
}
PreferencesMessage::VectorMeshes { enabled } => {
self.vector_meshes = enabled;
PreferencesMessage::BrushTool { enabled } => {
self.brush_tool = enabled;
if !enabled && tool_message_handler.tool_state.tool_data.active_tool_type == ToolType::Brush {
responses.add(ToolMessage::ActivateToolSelect);
}
responses.add(ToolMessage::RefreshToolShelf);
}
PreferencesMessage::ModifyLayout { zoom_with_scroll } => {
self.zoom_with_scroll = zoom_with_scroll;
@@ -93,6 +111,10 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
PreferencesMessage::ViewportZoomWheelRate { rate } => {
self.viewport_zoom_wheel_rate = rate;
}
PreferencesMessage::UIScale { scale } => {
self.ui_scale = scale;
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
}
}
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
@@ -19,9 +19,9 @@ impl std::fmt::Display for SelectionMode {
impl SelectionMode {
pub fn tooltip_description(&self) -> &'static str {
match self {
SelectionMode::Touched => "Select all layers at least partially covered by the dragged selection area",
SelectionMode::Enclosed => "Select only layers fully enclosed by the dragged selection area",
SelectionMode::Directional => r#""Touched" for leftward drags, "Enclosed" for rightward drags"#,
SelectionMode::Touched => "Select all layers at least partially covered by the dragged selection area.",
SelectionMode::Enclosed => "Select only layers fully enclosed by the dragged selection area.",
SelectionMode::Directional => r#""Touched" for leftward drags, "Enclosed" for rightward drags."#,
}
}
}
+3 -3
View File
@@ -7,6 +7,7 @@ pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscrimin
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::clipboard::{ClipboardMessage, ClipboardMessageDiscriminant, ClipboardMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};
@@ -14,11 +15,11 @@ pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage,
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler};
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler};
pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler};
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::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
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};
@@ -26,11 +27,11 @@ pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, Nod
pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler};
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::{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::viewport::{ViewportMessage, ViewportMessageDiscriminant, ViewportMessageHandler};
// Message, MessageDiscriminant
pub use crate::messages::message::{Message, MessageDiscriminant};
@@ -50,7 +51,6 @@ pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, S
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
// Helper/miscellaneous
pub use crate::messages::globals::global_variables::*;
pub use crate::messages::portfolio::document::utility_types::misc::DocumentId;
pub use graphite_proc_macros::*;
pub use std::collections::{HashMap, HashSet, VecDeque};
@@ -34,9 +34,9 @@ impl AutoPanning {
}
}
pub fn setup_by_mouse_position(&mut self, input: &InputPreprocessorMessageHandler, messages: &[Message], responses: &mut VecDeque<Message>) {
pub fn setup_by_mouse_position(&mut self, input: &InputPreprocessorMessageHandler, viewport: &ViewportMessageHandler, messages: &[Message], responses: &mut VecDeque<Message>) {
let mouse_position = input.mouse.position;
let viewport_size = input.viewport_bounds.size();
let viewport_size = viewport.size().into_dvec2();
let is_pointer_outside_edge = mouse_position.x < 0. || mouse_position.x > viewport_size.x || mouse_position.y < 0. || mouse_position.y > viewport_size.y;
match is_pointer_outside_edge {
@@ -50,12 +50,12 @@ impl AutoPanning {
/// If the mouse was beyond any edge, it returns the amount shifted. Otherwise it returns None.
/// The shift is proportional to the distance between edge and mouse, and to the duration of the frame.
/// It is also guaranteed to be integral.
pub fn shift_viewport(&self, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) -> Option<DVec2> {
pub fn shift_viewport(&self, input: &InputPreprocessorMessageHandler, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<DVec2> {
if !self.subscribed_to_animation_frame {
return None;
}
let viewport_size = input.viewport_bounds.size();
let viewport_size = viewport.size().into_dvec2();
let mouse_position = input.mouse.position.clamp(
DVec2::ZERO - DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS),
viewport_size + DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS),
@@ -79,20 +79,20 @@ impl ToolColorOptions {
reset_callback: impl Fn(&IconButton) -> Message + 'static + Send + Sync,
radio_callback: fn(ToolColorType) -> WidgetCallback<()>,
color_callback: impl Fn(&ColorInput) -> Message + 'static + Send + Sync,
) -> Vec<WidgetHolder> {
let mut widgets = vec![TextLabel::new(label_text).widget_holder()];
) -> Vec<WidgetInstance> {
let mut widgets = vec![TextLabel::new(label_text).widget_instance()];
if !color_allow_none {
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
} else {
let reset = IconButton::new("CloseX", 12)
.disabled(self.custom_color.is_none() && self.color_type == ToolColorType::Custom)
.tooltip("Clear Color")
.tooltip_label("Clear Color")
.on_update(reset_callback);
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
widgets.push(reset.widget_holder());
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
widgets.push(reset.widget_instance());
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
};
let entries = vec![
@@ -101,22 +101,22 @@ impl ToolColorOptions {
("CustomColor", "Custom Color", ToolColorType::Custom),
]
.into_iter()
.map(|(icon, tooltip, color_type)| {
let mut entry = RadioEntryData::new(format!("{color_type:?}")).tooltip(tooltip).icon(icon);
.map(|(icon, label, color_type)| {
let mut entry = RadioEntryData::new(format!("{color_type:?}")).tooltip_label(label).icon(icon);
entry.on_update = radio_callback(color_type);
entry
})
.collect();
let radio = RadioInput::new(entries).selected_index(Some(self.color_type.clone() as u32)).widget_holder();
let radio = RadioInput::new(entries).selected_index(Some(self.color_type.clone() as u32)).widget_instance();
widgets.push(radio);
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
let fill_choice = match self.active_color() {
Some(color) => FillChoice::Solid(color.to_gamma_srgb()),
None => FillChoice::None,
};
let color_button = ColorInput::new(fill_choice).allow_none(color_allow_none).on_update(color_callback);
widgets.push(color_button.widget_holder());
widgets.push(color_button.widget_instance());
widgets
}
@@ -6,6 +6,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::operations::circular_repeat::CircularRepeatGizmoHandler;
use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler;
use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler;
use crate::messages::tool::common_functionality::shapes::grid_shape::GridGizmoHandler;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::{GizmoContext, ShapeGizmoHandler};
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
@@ -28,6 +29,7 @@ pub enum ShapeGizmoHandlers {
Arc(ArcGizmoHandler),
Circle(CircleGizmoHandler),
CircularRepeat(CircularRepeatGizmoHandler),
Grid(GridGizmoHandler),
}
impl ShapeGizmoHandlers {
@@ -40,6 +42,7 @@ impl ShapeGizmoHandlers {
Self::Arc(_) => "arc",
Self::Circle(_) => "circle",
Self::CircularRepeat(_) => "circular_repeat",
Self::Grid(_) => "grid",
Self::None => "none",
}
}
@@ -52,6 +55,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_state(layer, mouse_position, ctx),
Self::Circle(h) => h.handle_state(layer, mouse_position, ctx),
Self::CircularRepeat(h) => h.handle_state(layer, mouse_position, ctx),
Self::Grid(h) => h.handle_state(layer, mouse_position, ctx),
Self::None => {}
}
}
@@ -64,6 +68,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.is_any_gizmo_hovered(),
Self::Circle(h) => h.is_any_gizmo_hovered(),
Self::CircularRepeat(h) => h.is_any_gizmo_hovered(),
Self::Grid(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
@@ -76,6 +81,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_click(),
Self::Circle(h) => h.handle_click(),
Self::CircularRepeat(h) => h.handle_click(),
Self::Grid(h) => h.handle_click(),
Self::None => {}
}
}
@@ -88,6 +94,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_update(drag_start, ctx),
Self::Circle(h) => h.handle_update(drag_start, ctx),
Self::CircularRepeat(h) => h.handle_update(drag_start, ctx),
Self::Grid(h) => h.handle_update(drag_start, ctx),
Self::None => {}
}
}
@@ -99,6 +106,7 @@ impl ShapeGizmoHandlers {
Self::Polygon(h) => h.cleanup(),
Self::Arc(h) => h.cleanup(),
Self::Circle(h) => h.cleanup(),
Self::Grid(h) => h.cleanup(),
Self::CircularRepeat(h) => h.cleanup(),
Self::None => {}
}
@@ -112,6 +120,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.overlays(layer, mouse_position, ctx, overlay_context),
Self::Circle(h) => h.overlays(layer, mouse_position, ctx, overlay_context),
Self::CircularRepeat(h) => h.overlays(layer, mouse_position, ctx, overlay_context),
Self::Grid(h) => h.overlays(layer, mouse_position, ctx, overlay_context),
Self::None => {}
}
}
@@ -124,6 +133,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.dragging_overlays(mouse_position, ctx, overlay_context),
Self::Circle(h) => h.dragging_overlays(mouse_position, ctx, overlay_context),
Self::CircularRepeat(h) => h.dragging_overlays(mouse_position, ctx, overlay_context),
Self::Grid(h) => h.dragging_overlays(mouse_position, ctx, overlay_context),
Self::None => {}
}
}
@@ -135,6 +145,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.mouse_cursor_icon(),
Self::Circle(h) => h.mouse_cursor_icon(),
Self::CircularRepeat(h) => h.mouse_cursor_icon(),
Self::Grid(h) => h.mouse_cursor_icon(),
Self::None => None,
}
}
@@ -178,6 +189,10 @@ impl GizmoManager {
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default()));
}
// Grid
if graph_modification_utils::get_grid_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Grid(GridGizmoHandler::default()));
}
None
}
@@ -191,7 +206,6 @@ impl GizmoManager {
None
}
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
pub fn hovering_over_gizmo(&self) -> bool {
self.active_shape_handler.is_some()
@@ -51,7 +51,7 @@ impl RadiusHandle {
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
let circle_point = calculate_circle_point_position(angle, radius.abs());
let direction = circle_point.normalize();
let Some(direction) = circle_point.try_normalize() else { return false };
let mouse_distance = mouse_position.distance(center);
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
@@ -0,0 +1,441 @@
use crate::consts::GRID_ROW_COLUMN_GIZMO_OFFSET;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::prelude::{GraphOperationMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{GizmoContext, extract_grid_parameters};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::NodeInputDecleration;
use graphene_std::vector::misc::{GridType, dvec2_to_point, get_line_endpoints};
use kurbo::{Line, ParamCurveNearest, Rect};
use std::collections::VecDeque;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum RowColumnGizmoState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct RowColumnGizmo {
pub layer: Option<LayerNodeIdentifier>,
pub gizmo_type: RowColumnGizmoType,
initial_rows: u32,
initial_columns: u32,
spacing: DVec2,
initial_mouse_start: Option<DVec2>,
gizmo_state: RowColumnGizmoState,
}
impl RowColumnGizmo {
pub fn cleanup(&mut self) {
self.layer = None;
self.gizmo_state = RowColumnGizmoState::Inactive;
self.initial_mouse_start = None;
}
pub fn update_state(&mut self, state: RowColumnGizmoState) {
self.gizmo_state = state;
}
pub fn is_hovered(&self) -> bool {
self.gizmo_state == RowColumnGizmoState::Hover
}
pub fn is_dragging(&self) -> bool {
self.gizmo_state == RowColumnGizmoState::Dragging
}
fn initial_dimension(&self) -> u32 {
match &self.gizmo_type {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => self.initial_rows,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => self.initial_columns,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, ctx: &mut GizmoContext, mouse_position: DVec2) {
let GizmoContext { document, .. } = ctx;
let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
if let Some(gizmo_type) = check_if_over_gizmo(grid_type, columns, rows, spacing, angles, mouse_position, viewport) {
self.layer = Some(layer);
self.gizmo_type = gizmo_type;
self.initial_rows = rows;
self.initial_columns = columns;
self.spacing = spacing;
self.initial_mouse_start = None;
self.update_state(RowColumnGizmoState::Hover);
}
}
pub fn overlays(&self, layer: Option<LayerNodeIdentifier>, ctx: &mut GizmoContext, _mouse_position: DVec2, overlay_context: &mut OverlayContext) {
let GizmoContext { document, .. } = ctx;
let Some(layer) = layer.or(self.layer) else { return };
let Some((grid_type, spacing, columns, rows, angles)) = extract_grid_parameters(layer, document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
if !matches!(self.gizmo_state, RowColumnGizmoState::Inactive) {
let line = self.gizmo_type.line(grid_type, columns, rows, spacing, angles, viewport);
let (p0, p1) = get_line_endpoints(line);
overlay_context.dashed_line(p0, p1, None, None, Some(5.), Some(5.), Some(0.5));
}
}
pub fn update(&mut self, ctx: &mut GizmoContext, drag_start: DVec2) {
let GizmoContext { document, input, responses, .. } = ctx;
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let Some((grid_type, _, columns, rows, angles)) = extract_grid_parameters(layer, document) else {
return;
};
let direction = self.gizmo_type.direction(viewport);
let delta_vector = input.mouse.position - self.initial_mouse_start.unwrap_or(drag_start);
let projection = delta_vector.project_onto(self.gizmo_type.direction(viewport));
let delta = viewport.inverse().transform_vector2(projection).length() * delta_vector.dot(direction).signum();
if delta.abs() < 1e-6 {
return;
}
let dimensions_to_add = (delta / (self.gizmo_type.spacing(self.spacing, grid_type, angles))).floor() as i32;
let new_dimension = (self.initial_dimension() as i32 + dimensions_to_add).max(1) as u32;
let Some(node_id) = graph_modification_utils::get_grid_id(layer, &document.network_interface) else {
return;
};
let dimensions_delta = new_dimension as i32 - self.gizmo_type.initial_dimension(rows, columns) as i32;
let transform = self.transform_grid(dimensions_delta, self.spacing, grid_type, angles, viewport);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, self.gizmo_type.index()),
input: NodeInput::value(TaggedValue::U32((self.initial_dimension() as i32 + dimensions_to_add).max(1) as u32), false),
});
responses.add(GraphOperationMessage::TransformChange {
layer,
transform,
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
if self.initial_dimension() as i32 + dimensions_to_add < 1 {
self.initial_mouse_start = Some(input.mouse.position);
self.gizmo_type = self.gizmo_type.opposite_gizmo_type();
self.initial_rows = 1;
self.initial_columns = 1;
}
}
fn transform_grid(&self, dimensions_delta: i32, spacing: DVec2, grid_type: GridType, angles: DVec2, viewport: DAffine2) -> DAffine2 {
match &self.gizmo_type {
RowColumnGizmoType::Top => {
let move_up_by = self.gizmo_type.direction(viewport) * dimensions_delta as f64 * spacing.y;
DAffine2::from_translation(move_up_by)
}
RowColumnGizmoType::Left => {
let move_left_by = self.gizmo_type.direction(viewport) * dimensions_delta as f64 * self.gizmo_type.spacing(spacing, grid_type, angles);
DAffine2::from_translation(move_left_by)
}
RowColumnGizmoType::Bottom | RowColumnGizmoType::Right | RowColumnGizmoType::None => DAffine2::IDENTITY,
}
}
}
fn check_if_over_gizmo(grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, mouse_position: DVec2, viewport: DAffine2) -> Option<RowColumnGizmoType> {
let mouse_point = dvec2_to_point(mouse_position);
let accuracy = 1e-6;
let threshold = 32.;
for gizmo_type in RowColumnGizmoType::all() {
let line = gizmo_type.line(grid_type, columns, rows, spacing, angles, viewport);
let rect = gizmo_type.rect(grid_type, columns, rows, spacing, angles, viewport);
if rect.contains(mouse_point) || line.nearest(mouse_point, accuracy).distance_sq < threshold {
return Some(gizmo_type);
}
}
None
}
fn convert_to_gizmo_line(p0: DVec2, p1: DVec2) -> Line {
Line {
p0: dvec2_to_point(p0),
p1: dvec2_to_point(p1),
}
}
/// Get corners of the rectangular-grid.
/// Returns a tuple of (topleft,topright,bottomright,bottomleft)
fn get_corners(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2, DVec2, DVec2) {
let (width, height) = (spacing.x, spacing.y);
let x_distance = (columns - 1) as f64 * width;
let y_distance = (rows - 1) as f64 * height;
let point0 = DVec2::ZERO;
let point1 = DVec2::new(x_distance, 0.);
let point2 = DVec2::new(x_distance, y_distance);
let point3 = DVec2::new(0., y_distance);
(point0, point1, point2, point3)
}
fn get_rectangle_top_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) {
let (top_left, top_right, _, _) = get_corners(columns, rows, spacing);
let offset = if columns == 1 || rows == 1 {
DVec2::ZERO
} else if columns == 2 {
DVec2::new(spacing.x * 0.25, 0.)
} else {
DVec2::new(spacing.x * 0.5, 0.)
};
(top_left + offset, top_right - offset)
}
fn get_rectangle_bottom_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) {
let (_, _, bottom_right, bottom_left) = get_corners(columns, rows, spacing);
let offset = if columns == 1 || rows == 1 {
DVec2::ZERO
} else if columns == 2 {
DVec2::new(spacing.x * 0.25, 0.)
} else {
DVec2::new(spacing.x * 0.5, 0.)
};
(bottom_left + offset, bottom_right - offset)
}
fn get_rectangle_right_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) {
let (_, top_right, bottom_right, _) = get_corners(columns, rows, spacing);
let offset = if columns == 1 || rows == 1 {
DVec2::ZERO
} else if rows == 2 {
DVec2::new(0., -spacing.y * 0.25)
} else {
DVec2::new(0., -spacing.y * 0.5)
};
(top_right - offset, bottom_right + offset)
}
fn get_rectangle_left_line_points(columns: u32, rows: u32, spacing: DVec2) -> (DVec2, DVec2) {
let (top_left, _, _, bottom_left) = get_corners(columns, rows, spacing);
let offset = if columns == 1 || rows == 1 {
DVec2::ZERO
} else if rows == 2 {
DVec2::new(0., -spacing.y * 0.25)
} else {
DVec2::new(0., -spacing.y * 0.5)
};
(top_left - offset, bottom_left + offset)
}
fn calculate_isometric_point(column: u32, row: u32, angles: DVec2, spacing: DVec2) -> DVec2 {
let tan_a = angles.x.to_radians().tan();
let tan_b = angles.y.to_radians().tan();
let spacing = DVec2::new(spacing.y / (tan_a + tan_b), spacing.y);
let a_angles_eaten = column.div_ceil(2) as f64;
let b_angles_eaten = (column / 2) as f64;
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
DVec2::new(spacing.x * column as f64, spacing.y * row as f64 + offset_y_fraction * spacing.x)
}
fn calculate_isometric_top_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) {
let top_left = calculate_isometric_point(0, 0, angles, spacing);
let top_right = calculate_isometric_point(columns - 1, 0, angles, spacing);
let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) };
let isometric_spacing = calculate_isometric_offset(spacing, angles);
let isometric_offset = DVec2::new(0., isometric_spacing.y);
let end_isometric_offset = if columns.is_multiple_of(2) { DVec2::ZERO } else { DVec2::new(0., isometric_spacing.y) };
(top_left + offset - isometric_offset, top_right - offset - end_isometric_offset)
}
fn calculate_isometric_bottom_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) {
let bottom_left = calculate_isometric_point(0, rows - 1, angles, spacing);
let bottom_right = calculate_isometric_point(columns - 1, rows - 1, angles, spacing);
let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(spacing.x * 0.5, 0.) };
let isometric_offset = if columns.is_multiple_of(2) {
let offset = calculate_isometric_offset(spacing, angles);
DVec2::new(0., offset.y)
} else {
DVec2::ZERO
};
(bottom_left + offset, bottom_right - offset + isometric_offset)
}
fn calculate_isometric_offset(spacing: DVec2, angles: DVec2) -> DVec2 {
let first_point = calculate_isometric_point(0, 0, angles, spacing);
let second_point = calculate_isometric_point(1, 0, angles, spacing);
DVec2::new(first_point.x - second_point.x, first_point.y - second_point.y)
}
fn calculate_isometric_right_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) {
let top_right = calculate_isometric_point(columns - 1, 0, angles, spacing);
let bottom_right = calculate_isometric_point(columns - 1, rows - 1, angles, spacing);
let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) };
(top_right - offset, bottom_right + offset)
}
fn calculate_isometric_left_line_points(columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) {
let top_left = calculate_isometric_point(0, 0, angles, spacing);
let bottom_left = calculate_isometric_point(0, rows - 1, angles, spacing);
let offset = if columns == 1 || rows == 1 { DVec2::ZERO } else { DVec2::new(0., -spacing.y * 0.5) };
(top_left - offset, bottom_left + offset)
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum RowColumnGizmoType {
#[default]
None,
Top,
Bottom,
Left,
Right,
}
impl RowColumnGizmoType {
pub fn get_line_points(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2) -> (DVec2, DVec2) {
match grid_type {
GridType::Rectangular => match self {
Self::Top => get_rectangle_top_line_points(columns, rows, spacing),
Self::Right => get_rectangle_right_line_points(columns, rows, spacing),
Self::Bottom => get_rectangle_bottom_line_points(columns, rows, spacing),
Self::Left => get_rectangle_left_line_points(columns, rows, spacing),
Self::None => panic!("RowColumnGizmoType::None does not have line points"),
},
GridType::Isometric => match self {
Self::Top => calculate_isometric_top_line_points(columns, rows, spacing, angles),
Self::Right => calculate_isometric_right_line_points(columns, rows, spacing, angles),
Self::Bottom => calculate_isometric_bottom_line_points(columns, rows, spacing, angles),
Self::Left => calculate_isometric_left_line_points(columns, rows, spacing, angles),
Self::None => panic!("RowColumnGizmoType::None does not have line points"),
},
}
}
fn line(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> Line {
let (p0, p1) = self.get_line_points(grid_type, columns, rows, spacing, angles);
let direction = self.direction(viewport);
let gap = GRID_ROW_COLUMN_GIZMO_OFFSET * viewport.inverse().transform_vector2(direction).normalize();
convert_to_gizmo_line(viewport.transform_point2(p0 + gap), viewport.transform_point2(p1 + gap))
}
fn rect(&self, grid_type: GridType, columns: u32, rows: u32, spacing: DVec2, angles: DVec2, viewport: DAffine2) -> Rect {
let (p0, p1) = self.get_line_points(grid_type, columns, rows, spacing, angles);
let direction = self.direction(viewport);
let gap = GRID_ROW_COLUMN_GIZMO_OFFSET * direction.normalize();
let (x0, x1) = match self {
Self::Top | Self::Left => (viewport.transform_point2(p0 + gap), viewport.transform_point2(p1)),
Self::Bottom | Self::Right => (viewport.transform_point2(p0), viewport.transform_point2(p1 + gap)),
Self::None => panic!("RowColumnGizmoType::None does not have opposite"),
};
Rect::new(x0.x, x0.y, x1.x, x1.y)
}
fn opposite_gizmo_type(&self) -> Self {
match self {
Self::Top => Self::Bottom,
Self::Right => Self::Left,
Self::Bottom => Self::Top,
Self::Left => Self::Right,
Self::None => panic!("RowColumnGizmoType::None does not have opposite"),
}
}
pub fn direction(&self, viewport: DAffine2) -> DVec2 {
match self {
RowColumnGizmoType::Top => viewport.transform_vector2(-DVec2::Y),
RowColumnGizmoType::Bottom => viewport.transform_vector2(DVec2::Y),
RowColumnGizmoType::Right => viewport.transform_vector2(DVec2::X),
RowColumnGizmoType::Left => viewport.transform_vector2(-DVec2::X),
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a line"),
}
}
fn initial_dimension(&self, rows: u32, columns: u32) -> u32 {
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => rows,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => columns,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
fn spacing(&self, spacing: DVec2, grid_type: GridType, angles: DVec2) -> f64 {
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => spacing.y,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => {
if grid_type == GridType::Rectangular {
spacing.x
} else {
spacing.y / (angles.x.to_radians().tan() + angles.y.to_radians().tan())
}
}
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
fn index(&self) -> usize {
use graphene_std::vector::generator_nodes::grid::*;
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => RowsInput::INDEX,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => ColumnsInput::INDEX,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
pub fn mouse_icon(&self) -> MouseCursorIcon {
match self {
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => MouseCursorIcon::NSResize,
RowColumnGizmoType::Left | RowColumnGizmoType::Right => MouseCursorIcon::EWResize,
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
}
}
pub fn all() -> [Self; 4] {
[Self::Top, Self::Right, Self::Bottom, Self::Left]
}
}

Some files were not shown because too many files have changed in this diff Show More