mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 07:48:02 +08:00
Revamp the ColorPicker popover and ColorInput widget (#830)
* Add cancel hint to Eyedropper tool * Improve eyedropper overlay CSS * Make CSS for transparent checkered background reusable * Add color choice preview to color picker * Draw text and markers as contrasting white or black * Add reactive color updating and new/initial swapping * Add Hex, RGB, HSV, and Opacity inputs * Add none color and preset buttons * Add eyedropper button and fix alignment (now visually done) * Wire up none colors through the backend and style the ColorInput widget * Add color info chip to ColorInput widget * Fix all UX bugs * Add more tooltips * Fix FloatingMenu recursive loop * Prevent mouse stray from closing color picker while dragging pickers Closes #703 * Fix deselect all layers shortcut * Add temporary eyedropper for Chromium browsers and a coming soon fallback
This commit is contained in:
@@ -201,7 +201,7 @@ pub fn default_mapping() -> Mapping {
|
||||
entry!(KeyDown(KeyP); modifiers=[Alt], action_dispatch=DocumentMessage::DebugPrintDocument),
|
||||
entry!(KeyDown(KeyZ); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::Redo),
|
||||
entry!(KeyDown(KeyZ); modifiers=[Accel], action_dispatch=DocumentMessage::Undo),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=DocumentMessage::SelectAllLayers),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::messages::layout::utility_types::layout_widget::Layout;
|
||||
use crate::messages::layout::utility_types::layout_widget::Widget;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::text_layer::Font;
|
||||
|
||||
use serde_json::Value;
|
||||
@@ -60,8 +61,23 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::ColorInput(color_input) => {
|
||||
let update_value = value.as_str().map(String::from);
|
||||
color_input.value = update_value;
|
||||
let update_value = value.as_object().expect("ColorInput update was not of type: object");
|
||||
let parsed_color = (|| {
|
||||
let is_none = update_value.get("none")?.as_bool()?;
|
||||
|
||||
if !is_none {
|
||||
Some(Some(Color::from_rgbaf32(
|
||||
update_value.get("red")?.as_f64()? as f32,
|
||||
update_value.get("green")?.as_f64()? as f32,
|
||||
update_value.get("blue")?.as_f64()? as f32,
|
||||
update_value.get("alpha")?.as_f64()? as f32,
|
||||
)?))
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
})()
|
||||
.unwrap_or_else(|| panic!("ColorInput update was not able to be parsed with color data: {:?}", color_input));
|
||||
color_input.value = parsed_color;
|
||||
let callback_message = (color_input.on_update.callback)(color_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
|
||||
@@ -39,13 +39,12 @@ impl Default for CheckboxInput {
|
||||
#[derive(Clone, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorInput {
|
||||
pub value: Option<String>,
|
||||
|
||||
pub label: Option<String>,
|
||||
pub value: Option<Color>,
|
||||
|
||||
// TODO: Add allow_none
|
||||
#[serde(rename = "noTransparency")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub no_transparency: bool,
|
||||
pub no_transparency: bool, // TODO: Rename allow_transparency (and invert usages)
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
@@ -295,6 +294,8 @@ pub struct TextInput {
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
pub centered: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel,
|
||||
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::document::pick_layer_safe_imaginate_resolution;
|
||||
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
|
||||
use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
|
||||
@@ -190,18 +189,10 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||
value: Some(color.rgba_hex()),
|
||||
value: Some(*color),
|
||||
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
||||
if let Some(value) = &text_input.value {
|
||||
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||
let new_fill = Fill::Solid(color);
|
||||
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
||||
} else {
|
||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||
}
|
||||
} else {
|
||||
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
|
||||
}
|
||||
let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None };
|
||||
PropertiesPanelMessage::ModifyFill { fill }.into()
|
||||
}),
|
||||
no_transparency: true,
|
||||
..Default::default()
|
||||
@@ -1186,21 +1177,11 @@ fn node_gradient_color(gradient: &Gradient, percent_label: &'static str, positio
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||
value: gradient_clone.positions[position].1.map(|color| color.rgba_hex()),
|
||||
value: gradient_clone.positions[position].1,
|
||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||
if let Some(value) = &text_input.value {
|
||||
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||
let mut new_gradient = (*gradient_clone).clone();
|
||||
new_gradient.positions[position].1 = Some(color);
|
||||
send_fill_message(new_gradient)
|
||||
} else {
|
||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||
}
|
||||
} else {
|
||||
let mut new_gradient = (*gradient_clone).clone();
|
||||
new_gradient.positions[position].1 = None;
|
||||
send_fill_message(new_gradient)
|
||||
}
|
||||
let mut new_gradient = (*gradient_clone).clone();
|
||||
new_gradient.positions[position].1 = text_input.value;
|
||||
send_fill_message(new_gradient)
|
||||
}),
|
||||
..ColorInput::default()
|
||||
})),
|
||||
@@ -1223,18 +1204,10 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutGroup> {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||
value: if let Fill::Solid(color) = fill { Some(color.rgba_hex()) } else { None },
|
||||
value: if let Fill::Solid(color) = fill { Some(*color) } else { None },
|
||||
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
||||
if let Some(value) = &text_input.value {
|
||||
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||
let new_fill = Fill::Solid(color);
|
||||
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
||||
} else {
|
||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||
}
|
||||
} else {
|
||||
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
|
||||
}
|
||||
let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None };
|
||||
PropertiesPanelMessage::ModifyFill { fill }.into()
|
||||
}),
|
||||
..ColorInput::default()
|
||||
})),
|
||||
@@ -1276,7 +1249,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||
value: stroke.color().map(|color| color.rgba_hex()),
|
||||
value: stroke.color(),
|
||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||
internal_stroke1
|
||||
.clone()
|
||||
@@ -1324,6 +1297,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: stroke.dash_lengths(),
|
||||
centered: true,
|
||||
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
||||
internal_stroke3
|
||||
.clone()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
@@ -177,8 +177,13 @@ impl Fsm for EyedropperToolFsmState {
|
||||
plus: false,
|
||||
},
|
||||
])]),
|
||||
EyedropperToolFsmState::SamplingPrimary => HintData(vec![]),
|
||||
EyedropperToolFsmState::SamplingSecondary => HintData(vec![]),
|
||||
EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::Escape])],
|
||||
key_groups_mac: None,
|
||||
mouse: None,
|
||||
label: String::from("Cancel"),
|
||||
plus: false,
|
||||
}])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
|
||||
Reference in New Issue
Block a user