Remake Eyedropper tool to sample pixel colors from viewport canvas (#801)

* Remake Eyedropper tool to sample pixel colors from viewport canvas

* Bug fixes

* Reorder export buttons

* Remove the larger primary/secondary ring

* Add aborting with Escape
This commit is contained in:
Keavon Chambers
2022-10-21 01:50:02 -07:00
parent fe1a03fac7
commit 58a53a995d
17 changed files with 454 additions and 131 deletions

View File

@@ -63,7 +63,7 @@ impl PropertyHolder for ExportDialogMessageHandler {
})),
];
let entries = [(FileType::Svg, "SVG"), (FileType::Png, "PNG"), (FileType::Jpg, "JPG")]
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
.into_iter()
.map(|(val, name)| RadioEntryData {
label: name.into(),

View File

@@ -157,6 +157,16 @@ pub enum FrontendMessage {
size: (f64, f64),
multiplier: (f64, f64),
},
UpdateEyedropperSamplingState {
#[serde(rename = "mousePosition")]
mouse_position: Option<(f64, f64)>,
#[serde(rename = "primaryColor")]
primary_color: String,
#[serde(rename = "secondaryColor")]
secondary_color: String,
#[serde(rename = "setColorChoice")]
set_color_choice: Option<String>,
},
UpdateImageData {
#[serde(rename = "documentId")]
document_id: u64,

View File

@@ -21,6 +21,7 @@ pub struct FrontendImageData {
pub enum MouseCursorIcon {
#[default]
Default,
None,
ZoomIn,
ZoomOut,
Grabbing,
@@ -36,17 +37,17 @@ pub enum MouseCursorIcon {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum FileType {
#[default]
Svg,
Png,
Jpg,
Svg,
}
impl FileType {
pub fn to_mime(self) -> &'static str {
match self {
FileType::Svg => "image/svg+xml",
FileType::Png => "image/png",
FileType::Jpg => "image/jpeg",
FileType::Svg => "image/svg+xml",
}
}
}

View File

@@ -92,8 +92,12 @@ pub fn default_mapping() -> Mapping {
entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
//
// EyedropperToolMessage
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown),
entry!(PointerMove; action_dispatch=EyedropperToolMessage::PointerMove),
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftPointerDown),
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightPointerDown),
entry!(KeyUp(Lmb); action_dispatch=EyedropperToolMessage::LeftPointerUp),
entry!(KeyUp(Rmb); action_dispatch=EyedropperToolMessage::RightPointerUp),
entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort),
//
// TextToolMessage
entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
@@ -170,8 +174,8 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm),
//
// FillToolMessage
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown),
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftPointerDown),
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightPointerDown),
//
// ToolMessage
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),

View File

@@ -27,6 +27,10 @@ impl ViewportBounds {
pub fn center(&self) -> DVec2 {
self.bottom_right.lerp(self.top_left, 0.5)
}
pub fn in_bounds(&self, position: ViewportPosition) -> bool {
position.x >= 0. && position.y >= 0. && position.x <= self.bottom_right.x && position.y <= self.bottom_right.y
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]

View File

@@ -910,8 +910,8 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
\n\
Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
\n\
To boost the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
\"(colorless:0.7) green (ideas sleep:1.3) furiously\"
To boost (or lessen) the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
\"Colorless green ideas (sleep:1.3) furiously\"
"
.trim()
.into(),

View File

@@ -69,7 +69,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
return;
}
// Send the Abort state transition to the tool
// Send the old and new tools a transition to their FSM Abort states
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
@@ -82,8 +82,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
}
}
};
// Send the old and new tools a transition to their FSM Abort states
send_abort_to_tool(tool_type, true);
send_abort_to_tool(old_tool, false);

View File

@@ -1,15 +1,10 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
@@ -27,8 +22,11 @@ pub enum EyedropperToolMessage {
Abort,
// Tool-specific messages
LeftMouseDown,
RightMouseDown,
LeftPointerDown,
LeftPointerUp,
PointerMove,
RightPointerDown,
RightPointerUp,
}
impl ToolMetadata for EyedropperTool {
@@ -67,8 +65,12 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EyedropperTo
}
advertise_actions!(EyedropperToolMessageDiscriminant;
LeftMouseDown,
RightMouseDown,
LeftPointerDown,
LeftPointerUp,
PointerMove,
RightPointerDown,
RightPointerUp,
Abort,
);
}
@@ -85,6 +87,8 @@ impl ToolTransition for EyedropperTool {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EyedropperToolFsmState {
Ready,
SamplingPrimary,
SamplingSecondary,
}
impl Default for EyedropperToolFsmState {
@@ -104,7 +108,7 @@ impl Fsm for EyedropperToolFsmState {
self,
event: ToolMessage,
_tool_data: &mut Self::ToolData,
(document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
(_document, _document_id, global_tool_data, input, _font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
@@ -113,28 +117,41 @@ impl Fsm for EyedropperToolFsmState {
if let ToolMessage::Eyedropper(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
// Ready -> Sampling
(Ready, mouse_down) | (Ready, mouse_down) if mouse_down == LeftPointerDown || mouse_down == RightPointerDown => {
update_cursor_preview(responses, input, global_tool_data, None);
// TODO: Destroy this pyramid
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
if let Ok(layer) = document.graphene_document.layer(path) {
if let LayerDataType::Shape(shape) = &layer.data {
if shape.style.fill().is_some() {
match lmb_or_rmb {
EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: shape.style.fill().color() }.into()),
EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: shape.style.fill().color() }.into()),
_ => {}
}
}
}
}
if mouse_down == LeftPointerDown {
SamplingPrimary
} else {
SamplingSecondary
}
}
// Sampling -> Sampling
(SamplingPrimary, PointerMove) | (SamplingSecondary, PointerMove) => {
if input.viewport_bounds.in_bounds(input.mouse.position) {
update_cursor_preview(responses, input, global_tool_data, None);
} else {
disable_cursor_preview(responses);
}
self
}
// Sampling -> Ready
(SamplingPrimary, mouse_up) | (SamplingSecondary, mouse_up) if mouse_up == LeftPointerUp || mouse_up == RightPointerUp => {
let set_color_choice = if self == SamplingPrimary { "Primary".to_string() } else { "Secondary".to_string() };
update_cursor_preview(responses, input, global_tool_data, Some(set_color_choice));
disable_cursor_preview(responses);
Ready
}
// Any -> Ready
(_, Abort) => {
disable_cursor_preview(responses);
Ready
}
// Ready -> Ready
_ => self,
}
} else {
@@ -160,12 +177,43 @@ impl Fsm for EyedropperToolFsmState {
plus: false,
},
])]),
EyedropperToolFsmState::SamplingPrimary => HintData(vec![]),
EyedropperToolFsmState::SamplingSecondary => HintData(vec![]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
let cursor = match *self {
EyedropperToolFsmState::Ready => MouseCursorIcon::Default,
EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => MouseCursorIcon::None,
};
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
}
}
fn disable_cursor_preview(responses: &mut VecDeque<Message>) {
responses.push_back(
FrontendMessage::UpdateEyedropperSamplingState {
mouse_position: None,
primary_color: "".into(),
secondary_color: "".into(),
set_color_choice: None,
}
.into(),
);
}
fn update_cursor_preview(responses: &mut VecDeque<Message>, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option<String>) {
responses.push_back(
FrontendMessage::UpdateEyedropperSamplingState {
mouse_position: Some(input.mouse.position.into()),
primary_color: "#".to_string() + global_tool_data.primary_color.rgb_hex().as_str(),
secondary_color: "#".to_string() + global_tool_data.secondary_color.rgb_hex().as_str(),
set_color_choice,
}
.into(),
);
}

View File

@@ -28,8 +28,8 @@ pub enum FillToolMessage {
Abort,
// Tool-specific messages
LeftMouseDown,
RightMouseDown,
LeftPointerDown,
RightPointerDown,
}
impl ToolMetadata for FillTool {
@@ -68,8 +68,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FillTool {
}
advertise_actions!(FillToolMessageDiscriminant;
LeftMouseDown,
RightMouseDown,
LeftPointerDown,
RightPointerDown,
);
}
@@ -114,15 +114,15 @@ impl Fsm for FillToolFsmState {
if let ToolMessage::Fill(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftPointerDown || lmb_or_rmb == RightPointerDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
let color = match lmb_or_rmb {
LeftMouseDown => global_tool_data.primary_color,
RightMouseDown => global_tool_data.secondary_color,
LeftPointerDown => global_tool_data.primary_color,
RightPointerDown => global_tool_data.secondary_color,
Abort => unreachable!(),
};
let fill = Fill::Solid(color);

View File

@@ -78,6 +78,8 @@ impl DocumentToolData {
}
.into(),
);
responses.push_back(EyedropperToolMessage::PointerMove.into());
}
}