mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
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:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -78,6 +78,8 @@ impl DocumentToolData {
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(EyedropperToolMessage::PointerMove.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user