From e44f99309535a24d96a264296a8cca1f4733c5c4 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 15 Dec 2025 11:45:26 +0000 Subject: [PATCH 01/16] Desktop: Forward and Backward mouse button support (#3472) forward and backward mouse button support --- desktop/src/app.rs | 40 ++++++++++++++----- desktop/src/cef/input.rs | 2 +- .../src/handle_desktop_wrapper_message.rs | 3 ++ desktop/wrapper/src/messages.rs | 5 +++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 8fcb4077f..fd95ddf50 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,17 +1,13 @@ use rfd::AsyncFileDialog; use std::fs; use std::path::PathBuf; -use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; -use std::sync::mpsc::SyncSender; +use std::sync::mpsc::{Receiver, Sender, SyncSender}; use std::thread; -use std::time::Duration; -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; use winit::dpi::PhysicalSize; -use winit::event::WindowEvent; -use winit::event_loop::ActiveEventLoop; -use winit::event_loop::ControlFlow; +use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::window::WindowId; use crate::cef; @@ -20,7 +16,7 @@ use crate::event::{AppEvent, AppEventScheduler}; use crate::persist::PersistentData; use crate::render::{RenderError, RenderState}; use crate::window::Window; -use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; +use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Platform}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { @@ -520,6 +516,32 @@ impl ApplicationHandler for App { }; } } + + // Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor + WindowEvent::PointerButton { + button: ButtonSource::Mouse(button), + state: ElementState::Pressed, + .. + } => { + let mouse_keys = match button { + MouseButton::Back => Some(MouseKeys::BACK), + MouseButton::Forward => Some(MouseKeys::FORWARD), + _ => None, + }; + if let Some(mouse_keys) = mouse_keys { + let message = DesktopWrapperMessage::Input(InputMessage::PointerDown { + editor_mouse_state: MouseState { mouse_keys, ..Default::default() }, + modifier_keys: Default::default(), + }); + self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); + + let message = DesktopWrapperMessage::Input(InputMessage::PointerUp { + editor_mouse_state: Default::default(), + modifier_keys: Default::default(), + }); + self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); + } + } _ => {} } diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index d70550d0a..a764db916 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -45,7 +45,7 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat MouseButton::Left => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT), MouseButton::Right => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT), MouseButton::Middle => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE), - _ => return, //TODO: Handle Forward and Back button + _ => return, }; let Some(host) = browser.host() else { return }; diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 01daee633..b1857e9f8 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -12,6 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess DesktopWrapperMessage::FromWeb(message) => { dispatcher.queue_editor_message(*message); } + DesktopWrapperMessage::Input(message) => { + dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message)); + } DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { OpenFileDialogContext::Document => { dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 350bf5e70..78a0faca1 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -3,6 +3,10 @@ use std::path::PathBuf; pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; +pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; +pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys}; +pub use graphite_editor::messages::prelude::InputPreprocessorMessage as InputMessage; + pub use graphite_editor::messages::prelude::DocumentId; pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences; pub enum DesktopFrontendMessage { @@ -69,6 +73,7 @@ pub enum DesktopFrontendMessage { pub enum DesktopWrapperMessage { FromWeb(Box), + Input(InputMessage), OpenFileDialogResult { path: PathBuf, content: Vec, From 820865389cd9a6f1e478c1dabe63529c7e591a0f Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 15 Dec 2025 14:11:43 +0000 Subject: [PATCH 02/16] Desktop: UI scale preference (#3475) * ui scale preference * cleanup * add update ui scale message to SIDE_EFFECT_FREE_MESSAGES * fix mac title bar height * hide UI preference section on web * set % as unit of ui scale --- desktop/src/app.rs | 8 +- .../wrapper/src/intercept_frontend_message.rs | 4 + desktop/wrapper/src/messages.rs | 3 + editor/src/consts.rs | 5 + editor/src/dispatcher.rs | 1 + .../preferences_dialog_message_handler.rs | 461 ++++++++++-------- .../src/messages/frontend/frontend_message.rs | 3 + .../preferences/preferences_message.rs | 1 + .../preferences_message_handler.rs | 9 +- .../window/title-bar/TitleBar.svelte | 6 +- frontend/src/messages.ts | 5 + frontend/src/state-providers/app-window.ts | 9 +- 12 files changed, 296 insertions(+), 219 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index fd95ddf50..9aa45fb3c 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -27,6 +27,7 @@ pub(crate) struct App { window_size: PhysicalSize, window_maximized: bool, window_fullscreen: bool, + ui_scale: f64, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, desktop_wrapper: DesktopWrapper, @@ -83,6 +84,7 @@ impl App { window_size: PhysicalSize { width: 0, height: 0 }, window_maximized: false, window_fullscreen: false, + ui_scale: 1., app_event_receiver, app_event_scheduler, desktop_wrapper: DesktopWrapper::new(), @@ -119,7 +121,7 @@ impl App { } let size = window.surface_size(); - let scale = window.scale_factor(); + let scale = window.scale_factor() * self.ui_scale; let is_new_size = size != self.window_size; let is_new_scale = scale != self.window_scale; @@ -228,6 +230,10 @@ impl App { render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]); } } + DesktopFrontendMessage::UpdateUIScale { scale } => { + self.ui_scale = scale; + self.resize(); + } DesktopFrontendMessage::UpdateOverlays(scene) => { if let Some(render_state) = &mut self.render_state { render_state.set_overlays_scene(scene); diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index bc6398b36..764f11fdd 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -67,6 +67,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => { dispatcher.respond(DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height }); } + FrontendMessage::UpdateUIScale { scale } => { + dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale }); + return Some(FrontendMessage::UpdateUIScale { scale }); + } FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => { dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument { id: document_id, diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 78a0faca1..a5769ecfe 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -35,6 +35,9 @@ pub enum DesktopFrontendMessage { width: f64, height: f64, }, + UpdateUIScale { + scale: f64, + }, UpdateOverlays(vello::Scene), PersistenceWriteDocument { id: DocumentId, diff --git a/editor/src/consts.rs b/editor/src/consts.rs index b17a8621c..f515ee07b 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -158,3 +158,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.; diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index ae437d11b..3e4b575b9 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -52,6 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ ))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), + 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. diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 74034200a..0c25cf464 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,4 +1,4 @@ -use crate::consts::{VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE}; +use crate::consts::{UI_SCALE_DEFAULT, UI_SCALE_MAX, UI_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::preferences::SelectionMode; @@ -36,250 +36,273 @@ 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_instance()]; + 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(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::Unrelated).widget_instance(), + TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(), + ]; + let zoom_rate = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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_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(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::Unrelated).widget_instance(), - TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(), - ]; - let zoom_rate = vec![ - Separator::new(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::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 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(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)."; - let zoom_with_scroll = vec![ - Separator::new(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::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(), - ]; + 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_instance()]; + let selection_label = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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_instance(), - Separator::new(SeparatorType::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_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(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::Unrelated).widget_instance(), + selection_mode, + ]; - 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(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::Unrelated).widget_instance(), - selection_mode, - ]; + rows.extend_from_slice(&[header, selection_label, selection_mode]); + } + + // ========== + // UI + // ========== + #[cfg(not(target_family = "wasm"))] + { + let header = vec![TextLabel::new("UI").italic(true).widget_instance()]; + + let scale_description = "Adjust the scale of the user interface (100 is default)."; + let scale_label = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::Unrelated).widget_instance(), + TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(), + ]; + let scale = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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(UI_SCALE_MIN)) + .max(ui_scale_to_display(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: 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_instance()]; + 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(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::Unrelated).widget_instance(), + graph_wire_style, + ]; - 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(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::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(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::Unrelated).widget_instance(), - 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_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 use_vello = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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 use_vello = vec![ - Separator::new(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::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_description = " + let checkbox_id = CheckboxId::new(); + let vector_mesh_description = " Allow the Pen tool to produce branching geometry, where more than two segments may be connected to one anchor point.\n\ \n\ Currently, vector meshes do not properly render strokes (branching joins) and fills (multiple regions). " - .trim(); - let vector_meshes = vec![ - Separator::new(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::Unrelated).widget_instance(), - CheckboxInput::new(preferences.vector_meshes) - .tooltip_label("Vector Meshes") - .tooltip_description(vector_mesh_description) - .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into()) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Vector Meshes") - .tooltip_label("Vector Meshes") - .tooltip_description(vector_mesh_description) - .for_checkbox(checkbox_id) - .widget_instance(), - ]; + .trim(); + let vector_meshes = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::Unrelated).widget_instance(), + CheckboxInput::new(preferences.vector_meshes) + .tooltip_label("Vector Meshes") + .tooltip_description(vector_mesh_description) + .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into()) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Vector Meshes") + .tooltip_label("Vector Meshes") + .tooltip_description(vector_mesh_description) + .for_checkbox(checkbox_id) + .widget_instance(), + ]; - let checkbox_id = CheckboxId::new(); - let brush_tool_description = " + let checkbox_id = CheckboxId::new(); + let brush_tool_description = " Enable the Brush tool to support basic raster-based layer painting.\n\ \n\ This legacy tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing. " - .trim(); - let brush_tool = vec![ - Separator::new(SeparatorType::Unrelated).widget_instance(), - Separator::new(SeparatorType::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(), - ]; + .trim(); + let brush_tool = vec![ + Separator::new(SeparatorType::Unrelated).widget_instance(), + Separator::new(SeparatorType::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(vec![ - // NAVIGATION - LayoutGroup::Row { widgets: navigation_header }, - // Navigation: Zoom Rate - LayoutGroup::Row { widgets: zoom_rate_label }, - LayoutGroup::Row { widgets: zoom_rate }, - // Navigation: Zoom with Scroll - LayoutGroup::Row { widgets: zoom_with_scroll }, - // - // EDITING - LayoutGroup::Row { widgets: editing_header }, - // Editing: Selection - LayoutGroup::Row { widgets: selection_label }, - LayoutGroup::Row { widgets: selection_mode }, - // - // EXPERIMENTAL - LayoutGroup::Row { widgets: experimental_header }, - // Experimental: Node Graph Wires - LayoutGroup::Row { widgets: node_graph_wires_label }, - LayoutGroup::Row { widgets: graph_wire_style }, - // Experimental: Vello Renderer - LayoutGroup::Row { widgets: use_vello }, - // Experimental: Vector Meshes - LayoutGroup::Row { widgets: vector_meshes }, - // Experimental: Brush Tool - LayoutGroup::Row { widgets: brush_tool }, - ]) + rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, vector_meshes, brush_tool]); + } + + Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect()) } pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { @@ -351,3 +374,13 @@ 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. +fn map_display_to_ui_scale(display: f64) -> f64 { + display / 100. +} + +/// Maps actual ui scale back to display values in percent. +fn ui_scale_to_display(scale: f64) -> f64 { + scale * 100. +} diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 573dc8ffd..202d7e239 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -338,6 +338,9 @@ pub enum FrontendMessage { width: f64, height: f64, }, + UpdateUIScale { + scale: f64, + }, #[cfg(not(target_family = "wasm"))] RenderOverlays { diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index 0b58e8c1f..027bdd406 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -17,4 +17,5 @@ pub enum PreferencesMessage { ModifyLayout { zoom_with_scroll: bool }, GraphWireStyle { style: GraphWireStyle }, ViewportZoomWheelRate { rate: f64 }, + UIScale { scale: f64 }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 06080b23c..2cc0b5aec 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -1,4 +1,4 @@ -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; @@ -14,6 +14,7 @@ pub struct PreferencesMessageHandler { pub brush_tool: bool, pub graph_wire_style: GraphWireStyle, pub viewport_zoom_wheel_rate: f64, + pub ui_scale: f64, } impl PreferencesMessageHandler { @@ -42,6 +43,7 @@ impl Default for PreferencesMessageHandler { brush_tool: false, graph_wire_style: GraphWireStyle::default(), viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE, + ui_scale: UI_SCALE_DEFAULT, } } } @@ -61,6 +63,7 @@ impl MessageHandler for PreferencesMessageHandler { responses.add(PreferencesMessage::ModifyLayout { zoom_with_scroll: self.zoom_with_scroll, }); + responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale }); } PreferencesMessage::ResetToDefaults => { refresh_dialog(responses); @@ -99,6 +102,10 @@ impl MessageHandler 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() }); diff --git a/frontend/src/components/window/title-bar/TitleBar.svelte b/frontend/src/components/window/title-bar/TitleBar.svelte index 71b046825..802066a93 100644 --- a/frontend/src/components/window/title-bar/TitleBar.svelte +++ b/frontend/src/components/window/title-bar/TitleBar.svelte @@ -17,6 +17,9 @@ let menuBarLayout: Layout = []; + // On mac menu bar needs to be scaled with inverse of UI scale to match native menu buttons. + $: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28; + onMount(() => { editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => { patchLayout(menuBarLayout, updateMenuBarLayout); @@ -25,7 +28,7 @@ }); - + {#if $appWindow.platform !== "Mac"} @@ -48,7 +51,6 @@ diff --git a/frontend/src/io-managers/fonts.ts b/frontend/src/io-managers/fonts.ts new file mode 100644 index 000000000..edd931b58 --- /dev/null +++ b/frontend/src/io-managers/fonts.ts @@ -0,0 +1,44 @@ +import { type Editor } from "@graphite/editor"; +import { TriggerFontCatalogLoad, TriggerFontDataLoad } from "@graphite/messages"; + +type ApiResponse = { family: string; variants: string[]; files: Record }[]; + +const FONT_LIST_API = "https://api.graphite.art/font-list"; + +export function createFontsManager(editor: Editor) { + // Subscribe to process backend events + editor.subscriptions.subscribeJsMessage(TriggerFontCatalogLoad, async () => { + const response = await fetch(FONT_LIST_API); + const fontListResponse = (await response.json()) as { items: ApiResponse }; + const fontListData = fontListResponse.items; + + const catalog = fontListData.map((font) => { + const styles = font.variants.map((variant) => { + const weight = variant === "regular" || variant === "italic" ? 400 : parseInt(variant, 10); + const italic = variant.endsWith("italic"); + const url = font.files[variant].replace("http://", "https://"); + + return { weight, italic, url }; + }); + return { name: font.family, styles }; + }); + + editor.handle.onFontCatalogLoad(catalog); + }); + + editor.subscriptions.subscribeJsMessage(TriggerFontDataLoad, async (triggerFontDataLoad) => { + const { fontFamily, fontStyle } = triggerFontDataLoad.font; + + try { + if (!triggerFontDataLoad.url) throw new Error("No URL provided for font data load"); + const response = await fetch(triggerFontDataLoad.url); + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + + editor.handle.onFontLoad(fontFamily, fontStyle, data); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to load font:", error); + } + }); +} diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 670551182..a7f44b70d 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -793,7 +793,7 @@ export class DisplayEditableTextbox extends JsMessage { @Type(() => Color) readonly color!: Color; - readonly url!: string; + readonly fontData!: ArrayBuffer; readonly transform!: number[]; @@ -804,6 +804,10 @@ export class DisplayEditableTextbox extends JsMessage { readonly align!: TextAlign; } +export class DisplayEditableTextboxUpdateFontData extends JsMessage { + readonly fontData!: ArrayBuffer; +} + export class DisplayEditableTextboxTransform extends JsMessage { readonly transform!: number[]; } @@ -865,9 +869,13 @@ export class Font { fontStyle!: string; } -export class TriggerFontLoad extends JsMessage { +export class TriggerFontCatalogLoad extends JsMessage {} + +export class TriggerFontDataLoad extends JsMessage { @Type(() => Font) font!: Font; + + url!: string; } export class TriggerVisitLink extends JsMessage { @@ -998,13 +1006,14 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri export type MenuListEntry = { value: string; label: string; - font?: URL; + font?: string; icon?: IconName; disabled?: boolean; tooltipLabel?: string; tooltipDescription?: string; tooltipShortcut?: ActionShortcut; children?: MenuListEntry[][]; + childrenHash?: bigint; }; export class CurveManipulatorGroup { @@ -1036,6 +1045,8 @@ export class CurveInput extends WidgetProps { export class DropdownInput extends WidgetProps { entries!: MenuListEntry[][]; + entriesHash!: bigint; + selectedIndex!: number | undefined; drawIcon!: boolean; @@ -1046,6 +1057,8 @@ export class DropdownInput extends WidgetProps { narrow!: boolean; + virtualScrolling!: boolean; + @Transform(({ value }: { value: string }) => value || undefined) tooltipLabel!: string | undefined; @@ -1062,25 +1075,6 @@ export class DropdownInput extends WidgetProps { maxWidth!: number; } -export class FontInput extends WidgetProps { - fontFamily!: string; - - fontStyle!: string; - - isStyle!: boolean; - - disabled!: boolean; - - @Transform(({ value }: { value: string }) => value || undefined) - tooltipLabel!: string | undefined; - - @Transform(({ value }: { value: string }) => value || undefined) - tooltipDescription!: string | undefined; - - @Transform(({ value }: { value: ActionShortcut }) => value || undefined) - tooltipShortcut!: ActionShortcut | undefined; -} - export class IconButton extends WidgetProps { icon!: IconName; @@ -1349,6 +1343,8 @@ export class TextButton extends WidgetProps { tooltipShortcut!: ActionShortcut | undefined; menuListChildren!: MenuListEntry[][]; + + menuListChildrenHash!: bigint; } export class BreadcrumbTrailButtons extends WidgetProps { @@ -1449,7 +1445,6 @@ const widgetSubTypes = [ { value: ColorInput, name: "ColorInput" }, { value: CurveInput, name: "CurveInput" }, { value: DropdownInput, name: "DropdownInput" }, - { value: FontInput, name: "FontInput" }, { value: IconButton, name: "IconButton" }, { value: ImageButton, name: "ImageButton" }, { value: ImageLabel, name: "ImageLabel" }, @@ -1695,6 +1690,7 @@ export const messageMakers: Record = { DisplayDialogDismiss, DisplayDialogPanic, DisplayEditableTextbox, + DisplayEditableTextboxUpdateFontData, DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, SendUIMetadata, @@ -1704,7 +1700,8 @@ export const messageMakers: Record = { TriggerDisplayThirdPartyLicensesDialog, TriggerExportImage, TriggerFetchAndOpenDocument, - TriggerFontLoad, + TriggerFontCatalogLoad, + TriggerFontDataLoad, TriggerImport, TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, diff --git a/frontend/src/state-providers/fonts.ts b/frontend/src/state-providers/fonts.ts deleted file mode 100644 index f364c3607..000000000 --- a/frontend/src/state-providers/fonts.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { writable } from "svelte/store"; - -import { type Editor } from "@graphite/editor"; -import { TriggerFontLoad } from "@graphite/messages"; - -export function createFontsState(editor: Editor) { - // TODO: Do some code cleanup to remove the need for this empty store - const { subscribe } = writable({}); - - function createURL(font: string, weight: string): URL { - const url = new URL("https://fonts.googleapis.com/css2"); - url.searchParams.set("display", "swap"); - url.searchParams.set("family", `${font}:wght@${weight}`); - url.searchParams.set("text", font); - - return url; - } - - async function fontNames(): Promise<{ name: string; url: URL | undefined }[]> { - const pickPreviewWeight = (variants: string[]) => { - const weights = variants.map((variant) => Number(variant.match(/.* \((\d+)\)/)?.[1] || "NaN")); - const weightGoal = 400; - const sorted = weights.map((weight) => [weight, Math.abs(weightGoal - weight - 1)]); - sorted.sort(([_, a], [__, b]) => a - b); - return sorted[0][0].toString(); - }; - return (await loadFontList()).map((font) => ({ name: font.family, url: createURL(font.family, pickPreviewWeight(font.variants)) })); - } - - async function getFontStyles(fontFamily: string): Promise<{ name: string; url: URL | undefined }[]> { - const font = (await loadFontList()).find((value) => value.family === fontFamily); - return font?.variants.map((variant) => ({ name: variant, url: undefined })) || []; - } - - async function getFontFileUrl(fontFamily: string, fontStyle: string): Promise { - const font = (await loadFontList()).find((value) => value.family === fontFamily); - const fontFileUrl = font?.files.get(fontStyle); - return fontFileUrl?.replace("http://", "https://"); - } - - function formatFontStyleName(fontStyle: string): string { - const isItalic = fontStyle.endsWith("italic"); - const weight = fontStyle === "regular" || fontStyle === "italic" ? 400 : parseInt(fontStyle, 10); - let weightName = ""; - - let bestWeight = Infinity; - weightNameMapping.forEach((nameChecking, weightChecking) => { - if (Math.abs(weightChecking - weight) < bestWeight) { - bestWeight = Math.abs(weightChecking - weight); - weightName = nameChecking; - } - }); - - return `${weightName}${isItalic ? " Italic" : ""} (${weight})`; - } - - let fontList: Promise<{ family: string; variants: string[]; files: Map }[]> | undefined; - - async function loadFontList(): Promise<{ family: string; variants: string[]; files: Map }[]> { - if (fontList) return fontList; - - fontList = new Promise<{ family: string; variants: string[]; files: Map }[]>((resolve) => { - fetch(fontListAPI) - .then((response) => response.json()) - .then((fontListResponse) => { - const fontListData = fontListResponse.items as { family: string; variants: string[]; files: Record }[]; - const result = fontListData.map((font) => { - const { family } = font; - const variants = font.variants.map(formatFontStyleName); - const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]])); - return { family, variants, files }; - }); - - resolve(result); - }); - }); - - return fontList; - } - - // Subscribe to process backend events - editor.subscriptions.subscribeJsMessage(TriggerFontLoad, async (triggerFontLoad) => { - const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle); - if (url) { - const response = await (await fetch(url)).arrayBuffer(); - editor.handle.onFontLoad(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle, url, new Uint8Array(response)); - } else { - editor.handle.errorDialog("Failed to load font", `The font ${triggerFontLoad.font.fontFamily} with style ${triggerFontLoad.font.fontStyle} does not exist`); - } - }); - - return { - subscribe, - fontNames, - getFontStyles, - getFontFileUrl, - }; -} -export type FontsState = ReturnType; - -const fontListAPI = "https://api.graphite.art/font-list"; - -// From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping -const weightNameMapping = new Map([ - [100, "Thin"], - [200, "Extra Light"], - [300, "Light"], - [400, "Regular"], - [500, "Medium"], - [600, "Semi Bold"], - [700, "Bold"], - [800, "Extra Bold"], - [900, "Black"], - [950, "Extra Black"], -]); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 05d02bf20..8f386c387 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -12,7 +12,7 @@ use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport; -use editor::messages::portfolio::utility_types::Platform; +use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, Platform}; use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; @@ -116,7 +116,6 @@ impl EditorHandle { #[cfg(not(feature = "native"))] fn dispatch>(&self, message: T) { // Process no further messages after a crash to avoid spamming the console - use crate::MESSAGE_BUFFER; if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { return; @@ -330,21 +329,43 @@ impl EditorHandle { /// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that) #[wasm_bindgen(js_name = widgetValueUpdate)] - pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { + pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> { + self.widget_value_update_helper(layout_target, widget_id, value, resend_widget) + } + + /// Commit the value of a given UI widget to the history + #[wasm_bindgen(js_name = widgetValueCommit)] + pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { + self.widget_value_commit_helper(layout_target, widget_id, value) + } + + /// Update the value of a given UI widget, and commit it to the history + #[wasm_bindgen(js_name = widgetValueCommitAndUpdate)] + pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> { + self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?; + self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?; + Ok(()) + } + + pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> { let widget_id = WidgetId(widget_id); match (from_value(layout_target), from_value(value)) { (Ok(layout_target), Ok(value)) => { let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value }; self.dispatch(message); + + if resend_widget { + let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id }; + self.dispatch(resend_message); + } + Ok(()) } (target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()), } } - /// Commit the value of a given UI widget to the history - #[wasm_bindgen(js_name = widgetValueCommit)] - pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { + pub fn widget_value_commit_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { let widget_id = WidgetId(widget_id); match (from_value(layout_target), from_value(value)) { (Ok(layout_target), Ok(value)) => { @@ -356,14 +377,6 @@ impl EditorHandle { } } - /// Update the value of a given UI widget, and commit it to the history - #[wasm_bindgen(js_name = widgetValueCommitAndUpdate)] - pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { - self.widget_value_commit(layout_target.clone(), widget_id, value.clone())?; - self.widget_value_update(layout_target, widget_id, value)?; - Ok(()) - } - #[wasm_bindgen(js_name = loadPreferences)] pub fn load_preferences(&self, preferences: Option) { let preferences = if let Some(preferences) = preferences { @@ -562,15 +575,21 @@ impl EditorHandle { Ok(()) } + /// The font catalog has been loaded + #[wasm_bindgen(js_name = onFontCatalogLoad)] + pub fn on_font_catalog_load(&self, catalog: JsValue) -> Result<(), JsValue> { + // Deserializing from TS type: `{ name: string; styles: { weight: number, italic: boolean, url: string }[] }[]` + let families = serde_wasm_bindgen::from_value::>(catalog)?; + let message = PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(families) }; + self.dispatch(message); + + Ok(()) + } + /// A font has been downloaded #[wasm_bindgen(js_name = onFontLoad)] - pub fn on_font_load(&self, font_family: String, font_style: String, preview_url: String, data: Vec) -> Result<(), JsValue> { - let message = PortfolioMessage::FontLoaded { - font_family, - font_style, - preview_url, - data, - }; + pub fn on_font_load(&self, font_family: String, font_style: String, data: Vec) -> Result<(), JsValue> { + let message = PortfolioMessage::FontLoaded { font_family, font_style, data }; self.dispatch(message); Ok(()) diff --git a/node-graph/libraries/core-types/src/consts.rs b/node-graph/libraries/core-types/src/consts.rs index 0487100e4..b2dd51562 100644 --- a/node-graph/libraries/core-types/src/consts.rs +++ b/node-graph/libraries/core-types/src/consts.rs @@ -5,5 +5,5 @@ pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK; pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5; // Fonts -pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; +pub const DEFAULT_FONT_FAMILY: &str = "Lato"; pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 00d7cf3cd..9dfe65635 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -8,7 +8,7 @@ fn text<'i: 'n>( _: impl Ctx, editor: &'i WasmEditorApi, text: String, - font_name: Font, + font: Font, #[unit(" px")] #[default(24.)] font_size: f64, @@ -39,5 +39,5 @@ fn text<'i: 'n>( align, }; - to_path(&text, &font_name, &editor.font_cache, typesetting, per_glyph_instances) + to_path(&text, &font, &editor.font_cache, typesetting, per_glyph_instances) } diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 7cdee13c1..887544889 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -7,16 +7,55 @@ use std::sync::Arc; use core_types::specta; /// A font type (storing font family and font style and an optional preview URL) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, core_types::specta::Type)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny, core_types::specta::Type)] pub struct Font { #[serde(rename = "fontFamily")] pub font_family: String, #[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")] pub font_style: String, + #[serde(skip)] + pub font_style_to_restore: Option, } + +impl std::hash::Hash for Font { + fn hash(&self, state: &mut H) { + self.font_family.hash(state); + self.font_style.hash(state); + // Don't consider `font_style_to_restore` in the HashMaps + } +} + +impl PartialEq for Font { + fn eq(&self, other: &Self) -> bool { + // Don't consider `font_style_to_restore` in the HashMaps + self.font_family == other.font_family && self.font_style == other.font_style + } +} + impl Font { pub fn new(font_family: String, font_style: String) -> Self { - Self { font_family, font_style } + Self { + font_family, + font_style, + font_style_to_restore: None, + } + } + + pub fn named_weight(weight: u32) -> &'static str { + // From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping + match weight { + 100 => "Thin", + 200 => "Extra Light", + 300 => "Light", + 400 => "Regular", + 500 => "Medium", + 600 => "Semi Bold", + 700 => "Bold", + 800 => "Extra Bold", + 900 => "Black", + 950 => "Extra Black", + _ => "Regular", + } } } impl Default for Font { @@ -24,21 +63,33 @@ impl Default for Font { Self::new(core_types::consts::DEFAULT_FONT_FAMILY.into(), core_types::consts::DEFAULT_FONT_STYLE.into()) } } + /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) -#[derive(Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Default, DynAny)] pub struct FontCache { /// Actual font file data used for rendering a font font_file_data: HashMap>, - /// Web font preview URLs used for showing fonts when live editing - preview_urls: HashMap, } impl std::fmt::Debug for FontCache { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FontCache") - .field("font_file_data", &self.font_file_data.keys().collect::>()) - .field("preview_urls", &self.preview_urls) - .finish() + f.debug_struct("FontCache").field("font_file_data", &self.font_file_data.keys().collect::>()).finish() + } +} + +impl std::hash::Hash for FontCache { + fn hash(&self, state: &mut H) { + self.font_file_data.len().hash(state); + self.font_file_data.keys().for_each(|font| font.hash(state)); + } +} + +impl PartialEq for FontCache { + fn eq(&self, other: &Self) -> bool { + if self.font_file_data.len() != other.font_file_data.len() { + return false; + } + self.font_file_data.keys().all(|font| other.font_file_data.contains_key(font)) } } @@ -70,26 +121,8 @@ impl FontCache { } /// Insert a new font into the cache - pub fn insert(&mut self, font: Font, perview_url: String, data: Vec) { + pub fn insert(&mut self, font: Font, data: Vec) { self.font_file_data.insert(font.clone(), data); - self.preview_urls.insert(font, perview_url); - } - - /// Gets the preview URL for showing in text field when live editing - pub fn get_preview_url(&self, font: &Font) -> Option<&String> { - self.preview_urls.get(font) - } -} - -impl std::hash::Hash for FontCache { - fn hash(&self, state: &mut H) { - self.preview_urls.len().hash(state); - self.preview_urls.iter().for_each(|(font, url)| { - font.hash(state); - url.hash(state) - }); - self.font_file_data.len().hash(state); - self.font_file_data.keys().for_each(|font| font.hash(state)); } } From e99f30e6336f825134b007ffc3f4e8973ed613f7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 19 Dec 2025 23:44:55 -0800 Subject: [PATCH 10/16] Stop hover transfer working between rows of Properties panel dropdown menus --- frontend/src/components/layout/FloatingMenu.svelte | 7 +++++-- frontend/src/components/widgets/WidgetSection.svelte | 2 +- frontend/src/components/widgets/buttons/TextButton.svelte | 3 ++- .../components/widgets/inputs/WorkingColorsInput.svelte | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index bc6112dc6..b26d6977b 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -372,6 +372,9 @@ // Start with the parent of the spawner for this floating menu and keep widening the search for any other valid spawners that are hover-transferrable let currentAncestor = (targetSpawner && ownSpawner?.parentElement) || undefined; while (currentAncestor) { + // If the current ancestor blocks hover transfer, stop searching + if (currentAncestor.hasAttribute("data-block-hover-transfer")) break; + const ownSpawnerDepthFromCurrentAncestor = ownSpawner && getDepthFromAncestor(ownSpawner, currentAncestor); const currentAncestor2 = currentAncestor; // This duplicate variable avoids an ESLint warning @@ -382,8 +385,8 @@ const notOurself = !ownDescendantMenuSpawners.includes(item); // And filter away unequal depths from the current ancestor const notUnequalDepths = notOurself && getDepthFromAncestor(item, currentAncestor2) === ownSpawnerDepthFromCurrentAncestor; - // And filter away elements that explicitly disable hover transfer - return notUnequalDepths && !(item as HTMLElement).getAttribute?.("data-floating-menu-spawner")?.includes("no-hover-transfer"); + // And filter away descendants that explicitly disable hover transfer + return notUnequalDepths && !(item instanceof HTMLElement && item.hasAttribute("data-block-hover-transfer")); }); // If none were found, widen the search by a level and keep trying (or stop looping if the root was reached) diff --git a/frontend/src/components/widgets/WidgetSection.svelte b/frontend/src/components/widgets/WidgetSection.svelte index c9964c74a..c51e72428 100644 --- a/frontend/src/components/widgets/WidgetSection.svelte +++ b/frontend/src/components/widgets/WidgetSection.svelte @@ -59,7 +59,7 @@ /> {#if expanded} - + {#each widgetData.layout as layoutGroup} {#if isWidgetSpanRow(layoutGroup)} diff --git a/frontend/src/components/widgets/buttons/TextButton.svelte b/frontend/src/components/widgets/buttons/TextButton.svelte index 94de4b2f5..86608e3d9 100644 --- a/frontend/src/components/widgets/buttons/TextButton.svelte +++ b/frontend/src/components/widgets/buttons/TextButton.svelte @@ -72,7 +72,8 @@ data-disabled={disabled || undefined} data-text-button tabindex={disabled ? -1 : 0} - data-floating-menu-spawner={menuListChildrenExists ? "" : "no-hover-transfer"} + data-floating-menu-spawner + data-block-hover-transfer={menuListChildrenExists ? undefined : ""} on:click={onClick} > {#if icon} diff --git a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte index cff3beacb..051f9367a 100644 --- a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte +++ b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte @@ -37,7 +37,7 @@ - + (primaryOpen = detail)} @@ -47,7 +47,7 @@ /> - + (secondaryOpen = detail)} From 2c21e1a90b4a931d166c7e35d285d20951798b97 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 20 Dec 2025 00:20:35 -0800 Subject: [PATCH 11/16] Desktop: Clean up the UI scale setting in the Preferences dialog --- .../preferences_dialog_message_handler.rs | 23 +++++++++++-------- .../preferences_message_handler.rs | 1 + 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 0c25cf464..e3960982a 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,4 +1,4 @@ -use crate::consts::{UI_SCALE_DEFAULT, UI_SCALE_MAX, UI_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE}; +use crate::consts::{VIEWPORT_ZOOM_WHEEL_RATE, VIEWPORT_ZOOM_WHEEL_RATE_CHANGE}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::preferences::SelectionMode; @@ -155,14 +155,14 @@ impl PreferencesDialogMessageHandler { rows.extend_from_slice(&[header, selection_label, selection_mode]); } - // ========== - // UI - // ========== + // ========= + // INTERFACE + // ========= #[cfg(not(target_family = "wasm"))] { - let header = vec![TextLabel::new("UI").italic(true).widget_instance()]; + let header = vec![TextLabel::new("Interface").italic(true).widget_instance()]; - let scale_description = "Adjust the scale of the user interface (100 is default)."; + let scale_description = "Adjust the scale of the entire user interface (100% is default)."; let scale_label = vec![ Separator::new(SeparatorType::Unrelated).widget_instance(), Separator::new(SeparatorType::Unrelated).widget_instance(), @@ -176,15 +176,18 @@ impl PreferencesDialogMessageHandler { .tooltip_description(scale_description) .mode_range() .int() - .min(ui_scale_to_display(UI_SCALE_MIN)) - .max(ui_scale_to_display(UI_SCALE_MAX)) + .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: UI_SCALE_DEFAULT }.into() + PreferencesMessage::UIScale { + scale: crate::consts::UI_SCALE_DEFAULT, + } + .into() } }) .widget_instance(), @@ -376,11 +379,13 @@ fn map_zoom_rate_to_display(rate: f64) -> f64 { } /// 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. } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 2cc0b5aec..98af0baca 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -6,6 +6,7 @@ use crate::messages::prelude::*; use graph_craft::wasm_application_io::EditorPreferences; #[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, From f1e8ebefc587fb092bd32547082be90b73a0da41 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 20 Dec 2025 01:05:15 -0800 Subject: [PATCH 12/16] Improve tooltip docs with Markdown styling and refined math node explanations (#3488) --- .../src/messages/frontend/frontend_message.rs | 3 + .../node_graph/document_node_definitions.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 3 + .../tool/common_functionality/pivot.rs | 1 + frontend/src/components/Editor.svelte | 2 +- .../floating-menus/ColorPicker.svelte | 36 ++- .../components/floating-menus/Tooltip.svelte | 28 +- frontend/src/components/panels/Layers.svelte | 18 +- frontend/src/components/views/Graph.svelte | 4 +- .../widgets/labels/TextLabel.svelte | 8 +- .../window/title-bar/WindowButtonsWeb.svelte | 19 +- frontend/src/io-managers/input.ts | 4 +- frontend/src/messages.ts | 6 + frontend/src/state-providers/app-window.ts | 2 +- frontend/src/state-providers/tooltip.ts | 27 +- .../libraries/vector-types/src/vector/misc.rs | 2 +- .../vector-types/src/vector/style.rs | 1 - node-graph/nodes/math/src/lib.rs | 286 ++++++++++-------- node-graph/nodes/raster/src/adjustments.rs | 9 +- 19 files changed, 276 insertions(+), 185 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index ae6b4ff27..a63c6a226 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -70,6 +70,9 @@ pub enum FrontendMessage { SendShortcutAltClick { shortcut: Option, }, + SendShortcutShiftClick { + shortcut: Option, + }, // Trigger prefix: cause a frontend specific API to do something TriggerAboutGraphiteLocalizedCommitDate { diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 61f00fc18..16cf2983a 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -274,7 +274,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("Merges new content as an entry into the graphic table that represents a layer compositing stack."), + description: Cow::Borrowed("Merges the provided content as a new element in the graphic table that represents a layer compositing stack."), properties: None, }, DocumentNodeDefinition { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 1d5fb9dee..70f344894 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -124,6 +124,9 @@ impl MessageHandler> for Portfolio responses.add(FrontendMessage::SendShortcutAltClick { shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft), }); + responses.add(FrontendMessage::SendShortcutShiftClick { + shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft), + }); // Before loading any documents, initially prepare the welcome screen buttons layout responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index 2b99a347c..0e61fd9a8 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -169,6 +169,7 @@ pub struct PivotGizmoState { impl PivotGizmoState { pub fn is_pivot_type(&self) -> bool { + // A disabled pivot is considered a pivot-type gizmo that is always centered self.gizmo_type == PivotGizmoType::Pivot || self.disabled } diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 851d148b8..ecbd24b8f 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -27,7 +27,7 @@ // State provider systems let dialog = createDialogState(editor); setContext("dialog", dialog); - let tooltip = createTooltipState(); + let tooltip = createTooltipState(editor); setContext("tooltip", tooltip); let document = createDocumentState(editor); setContext("document", document); diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index e3388f849..07abea96f 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -1,9 +1,9 @@ {#if label || description} @@ -40,7 +60,7 @@ {#if label || shortcut} {#if label} - {label} + {@html label} {/if} {#if shortcut} @@ -48,7 +68,7 @@ {/if} {#if description} - {description} + {@html description} {/if} diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index cad9f861d..2b343b03b 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -9,10 +9,10 @@ UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarRightLayout, UpdateLayersPanelBottomBarLayout, - SendShortcutAltClick, } from "@graphite/messages"; - import type { ActionShortcut, DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages"; + import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; + import type { TooltipState } from "@graphite/state-providers/tooltip"; import { operatingSystem } from "@graphite/utility-functions/platform"; import { extractPixelData } from "@graphite/utility-functions/rasterization"; @@ -49,6 +49,7 @@ const editor = getContext("editor"); const nodeGraph = getContext("nodeGraph"); + const tooltip = getContext("tooltip"); let list: LayoutCol | undefined; @@ -73,13 +74,7 @@ let layersPanelControlBarRightLayout: Layout = []; let layersPanelBottomBarLayout: Layout = []; - let altClickShortcut: ActionShortcut | undefined; - onMount(() => { - editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => { - altClickShortcut = data.shortcut; - }); - editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => { patchLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout); layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout; @@ -628,7 +623,7 @@ ? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)" : "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") + (listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")} - data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined} + data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined} on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)} tabindex="0" > @@ -639,8 +634,9 @@ {/if}
diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 58e09d796..90de83406 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -501,7 +501,7 @@ style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--layer-area-width={layerAreaWidth} style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0} - data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`} + data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`} data-tooltip-description={` ${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""} `.trim()} @@ -651,7 +651,7 @@ style:--clip-path-id={`url(#${clipPathId})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} - data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`} + data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`} data-tooltip-description={` ${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""} `.trim()} diff --git a/frontend/src/components/widgets/labels/TextLabel.svelte b/frontend/src/components/widgets/labels/TextLabel.svelte index 4174ebf43..8465ff871 100644 --- a/frontend/src/components/widgets/labels/TextLabel.svelte +++ b/frontend/src/components/widgets/labels/TextLabel.svelte @@ -72,7 +72,8 @@ font-style: italic; } - &.monospace { + &.monospace, + code { font-family: "Source Code Pro", monospace; font-size: 12px; } @@ -94,5 +95,10 @@ a { color: inherit; } + + code { + background: var(--color-3-darkgray); + padding: 0 2px; + } } diff --git a/frontend/src/components/window/title-bar/WindowButtonsWeb.svelte b/frontend/src/components/window/title-bar/WindowButtonsWeb.svelte index 5f2776f90..a87b41f62 100644 --- a/frontend/src/components/window/title-bar/WindowButtonsWeb.svelte +++ b/frontend/src/components/window/title-bar/WindowButtonsWeb.svelte @@ -1,24 +1,15 @@