From 85c635f92d9e8dd80467c0ff0c4e12592adc0a05 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 28 Oct 2022 18:36:04 -0700 Subject: [PATCH] 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 --- .../messages/input_mapper/default_mapping.rs | 2 +- .../messages/layout/layout_message_handler.rs | 20 +- .../utility_types/widgets/input_widgets.rs | 9 +- .../properties_panel/utility_functions.rs | 50 +- .../tool/tool_messages/eyedropper_tool.rs | 11 +- .../assets/icon-16px-solid/eyedropper.svg | 4 + frontend/src/App.vue | 15 + .../components/floating-menus/ColorPicker.vue | 477 ++++++++++++++++-- .../floating-menus/EyedropperPreview.vue | 18 +- .../src/components/layout/FloatingMenu.vue | 32 +- frontend/src/components/layout/LayoutCol.vue | 9 +- frontend/src/components/layout/LayoutRow.vue | 9 +- frontend/src/components/panels/Document.vue | 4 +- frontend/src/components/widgets/WidgetRow.vue | 2 +- .../widgets/groups/WidgetSection.vue | 14 +- .../components/widgets/inputs/ColorInput.vue | 181 +++---- .../components/widgets/inputs/FieldInput.vue | 4 +- .../widgets/inputs/SwatchPairInput.vue | 17 +- .../components/widgets/inputs/TextInput.vue | 8 + frontend/src/utility-functions/icons.ts | 4 +- frontend/src/wasm-communication/messages.ts | 196 ++++++- frontend/wasm/src/editor_api.rs | 9 + graphene/src/layers/style/mod.rs | 14 +- 23 files changed, 830 insertions(+), 279 deletions(-) create mode 100644 frontend/assets/icon-16px-solid/eyedropper.svg diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index a43766fe..1810a607 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -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), diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 5058c56f..e6dfc51f 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -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 Vec> MessageHandler { - 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); } diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index f80735af..8ac3f7e7 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -39,13 +39,12 @@ impl Default for CheckboxInput { #[derive(Clone, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq, Default)] pub struct ColorInput { - pub value: Option, - - pub label: Option, + pub value: Option, + // 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, diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index 14bbf10d..f0bbe900 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -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 { 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() diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index b65a6472..874276f4 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -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()); diff --git a/frontend/assets/icon-16px-solid/eyedropper.svg b/frontend/assets/icon-16px-solid/eyedropper.svg new file mode 100644 index 00000000..9da1b902 --- /dev/null +++ b/frontend/assets/icon-16px-solid/eyedropper.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a1397e0e..49a2365e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -67,6 +67,21 @@ --color-data-unused1-rgb: 214, 83, 110; --color-data-unused2: #70a898; --color-data-unused2-rgb: 112, 168, 152; + + --color-none: white; + --color-none-repeat: no-repeat; + --color-none-position: center center; + // 24px tall, 48px wide + --color-none-size-24px: 60px 24px; + --color-none-image-24px: url('data:image/svg+xml;utf8,'); + // 32px tall, 64px wide + --color-none-size-32px: 80px 32px; + --color-none-image-32px: url('data:image/svg+xml;utf8,'); + + --transparent-checkered-background: linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), + linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff); + --transparent-checkered-background-size: 16px 16px; + --transparent-checkered-background-position: 0 0, 8px 8px; } html, diff --git a/frontend/src/components/floating-menus/ColorPicker.vue b/frontend/src/components/floating-menus/ColorPicker.vue index 236938d2..f870e07c 100644 --- a/frontend/src/components/floating-menus/ColorPicker.vue +++ b/frontend/src/components/floating-menus/ColorPicker.vue @@ -1,14 +1,130 @@