Graphite/editor/src/messages/tool/tool_messages/eyedropper_tool.rs

241 lines
7.8 KiB
Rust

use super::tool_prelude::*;
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
use crate::messages::tool::utility_types::DocumentToolData;
use graphene_std::vector::style::RenderMode;
#[derive(Default, ExtractField)]
pub struct EyedropperTool {
fsm_state: EyedropperToolFsmState,
data: EyedropperToolData,
}
#[impl_message(Message, ToolMessage, Eyedropper)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
pub enum EyedropperToolMessage {
// Standard messages
Abort,
// Tool-specific messages
SamplePrimaryColorBegin,
SamplePrimaryColorEnd,
PointerMove,
SampleSecondaryColorBegin,
SampleSecondaryColorEnd,
PreviewImage { data: Vec<u8>, width: u32, height: u32 },
}
impl ToolMetadata for EyedropperTool {
fn icon_name(&self) -> String {
"GeneralEyedropperTool".into()
}
fn tooltip_label(&self) -> String {
"Eyedropper Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Eyedropper
}
}
impl LayoutHolder for EyedropperTool {
fn layout(&self) -> Layout {
Layout::default()
}
}
#[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: data.into(), width, height };
update_cursor_preview_common(responses, Some(image), context.input, context.global_tool_data, self.data.color_choice);
if !self.data.preview {
disable_cursor_preview(responses, &mut self.data);
}
return;
}
self.fsm_state.process_event(message, &mut self.data, context, &(), responses, true);
}
advertise_actions!(EyedropperToolMessageDiscriminant;
SamplePrimaryColorBegin,
SamplePrimaryColorEnd,
SampleSecondaryColorBegin,
SampleSecondaryColorEnd,
PointerMove,
Abort,
);
}
impl ToolTransition for EyedropperTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
tool_abort: Some(EyedropperToolMessage::Abort.into()),
working_color_changed: Some(EyedropperToolMessage::PointerMove.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum EyedropperToolFsmState {
#[default]
Ready,
SamplingPrimary,
SamplingSecondary,
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PrimarySecondary {
#[default]
Primary,
Secondary,
}
#[derive(Clone, Debug, Default)]
struct EyedropperToolData {
preview: bool,
color_choice: Option<PrimarySecondary>,
}
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 {
let ToolActionMessageContext {
document,
global_tool_data,
input,
viewport,
..
} = tool_action_data;
let render_mode = document.render_mode;
let ToolMessage::Eyedropper(event) = event else { return self };
match (self, event) {
// Ready -> Sampling
(EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => {
update_cursor_preview(responses, tool_data, input, global_tool_data, None, render_mode);
if mouse_down == EyedropperToolMessage::SamplePrimaryColorBegin {
EyedropperToolFsmState::SamplingPrimary
} else {
EyedropperToolFsmState::SamplingSecondary
}
}
// Sampling -> Sampling
(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, tool_data, input, global_tool_data, None, render_mode);
} else {
disable_cursor_preview(responses, tool_data);
}
self
}
// Sampling -> Ready
(EyedropperToolFsmState::SamplingPrimary, EyedropperToolMessage::SamplePrimaryColorEnd) | (EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::SampleSecondaryColorEnd) => {
let set_color_choice = match self {
EyedropperToolFsmState::SamplingPrimary => PrimarySecondary::Primary,
EyedropperToolFsmState::SamplingSecondary => PrimarySecondary::Secondary,
_ => unreachable!(),
};
update_cursor_preview(responses, tool_data, input, global_tool_data, Some(set_color_choice), render_mode);
disable_cursor_preview(responses, tool_data);
EyedropperToolFsmState::Ready
}
// Any -> Ready
(_, EyedropperToolMessage::Abort) => {
disable_cursor_preview(responses, tool_data);
EyedropperToolFsmState::Ready
}
// Ready -> Ready
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::Lmb, "Sample to Primary"),
HintInfo::keys_and_mouse([Key::Shift], MouseMotion::Lmb, "Sample to Secondary"),
])]),
EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => {
HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])])
}
};
hint_data.send_layout(responses);
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
let cursor = match *self {
EyedropperToolFsmState::Ready => MouseCursorIcon::Default,
EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => MouseCursorIcon::None,
};
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
}
}
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(),
set_color_choice: None,
});
}
fn update_cursor_preview(
responses: &mut VecDeque<Message>,
tool_data: &mut EyedropperToolData,
input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData,
set_color_choice: Option<PrimarySecondary>,
render_mode: RenderMode,
) {
tool_data.preview = true;
tool_data.color_choice = set_color_choice;
// On web, SVG Preview mode uses the frontend's SVG rasterization to sample pixels directly
#[cfg(target_family = "wasm")]
if render_mode == RenderMode::SvgPreview {
update_cursor_preview_common(responses, None, input, global_tool_data, set_color_choice);
return;
}
let _ = (&input, &global_tool_data, &render_mode);
// For Vello-rendered modes (Normal, Outline, and Pixel Preview), submit a backend render request
// which will return a zoomed-in pixel preview image via the EyedropperToolMessage::PreviewImage path
responses.add(PortfolioMessage::SubmitEyedropperPreviewRender);
}
fn update_cursor_preview_common(
responses: &mut VecDeque<Message>,
image: Option<EyedropperPreviewImage>,
input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData,
set_color_choice: Option<PrimarySecondary>,
) {
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(),
set_color_choice,
});
}