From bc34509ca1d958d46970154416184c5c45709e9f Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 26 Jan 2026 03:46:38 +0000 Subject: [PATCH 1/7] mostly done --- .../src/messages/frontend/frontend_message.rs | 2 + editor/src/messages/frontend/utility_types.rs | 7 + .../messages/portfolio/portfolio_message.rs | 1 + .../portfolio/portfolio_message_handler.rs | 34 ++- .../tool/tool_messages/eyedropper_tool.rs | 22 +- editor/src/node_graph_executor.rs | 254 +++++++++++------- editor/src/node_graph_executor/runtime.rs | 2 +- .../src/components/panels/Document.svelte | 75 ++++-- frontend/src/messages.ts | 8 + .../libraries/application-io/src/lib.rs | 1 + 10 files changed, 283 insertions(+), 123 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 188456d5b2..e908259586 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,5 +1,6 @@ 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::{ @@ -253,6 +254,7 @@ pub enum FrontendMessage { multiplier: (f64, f64), }, UpdateEyedropperSamplingState { + image: Option, #[serde(rename = "mousePosition")] mouse_position: Option<(f64, f64)>, #[serde(rename = "primaryColor")] diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 89dc539c53..31d2866fd1 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -62,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, + pub width: u32, + pub height: u32, +} diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index b161bfa4f1..2db5945c78 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -148,6 +148,7 @@ pub enum PortfolioMessage { document_id: DocumentId, ignore_hash: bool, }, + SubmitEyedropperPreviewRender, ToggleResetNodesToDefinitionsOnOpen, ToggleFocusDocument, ToggleDataPanelOpen, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index d95a78e2c7..9565c7e012 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -25,7 +25,7 @@ use crate::messages::tool::utility_types::{HintData, ToolType}; use crate::messages::viewport::ToPhysical; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use derivative::*; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, UVec2}; use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::raster_types::Image; @@ -1178,6 +1178,38 @@ impl MessageHandler> for Portfolio Ok(message) => responses.add_front(message), } } + #[cfg(not(target_family = "wasm"))] + PortfolioMessage::SubmitEyedropperPreviewRender => { + let Some(document_id) = self.active_document_id else { + return; + }; + let Some(document) = self.documents.get_mut(&document_id) else { + return; + }; + + let document_to_viewport = document + .navigation_handler + .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); + let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); + + let resolution = UVec2::splat(11); + + let result = self.executor.submit_eyedropper_preview(document_id, resolution, timing_information, pointer_position); + + match result { + Err(description) => { + responses.add(DialogMessage::DisplayDialogError { + title: "Unable to update node graph".to_string(), + description, + }); + } + Ok(message) => responses.add_front(message), + } + } + #[cfg(target_family = "wasm")] + PortfolioMessage::SubmitEyedropperPreviewRender => { + // For wasm this is implemented through svg rendering + } PortfolioMessage::ToggleFocusDocument => { self.focus_document = !self.focus_document; responses.add(MenuBarMessage::SendLayout); diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index ea7843a923..1aaa21dd2e 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::messages::tool::utility_types::DocumentToolData; +use crate::messages::{frontend::utility_types::EyedropperPreviewImage, tool::utility_types::DocumentToolData}; #[derive(Default, ExtractField)] pub struct EyedropperTool { @@ -19,6 +19,8 @@ pub enum EyedropperToolMessage { PointerMove, SampleSecondaryColorBegin, SampleSecondaryColorEnd, + + PreviewImage { data: Vec, width: u32, height: u32 }, } impl ToolMetadata for EyedropperTool { @@ -42,6 +44,17 @@ impl LayoutHolder for EyedropperTool { #[message_handler_data] impl<'a> MessageHandler> for EyedropperTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + if let ToolMessage::Eyedropper(EyedropperToolMessage::PreviewImage { data, width, height }) = message { + responses.add(FrontendMessage::UpdateEyedropperSamplingState { + image: Some(EyedropperPreviewImage { data, width, height }), + mouse_position: Some(context.input.mouse.position.into()), + primary_color: "#".to_string() + context.global_tool_data.primary_color.to_rgb_hex_srgb().as_str(), + secondary_color: "#".to_string() + context.global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(), + set_color_choice: Some("Primary".to_string()), + }); + return; + } + self.fsm_state.process_event(message, &mut self.data, context, &(), responses, true); } @@ -153,6 +166,7 @@ impl Fsm for EyedropperToolFsmState { fn disable_cursor_preview(responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateEyedropperSamplingState { + image: None, mouse_position: None, primary_color: "".into(), secondary_color: "".into(), @@ -161,10 +175,16 @@ fn disable_cursor_preview(responses: &mut VecDeque) { } fn update_cursor_preview(responses: &mut VecDeque, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option) { + #[cfg(target_family = "wasm")] responses.add(FrontendMessage::UpdateEyedropperSamplingState { + image: None, mouse_position: Some(input.mouse.position.into()), primary_color: "#".to_string() + global_tool_data.primary_color.to_rgb_hex_srgb().as_str(), secondary_color: "#".to_string() + global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(), set_color_choice, }); + #[cfg(not(target_family = "wasm"))] + { + responses.add(PortfolioMessage::SubmitEyedropperPreviewRender); + } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 8ecffbeebd..3a9357d78a 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -59,6 +59,7 @@ pub struct NodeGraphExecutor { #[derive(Debug, Clone)] struct ExecutionContext { + render_config: RenderConfig, export_config: Option, document_id: DocumentId, } @@ -157,12 +158,20 @@ impl NodeGraphExecutor { render_mode: document.render_mode, hide_artboards: false, for_export: false, + for_eyedropper: false, }; // Execute the node graph let execution_id = self.queue_execution(render_config); - self.futures.push_back((execution_id, ExecutionContext { export_config: None, document_id })); + self.futures.push_back(( + execution_id, + ExecutionContext { + render_config, + export_config: None, + document_id, + }, + )); Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into()) } @@ -184,6 +193,41 @@ impl NodeGraphExecutor { self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time, pointer) } + #[cfg(not(target_family = "wasm"))] + pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, resolution: UVec2, time: TimingInformation, pointer: DVec2) -> Result { + // footprint from pointer position and resolution + let viewport = Footprint { + transform: DAffine2::from_translation(pointer).inverse(), + resolution, + ..Default::default() + }; + let render_config = RenderConfig { + viewport, + scale: 1., + time, + pointer, + export_format: graphene_std::application_io::ExportFormat::Raster, + render_mode: graphene_std::vector::style::RenderMode::Normal, + hide_artboards: false, + for_export: false, + for_eyedropper: true, + }; + + // Execute the node graph + let execution_id = self.queue_execution(render_config); + + self.futures.push_back(( + execution_id, + ExecutionContext { + render_config, + export_config: None, + document_id, + }, + )); + + Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into()) + } + /// Evaluates a node graph for export pub fn submit_document_export(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, mut export_config: ExportConfig) -> Result<(), String> { let network = document.network_interface.document_network().clone(); @@ -217,6 +261,7 @@ impl NodeGraphExecutor { render_mode: document.render_mode, hide_artboards: export_config.transparent_background, for_export: true, + for_eyedropper: false, }; export_config.size = resolution.as_dvec2(); @@ -225,97 +270,14 @@ impl NodeGraphExecutor { .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None })) .map_err(|e| e.to_string())?; let execution_id = self.queue_execution(render_config); - let execution_context = ExecutionContext { - export_config: Some(export_config), - document_id, - }; - self.futures.push_back((execution_id, execution_context)); - - Ok(()) - } - - fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque) -> Result<(), String> { - let ExportConfig { - file_type, - name, - size, - scale_factor, - #[cfg(feature = "gpu")] - transparent_background, - artboard_name, - artboard_count, - .. - } = export_config; - - let file_extension = match file_type { - FileType::Svg => "svg", - FileType::Png => "png", - FileType::Jpg => "jpg", - }; - let base_name = match (artboard_name, artboard_count) { - (Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"), - _ => name, - }; - let name = format!("{base_name}.{file_extension}"); - - match node_graph_output { - TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Svg { svg, .. }, - .. - }) => { - if file_type == FileType::Svg { - responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); - } else { - let mime = file_type.to_mime().to_string(); - let size = (size * scale_factor).into(); - responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size }); - } - } - #[cfg(feature = "gpu")] - TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Buffer { data, width, height }, - .. - }) if file_type != FileType::Svg => { - use image::buffer::ConvertBuffer; - use image::{ImageFormat, RgbImage, RgbaImage}; - - let Some(image) = RgbaImage::from_raw(width, height, data) else { - return Err("Failed to create image buffer for export".to_string()); - }; - - let mut encoded = Vec::new(); - let mut cursor = std::io::Cursor::new(&mut encoded); - - match file_type { - FileType::Png => { - let result = if transparent_background { - image.write_to(&mut cursor, ImageFormat::Png) - } else { - let image: RgbImage = image.convert(); - image.write_to(&mut cursor, ImageFormat::Png) - }; - if let Err(err) = result { - return Err(format!("Failed to encode PNG: {err}")); - } - } - FileType::Jpg => { - let image: RgbImage = image.convert(); - let result = image.write_to(&mut cursor, ImageFormat::Jpeg); - if let Err(err) = result { - return Err(format!("Failed to encode JPG: {err}")); - } - } - FileType::Svg => { - return Err("SVG cannot be exported from an image buffer".to_string()); - } - } - - responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded }); - } - _ => { - return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})")); - } - }; + self.futures.push_back(( + execution_id, + ExecutionContext { + render_config, + export_config: Some(export_config), + document_id, + }, + )); Ok(()) } @@ -363,7 +325,10 @@ impl NodeGraphExecutor { if let Some(export_config) = execution_context.export_config { // Special handling for exporting the artwork - self.export(node_graph_output, export_config, responses)?; + self.process_export(node_graph_output, export_config, responses)?; + } else if execution_context.render_config.for_eyedropper { + // Special handling for eyedropper preview + self.process_eyedropper_preview(node_graph_output, responses)?; } else { self.process_node_graph_output(node_graph_output, responses)?; } @@ -464,6 +429,109 @@ impl NodeGraphExecutor { Ok(()) } + + fn process_eyedropper_preview(&self, node_graph_output: TaggedValue, responses: &mut VecDeque) -> Result<(), String> { + match node_graph_output { + #[cfg(feature = "gpu")] + TaggedValue::RenderOutput(RenderOutput { + data: RenderOutputType::Buffer { data, width, height }, + .. + }) => { + responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); + } + _ => { + return Err(format!("Incorrect render type for eyedropper: {node_graph_output}")); + } + }; + + Ok(()) + } + + fn process_export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque) -> Result<(), String> { + let ExportConfig { + file_type, + name, + size, + scale_factor, + #[cfg(feature = "gpu")] + transparent_background, + artboard_name, + artboard_count, + .. + } = export_config; + + let file_extension = match file_type { + FileType::Svg => "svg", + FileType::Png => "png", + FileType::Jpg => "jpg", + }; + let base_name = match (artboard_name, artboard_count) { + (Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"), + _ => name, + }; + let name = format!("{base_name}.{file_extension}"); + + match node_graph_output { + TaggedValue::RenderOutput(RenderOutput { + data: RenderOutputType::Svg { svg, .. }, + .. + }) => { + if file_type == FileType::Svg { + responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); + } else { + let mime = file_type.to_mime().to_string(); + let size = (size * scale_factor).into(); + responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size }); + } + } + #[cfg(feature = "gpu")] + TaggedValue::RenderOutput(RenderOutput { + data: RenderOutputType::Buffer { data, width, height }, + .. + }) if file_type != FileType::Svg => { + use image::buffer::ConvertBuffer; + use image::{ImageFormat, RgbImage, RgbaImage}; + + let Some(image) = RgbaImage::from_raw(width, height, data) else { + return Err("Failed to create image buffer for export".to_string()); + }; + + let mut encoded = Vec::new(); + let mut cursor = std::io::Cursor::new(&mut encoded); + + match file_type { + FileType::Png => { + let result = if transparent_background { + image.write_to(&mut cursor, ImageFormat::Png) + } else { + let image: RgbImage = image.convert(); + image.write_to(&mut cursor, ImageFormat::Png) + }; + if let Err(err) = result { + return Err(format!("Failed to encode PNG: {err}")); + } + } + FileType::Jpg => { + let image: RgbImage = image.convert(); + let result = image.write_to(&mut cursor, ImageFormat::Jpeg); + if let Err(err) = result { + return Err(format!("Failed to encode JPG: {err}")); + } + } + FileType::Svg => { + return Err("SVG cannot be exported from an image buffer".to_string()); + } + } + + responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded }); + } + _ => { + return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})")); + } + }; + + Ok(()) + } } // Re-export for usage by tests in other modules diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index bef56a57b6..39265a087a 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -246,7 +246,7 @@ impl NodeRuntime { Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(image_texture), metadata, - })) if render_config.for_export => { + })) if render_config.for_export || render_config.for_eyedropper => { let executor = self .editor_api .application_io diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 2ad4679769..d4f21161af 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -212,7 +212,14 @@ }); } - export async function updateEyedropperSamplingState(mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string): Promise<[number, number, number] | undefined> { + export async function updateEyedropperSamplingState( + image: ImageData | undefined, + mousePosition: XY | undefined, + colorPrimary: string, + colorSecondary: string, + ): Promise<[number, number, number] | undefined> { + var preview = image; + if (mousePosition === undefined) { cursorEyedropper = false; return undefined; @@ -224,40 +231,53 @@ cursorLeft = mousePosition.x; cursorTop = mousePosition.y; - // This works nearly perfectly, but sometimes at odd DPI scale factors like 1.25, the anti-aliasing color can yield slightly incorrect colors (potential room for future improvement) - const dpiFactor = window.devicePixelRatio; - const [width, height] = [canvasWidth, canvasHeight]; + if (image === undefined) { + // This works nearly perfectly, but sometimes at odd DPI scale factors like 1.25, the anti-aliasing color can yield slightly incorrect colors (potential room for future improvement) + const dpiFactor = window.devicePixelRatio; + const [width, height] = [canvasWidth, canvasHeight]; - const outsideArtboardsColor = getComputedStyle(window.document.documentElement).getPropertyValue("--color-2-mildblack"); - const outsideArtboards = ``; + const outsideArtboardsColor = getComputedStyle(window.document.documentElement).getPropertyValue("--color-2-mildblack"); + const outsideArtboards = ``; - const svg = ` - ${outsideArtboards}${artworkSvg} - `.trim(); + const svg = ` + ${outsideArtboards}${artworkSvg} + `.trim(); - if (!rasterizedCanvas) { - rasterizedCanvas = await rasterizeSVGCanvas(svg, width * dpiFactor, height * dpiFactor); - rasterizedContext = rasterizedCanvas.getContext("2d", { willReadFrequently: true }) || undefined; + if (!rasterizedCanvas) { + rasterizedCanvas = await rasterizeSVGCanvas(svg, width * dpiFactor, height * dpiFactor); + rasterizedContext = rasterizedCanvas.getContext("2d", { willReadFrequently: true }) || undefined; + } + if (!rasterizedContext) return undefined; + + preview = rasterizedContext.getImageData( + mousePosition.x * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, + mousePosition.y * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, + ZOOM_WINDOW_DIMENSIONS, + ZOOM_WINDOW_DIMENSIONS, + ); } - if (!rasterizedContext) return undefined; + const getCenterPixel = (imageData: ImageData) => { + const { width, height, data } = imageData; + const x = Math.floor(width / 2); + const y = Math.floor(height / 2); + const index = (y * width + x) * 4; + return { + r: data[index], + g: data[index + 1], + b: data[index + 2], + }; + }; + if (!preview) return undefined; + const pixel = getCenterPixel(preview); const rgbToHex = (r: number, g: number, b: number): string => `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`; - - const pixel = rasterizedContext.getImageData(mousePosition.x * dpiFactor, mousePosition.y * dpiFactor, 1, 1).data; - const hex = rgbToHex(pixel[0], pixel[1], pixel[2]); - const rgb: [number, number, number] = [pixel[0] / 255, pixel[1] / 255, pixel[2] / 255]; + const hex = rgbToHex(pixel.r, pixel.g, pixel.b); + const rgb: [number, number, number] = [pixel.r / 255, pixel.g / 255, pixel.b / 255]; cursorEyedropperPreviewColorChoice = hex; cursorEyedropperPreviewColorPrimary = colorPrimary; cursorEyedropperPreviewColorSecondary = colorSecondary; - - const previewRegion = rasterizedContext.getImageData( - mousePosition.x * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, - mousePosition.y * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, - ZOOM_WINDOW_DIMENSIONS, - ZOOM_WINDOW_DIMENSIONS, - ); - cursorEyedropperPreviewImageData = previewRegion; + cursorEyedropperPreviewImageData = image; return rgb; } @@ -413,8 +433,9 @@ editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => { await tick(); - const { mousePosition, primaryColor, secondaryColor, setColorChoice } = data; - const rgb = await updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor); + const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data; + const imageData = image !== undefined ? new ImageData(new Uint8ClampedArray(image.data), image.width, image.height) : undefined; + const rgb = await updateEyedropperSamplingState(imageData, mousePosition, primaryColor, secondaryColor); if (setColorChoice && rgb) { if (setColorChoice === "Primary") editor.handle.updatePrimaryColor(...rgb, 1); diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index a137b4755c..89add1e27c 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -678,7 +678,15 @@ export class UpdateDocumentRulers extends JsMessage { readonly visible!: boolean; } +export class EyedropperPreviewImage { + readonly data!: Uint8Array; + readonly width!: number; + readonly height!: number; +} + export class UpdateEyedropperSamplingState extends JsMessage { + readonly image!: EyedropperPreviewImage | undefined; + @TupleToVec2 readonly mousePosition!: XY | undefined; diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index cf0d9edc58..b4eeb1e6a2 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -244,6 +244,7 @@ pub struct RenderConfig { pub render_mode: RenderMode, pub hide_artboards: bool, pub for_export: bool, + pub for_eyedropper: bool, } struct Logger; From 06e41e1ece3b49e1ef4aaa914c27a76e933a3925 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 26 Jan 2026 15:34:34 +0000 Subject: [PATCH 2/7] fix --- .../portfolio/portfolio_message_handler.rs | 4 +- .../tool/tool_messages/eyedropper_tool.rs | 75 +++++++++++++------ 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 9565c7e012..819ff3deaa 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -25,7 +25,7 @@ use crate::messages::tool::utility_types::{HintData, ToolType}; use crate::messages::viewport::ToPhysical; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use derivative::*; -use glam::{DAffine2, DVec2, UVec2}; +use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::raster_types::Image; @@ -1192,7 +1192,7 @@ impl MessageHandler> for Portfolio .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); - let resolution = UVec2::splat(11); + let resolution = glam::UVec2::splat(11); let result = self.executor.submit_eyedropper_preview(document_id, resolution, timing_information, pointer_position); diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index 1aaa21dd2e..c5e0536be1 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -45,13 +45,11 @@ impl LayoutHolder for EyedropperTool { impl<'a> MessageHandler> for EyedropperTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { if let ToolMessage::Eyedropper(EyedropperToolMessage::PreviewImage { data, width, height }) = message { - responses.add(FrontendMessage::UpdateEyedropperSamplingState { - image: Some(EyedropperPreviewImage { data, width, height }), - mouse_position: Some(context.input.mouse.position.into()), - primary_color: "#".to_string() + context.global_tool_data.primary_color.to_rgb_hex_srgb().as_str(), - secondary_color: "#".to_string() + context.global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(), - set_color_choice: Some("Primary".to_string()), - }); + let image = EyedropperPreviewImage { data, width, height }; + update_cursor_preview_common(responses, Some(image), context.input, context.global_tool_data, self.data.color_choice.clone()); + if !self.data.preview { + disable_cursor_preview(responses, &mut self.data); + } return; } @@ -87,13 +85,16 @@ enum EyedropperToolFsmState { } #[derive(Clone, Debug, Default)] -struct EyedropperToolData {} +struct EyedropperToolData { + preview: bool, + color_choice: Option, +} impl Fsm for EyedropperToolFsmState { type ToolData = EyedropperToolData; type ToolOptions = (); - fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque) -> Self { let ToolActionMessageContext { global_tool_data, input, viewport, .. } = tool_action_data; @@ -102,7 +103,7 @@ impl Fsm for EyedropperToolFsmState { match (self, event) { // Ready -> Sampling (EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => { - update_cursor_preview(responses, input, global_tool_data, None); + update_cursor_preview(responses, tool_data, input, global_tool_data, None); if mouse_down == EyedropperToolMessage::SamplePrimaryColorBegin { EyedropperToolFsmState::SamplingPrimary @@ -114,9 +115,9 @@ impl Fsm for EyedropperToolFsmState { (EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => { let mouse_position = viewport.logical(input.mouse.position); if viewport.is_in_bounds(mouse_position + viewport.offset()) { - update_cursor_preview(responses, input, global_tool_data, None); + update_cursor_preview(responses, tool_data, input, global_tool_data, None); } else { - disable_cursor_preview(responses); + disable_cursor_preview(responses, tool_data); } self @@ -124,14 +125,14 @@ impl Fsm for EyedropperToolFsmState { // Sampling -> Ready (EyedropperToolFsmState::SamplingPrimary, EyedropperToolMessage::SamplePrimaryColorEnd) | (EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::SampleSecondaryColorEnd) => { let set_color_choice = if self == EyedropperToolFsmState::SamplingPrimary { "Primary" } else { "Secondary" }.to_string(); - update_cursor_preview(responses, input, global_tool_data, Some(set_color_choice)); - disable_cursor_preview(responses); + update_cursor_preview(responses, tool_data, input, global_tool_data, Some(set_color_choice)); + disable_cursor_preview(responses, tool_data); EyedropperToolFsmState::Ready } // Any -> Ready (_, EyedropperToolMessage::Abort) => { - disable_cursor_preview(responses); + disable_cursor_preview(responses, tool_data); EyedropperToolFsmState::Ready } @@ -164,7 +165,8 @@ impl Fsm for EyedropperToolFsmState { } } -fn disable_cursor_preview(responses: &mut VecDeque) { +fn disable_cursor_preview(responses: &mut VecDeque, tool_data: &mut EyedropperToolData) { + tool_data.preview = false; responses.add(FrontendMessage::UpdateEyedropperSamplingState { image: None, mouse_position: None, @@ -174,17 +176,44 @@ fn disable_cursor_preview(responses: &mut VecDeque) { }); } -fn update_cursor_preview(responses: &mut VecDeque, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option) { - #[cfg(target_family = "wasm")] +#[cfg(not(target_family = "wasm"))] +fn update_cursor_preview( + responses: &mut VecDeque, + tool_data: &mut EyedropperToolData, + _input: &InputPreprocessorMessageHandler, + _global_tool_data: &DocumentToolData, + set_color_choice: Option, +) { + tool_data.preview = true; + tool_data.color_choice = set_color_choice; + responses.add(PortfolioMessage::SubmitEyedropperPreviewRender); +} + +#[cfg(target_family = "wasm")] +fn update_cursor_preview( + responses: &mut VecDeque, + tool_data: &mut EyedropperToolData, + input: &InputPreprocessorMessageHandler, + global_tool_data: &DocumentToolData, + set_color_choice: Option, +) { + tool_data.preview = true; + tool_data.color_choice = set_color_choice.clone(); + update_cursor_preview_common(responses, None, input, global_tool_data, set_color_choice); +} + +fn update_cursor_preview_common( + responses: &mut VecDeque, + image: Option, + input: &InputPreprocessorMessageHandler, + global_tool_data: &DocumentToolData, + set_color_choice: Option, +) { responses.add(FrontendMessage::UpdateEyedropperSamplingState { - image: None, + image, mouse_position: Some(input.mouse.position.into()), primary_color: "#".to_string() + global_tool_data.primary_color.to_rgb_hex_srgb().as_str(), secondary_color: "#".to_string() + global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(), set_color_choice, }); - #[cfg(not(target_family = "wasm"))] - { - responses.add(PortfolioMessage::SubmitEyedropperPreviewRender); - } } From 7356f7e6bd7562061453d60ee249b155f8a69693 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 26 Jan 2026 18:39:27 +0000 Subject: [PATCH 3/7] kinda works but tilt and flip broken --- .../messages/portfolio/portfolio_message_handler.rs | 10 ++++++++-- editor/src/node_graph_executor.rs | 9 +++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 819ff3deaa..7f33103015 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1192,9 +1192,15 @@ impl MessageHandler> for Portfolio .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); - let resolution = glam::UVec2::splat(11); + const PREVIEW_RESOLUTION: u32 = 11; + let resolution = glam::UVec2::splat(PREVIEW_RESOLUTION); + let pointer_offset = -(glam::DVec2::splat(PREVIEW_RESOLUTION as f64 / 2.0) / document.document_ptz.zoom()); - let result = self.executor.submit_eyedropper_preview(document_id, resolution, timing_information, pointer_position); + let pointer_position = pointer_position + pointer_offset; + + let result = self + .executor + .submit_eyedropper_preview(document_id, resolution, document.document_ptz.zoom(), timing_information, pointer_position); match result { Err(description) => { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 3a9357d78a..cf6dc1f63e 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -194,16 +194,17 @@ impl NodeGraphExecutor { } #[cfg(not(target_family = "wasm"))] - pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, resolution: UVec2, time: TimingInformation, pointer: DVec2) -> Result { - // footprint from pointer position and resolution + pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, resolution: UVec2, zoom: f64, time: TimingInformation, pointer: DVec2) -> Result { + let transform = DAffine2::from_translation(pointer).inverse(); + let viewport = Footprint { - transform: DAffine2::from_translation(pointer).inverse(), + transform, resolution, ..Default::default() }; let render_config = RenderConfig { viewport, - scale: 1., + scale: zoom, time, pointer, export_format: graphene_std::application_io::ExportFormat::Raster, From 49c52970c25d8f1366a1236cc805629d4e7a109a Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 26 Jan 2026 22:14:10 +0000 Subject: [PATCH 4/7] fix footprint Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com> --- .../portfolio/portfolio_message_handler.rs | 17 +++++++++-------- editor/src/node_graph_executor.rs | 6 ++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 7f33103015..704aed497a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1187,20 +1187,21 @@ impl MessageHandler> for Portfolio return; }; - let document_to_viewport = document - .navigation_handler - .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); - let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); - const PREVIEW_RESOLUTION: u32 = 11; let resolution = glam::UVec2::splat(PREVIEW_RESOLUTION); - let pointer_offset = -(glam::DVec2::splat(PREVIEW_RESOLUTION as f64 / 2.0) / document.document_ptz.zoom()); - let pointer_position = pointer_position + pointer_offset; + let preview_offset_in_viewport = ipp.mouse.position - (glam::DVec2::splat(PREVIEW_RESOLUTION as f64 / 2.0)); + let preview_offset_in_viewport = DAffine2::from_translation(preview_offset_in_viewport); + + let document_to_viewport = document.metadata().document_to_viewport; + + let preview_transform = preview_offset_in_viewport.inverse() * document_to_viewport; + + let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); let result = self .executor - .submit_eyedropper_preview(document_id, resolution, document.document_ptz.zoom(), timing_information, pointer_position); + .submit_eyedropper_preview(document_id, preview_transform, resolution, timing_information, pointer_position); match result { Err(description) => { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index cf6dc1f63e..7e56c7ceb0 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -194,9 +194,7 @@ impl NodeGraphExecutor { } #[cfg(not(target_family = "wasm"))] - pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, resolution: UVec2, zoom: f64, time: TimingInformation, pointer: DVec2) -> Result { - let transform = DAffine2::from_translation(pointer).inverse(); - + pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, transform: DAffine2, resolution: UVec2, time: TimingInformation, pointer: DVec2) -> Result { let viewport = Footprint { transform, resolution, @@ -204,7 +202,7 @@ impl NodeGraphExecutor { }; let render_config = RenderConfig { viewport, - scale: zoom, + scale: 1., time, pointer, export_format: graphene_std::application_io::ExportFormat::Raster, From 88836aa25009f8aa1411c2c54c2079e1e874ce3a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 26 Jan 2026 16:35:28 -0800 Subject: [PATCH 5/7] Code review --- editor/src/consts.rs | 3 +++ .../portfolio/portfolio_message_handler.rs | 20 ++++++-------- .../tool/tool_messages/eyedropper_tool.rs | 6 ++++- editor/src/node_graph_executor.rs | 6 ++--- .../src/components/panels/Document.svelte | 27 +++++++++---------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index bfe61f48f3..cd13946d0d 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -124,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; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 704aed497a..42a5f65b5a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1180,28 +1180,24 @@ impl MessageHandler> for Portfolio } #[cfg(not(target_family = "wasm"))] PortfolioMessage::SubmitEyedropperPreviewRender => { - let Some(document_id) = self.active_document_id else { - return; - }; - let Some(document) = self.documents.get_mut(&document_id) else { - return; - }; + use crate::consts::EYEDROPPER_PREVIEW_AREA_RESOLUTION; + + let Some(document_id) = self.active_document_id else { return }; + let Some(document) = self.documents.get_mut(&document_id) else { return }; - const PREVIEW_RESOLUTION: u32 = 11; - let resolution = glam::UVec2::splat(PREVIEW_RESOLUTION); + let resolution = glam::UVec2::splat(EYEDROPPER_PREVIEW_AREA_RESOLUTION); - let preview_offset_in_viewport = ipp.mouse.position - (glam::DVec2::splat(PREVIEW_RESOLUTION as f64 / 2.0)); + let preview_offset_in_viewport = ipp.mouse.position - (glam::DVec2::splat(EYEDROPPER_PREVIEW_AREA_RESOLUTION as f64 / 2.)); let preview_offset_in_viewport = DAffine2::from_translation(preview_offset_in_viewport); let document_to_viewport = document.metadata().document_to_viewport; let preview_transform = preview_offset_in_viewport.inverse() * document_to_viewport; - let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position); let result = self .executor - .submit_eyedropper_preview(document_id, preview_transform, resolution, timing_information, pointer_position); + .submit_eyedropper_preview(document_id, preview_transform, pointer_position, resolution, timing_information); match result { Err(description) => { @@ -1215,7 +1211,7 @@ impl MessageHandler> for Portfolio } #[cfg(target_family = "wasm")] PortfolioMessage::SubmitEyedropperPreviewRender => { - // For wasm this is implemented through svg rendering + // TODO: Currently for Wasm, this is implemented through SVG rendering but the Eyedropper tool doesn't work at all when Vello is enabled as the renderer } PortfolioMessage::ToggleFocusDocument => { self.focus_document = !self.focus_document; diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index c5e0536be1..eddb7cb863 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -1,5 +1,6 @@ use super::tool_prelude::*; -use crate::messages::{frontend::utility_types::EyedropperPreviewImage, tool::utility_types::DocumentToolData}; +use crate::messages::frontend::utility_types::EyedropperPreviewImage; +use crate::messages::tool::utility_types::DocumentToolData; #[derive(Default, ExtractField)] pub struct EyedropperTool { @@ -46,7 +47,9 @@ impl<'a> MessageHandler> for Eyed fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { if let ToolMessage::Eyedropper(EyedropperToolMessage::PreviewImage { data, width, height }) = message { let image = EyedropperPreviewImage { data, width, height }; + update_cursor_preview_common(responses, Some(image), context.input, context.global_tool_data, self.data.color_choice.clone()); + if !self.data.preview { disable_cursor_preview(responses, &mut self.data); } @@ -199,6 +202,7 @@ fn update_cursor_preview( ) { tool_data.preview = true; tool_data.color_choice = set_color_choice.clone(); + update_cursor_preview_common(responses, None, input, global_tool_data, set_color_choice); } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 7e56c7ceb0..2b78e595a6 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -194,7 +194,7 @@ impl NodeGraphExecutor { } #[cfg(not(target_family = "wasm"))] - pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, transform: DAffine2, resolution: UVec2, time: TimingInformation, pointer: DVec2) -> Result { + pub(crate) fn submit_eyedropper_preview(&mut self, document_id: DocumentId, transform: DAffine2, pointer: DVec2, resolution: UVec2, time: TimingInformation) -> Result { let viewport = Footprint { transform, resolution, @@ -326,7 +326,7 @@ impl NodeGraphExecutor { // Special handling for exporting the artwork self.process_export(node_graph_output, export_config, responses)?; } else if execution_context.render_config.for_eyedropper { - // Special handling for eyedropper preview + // Special handling for Eyedropper tool preview self.process_eyedropper_preview(node_graph_output, responses)?; } else { self.process_node_graph_output(node_graph_output, responses)?; @@ -439,7 +439,7 @@ impl NodeGraphExecutor { responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); } _ => { - return Err(format!("Incorrect render type for eyedropper: {node_graph_output}")); + // TODO: Support Eyedropper preview in SVG mode on desktop } }; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index d4f21161af..8d1948a5ea 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -213,13 +213,12 @@ } export async function updateEyedropperSamplingState( + // `image` is currently only used for Vello renders image: ImageData | undefined, mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string, ): Promise<[number, number, number] | undefined> { - var preview = image; - if (mousePosition === undefined) { cursorEyedropper = false; return undefined; @@ -231,7 +230,8 @@ cursorLeft = mousePosition.x; cursorTop = mousePosition.y; - if (image === undefined) { + let preview = image; + if (!preview) { // This works nearly perfectly, but sometimes at odd DPI scale factors like 1.25, the anti-aliasing color can yield slightly incorrect colors (potential room for future improvement) const dpiFactor = window.devicePixelRatio; const [width, height] = [canvasWidth, canvasHeight]; @@ -255,10 +255,11 @@ ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS, ); + if (!preview) return undefined; } - const getCenterPixel = (imageData: ImageData) => { - const { width, height, data } = imageData; + const centerPixel = (() => { + const { width, height, data } = preview; const x = Math.floor(width / 2); const y = Math.floor(height / 2); const index = (y * width + x) * 4; @@ -267,17 +268,14 @@ g: data[index + 1], b: data[index + 2], }; - }; - if (!preview) return undefined; - const pixel = getCenterPixel(preview); - const rgbToHex = (r: number, g: number, b: number): string => `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`; - const hex = rgbToHex(pixel.r, pixel.g, pixel.b); - const rgb: [number, number, number] = [pixel.r / 255, pixel.g / 255, pixel.b / 255]; + })(); + const hex = [centerPixel.r, centerPixel.g, centerPixel.b].map((x) => x.toString(16).padStart(2, "0")).join(""); + const rgb: [number, number, number] = [centerPixel.r / 255, centerPixel.g / 255, centerPixel.b / 255]; - cursorEyedropperPreviewColorChoice = hex; + cursorEyedropperPreviewColorChoice = "#" + hex; cursorEyedropperPreviewColorPrimary = colorPrimary; cursorEyedropperPreviewColorSecondary = colorSecondary; - cursorEyedropperPreviewImageData = image; + cursorEyedropperPreviewImageData = preview; return rgb; } @@ -298,6 +296,7 @@ // Update mouse cursor icon export function updateMouseCursor(cursor: MouseCursorIcon) { + console.log("Updating mouse cursor to:", cursor); let cursorString: string = cursor; // This isn't very clean but it's good enough for now until we need more icons, then we can build something more robust (consider blob URLs) @@ -398,7 +397,7 @@ canvasWidth = Math.ceil(parseFloat(getComputedStyle(viewport).width)); canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height)); - devicePixelRatio = window.devicePixelRatio || 1.0; + devicePixelRatio = window.devicePixelRatio || 1; // Resize the rulers rulerHorizontal?.resize(); From 91b908131db1f9fe6905799bca6e9b02ac23ea72 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 27 Jan 2026 00:55:05 +0000 Subject: [PATCH 6/7] fix cursor hiding --- desktop/src/cef/internal/display_handler.rs | 5 ++++- desktop/src/window.rs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/desktop/src/cef/internal/display_handler.rs b/desktop/src/cef/internal/display_handler.rs index d1263e854c..bd99c29570 100644 --- a/desktop/src/cef/internal/display_handler.rs +++ b/desktop/src/cef/internal/display_handler.rs @@ -78,7 +78,6 @@ impl ImplDisplayHandler for DisplayHandlerImpl { CT_PROGRESS => CursorIcon::Progress, CT_NODROP => CursorIcon::NoDrop, CT_COPY => CursorIcon::Copy, - CT_NONE => CursorIcon::Default, CT_NOTALLOWED => CursorIcon::NotAllowed, CT_ZOOMIN => CursorIcon::ZoomIn, CT_ZOOMOUT => CursorIcon::ZoomOut, @@ -91,6 +90,10 @@ impl ImplDisplayHandler for DisplayHandlerImpl { CT_DND_COPY => CursorIcon::Copy, CT_DND_LINK => CursorIcon::Alias, CT_NUM_VALUES => CursorIcon::Default, + CT_NONE => { + self.event_handler.cursor_change(crate::window::Cursor::None); + return 1; // We handled the cursor change. + } _ => CursorIcon::Default, }; diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 4540e56200..a9b48e38fc 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -169,7 +169,12 @@ impl Window { }; custom_cursor.into() } + Cursor::None => { + self.winit_window.set_cursor_visible(false); + return; + } }; + self.winit_window.set_cursor_visible(true); self.winit_window.set_cursor(cursor); } @@ -215,6 +220,7 @@ impl Window { pub(crate) enum Cursor { Icon(CursorIcon), Custom(CustomCursorSource), + None, } impl From for Cursor { fn from(icon: CursorIcon) -> Self { From 80f0b9a119c4b9bccacf6fafde65d9b3de11e10b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 26 Jan 2026 17:08:35 -0800 Subject: [PATCH 7/7] Remove console.log --- frontend/src/components/panels/Document.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 8d1948a5ea..98722c4d76 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -296,7 +296,6 @@ // Update mouse cursor icon export function updateMouseCursor(cursor: MouseCursorIcon) { - console.log("Updating mouse cursor to:", cursor); let cursorString: string = cursor; // This isn't very clean but it's good enough for now until we need more icons, then we can build something more robust (consider blob URLs)