Desktop: Add Eyedropper tool support with native Vello (#3684)

* mostly done

* fix

* kinda works but tilt and flip broken

* fix footprint

Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com>

* Code review

* fix cursor hiding

* Remove console.log

---------

Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2026-01-27 01:24:09 +00:00
committed by GitHub
co-authored by James Lindsay Keavon Chambers
parent 19e9af3d43
commit 5fd1a24f16
13 changed files with 340 additions and 135 deletions
@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
use crate::messages::tool::utility_types::DocumentToolData;
#[derive(Default, ExtractField)]
@@ -19,6 +20,8 @@ pub enum EyedropperToolMessage {
PointerMove,
SampleSecondaryColorBegin,
SampleSecondaryColorEnd,
PreviewImage { data: Vec<u8>, width: u32, height: u32 },
}
impl ToolMetadata for EyedropperTool {
@@ -42,6 +45,17 @@ impl LayoutHolder for EyedropperTool {
#[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for EyedropperTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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);
}
return;
}
self.fsm_state.process_event(message, &mut self.data, context, &(), responses, true);
}
@@ -74,13 +88,16 @@ enum EyedropperToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct EyedropperToolData {}
struct EyedropperToolData {
preview: bool,
color_choice: Option<String>,
}
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<Message>) -> Self {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionMessageContext {
global_tool_data, input, viewport, ..
} = tool_action_data;
@@ -89,7 +106,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
@@ -101,9 +118,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
@@ -111,14 +128,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
}
@@ -151,8 +168,10 @@ impl Fsm for EyedropperToolFsmState {
}
}
fn disable_cursor_preview(responses: &mut VecDeque<Message>) {
fn disable_cursor_preview(responses: &mut VecDeque<Message>, tool_data: &mut EyedropperToolData) {
tool_data.preview = false;
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
image: None,
mouse_position: None,
primary_color: "".into(),
secondary_color: "".into(),
@@ -160,8 +179,42 @@ fn disable_cursor_preview(responses: &mut VecDeque<Message>) {
});
}
fn update_cursor_preview(responses: &mut VecDeque<Message>, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option<String>) {
#[cfg(not(target_family = "wasm"))]
fn update_cursor_preview(
responses: &mut VecDeque<Message>,
tool_data: &mut EyedropperToolData,
_input: &InputPreprocessorMessageHandler,
_global_tool_data: &DocumentToolData,
set_color_choice: Option<String>,
) {
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<Message>,
tool_data: &mut EyedropperToolData,
input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData,
set_color_choice: Option<String>,
) {
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<Message>,
image: Option<EyedropperPreviewImage>,
input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData,
set_color_choice: Option<String>,
) {
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
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(),