From ec8c8d648539a1f3ab1c63d2b937a87882e90f14 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 10 Feb 2025 05:46:41 -0800 Subject: [PATCH] Polish and add aborting to several input widgets: no Esc closing parent menus; color picker axis align; repeat on arrow buttons (#2276) * Remove color input outline; reduce antialiasing compositing artifacts in color widgets * Rename ColorButton to ColorInput * Add features and aborting to several other widgets - Prevent Esc from closing parent floating menus when aborting - Fix missing icon regression - Gutter resizing abort - Color picker aborts, Shift axis alignment, improve click/drag behavior for gradient spectrum - Scrollbar abort, repeat when held, fix directional arrows when viewport is zoomed - Number input abort, repeat when held * Move ColorInput into the inputs folder * Fix tiny logo --- .../messages/layout/layout_message_handler.rs | 6 +- .../layout/utility_types/layout_widget.rs | 6 +- .../utility_types/widgets/button_widgets.rs | 4 +- .../document/navigation/navigation_message.rs | 2 + .../navigation/navigation_message_handler.rs | 23 ++ .../node_graph/document_node_definitions.rs | 6 +- .../node_graph/node_graph_message_handler.rs | 4 +- .../document/node_graph/node_properties.rs | 24 +- .../document/overlays/grid_overlays.rs | 4 +- .../common_functionality/color_selector.rs | 4 +- .../messages/tool/tool_messages/brush_tool.rs | 2 +- .../tool/tool_messages/ellipse_tool.rs | 4 +- .../tool/tool_messages/freehand_tool.rs | 4 +- .../messages/tool/tool_messages/line_tool.rs | 2 +- .../messages/tool/tool_messages/pen_tool.rs | 4 +- .../tool/tool_messages/polygon_tool.rs | 4 +- .../tool/tool_messages/rectangle_tool.rs | 4 +- .../tool/tool_messages/spline_tool.rs | 4 +- .../messages/tool/tool_messages/text_tool.rs | 2 +- frontend/src/components/Editor.svelte | 1 + .../floating-menus/ColorPicker.svelte | 215 +++++++++-- .../src/components/layout/FloatingMenu.svelte | 35 +- .../src/components/panels/Document.svelte | 32 +- frontend/src/components/panels/Layers.svelte | 2 +- .../src/components/widgets/WidgetSpan.svelte | 6 +- .../widgets/buttons/ColorButton.svelte | 129 ------- .../widgets/inputs/ColorInput.svelte | 141 ++++++++ .../widgets/inputs/FieldInput.svelte | 11 +- .../widgets/inputs/NumberInput.svelte | 92 ++++- .../widgets/inputs/ScrollbarInput.svelte | 338 +++++++++++------- .../widgets/inputs/SpectrumInput.svelte | 133 +++++-- .../widgets/inputs/WorkingColorsInput.svelte | 56 ++- .../widgets/labels/IconLabel.svelte | 11 +- .../components/window/workspace/Panel.svelte | 6 +- .../window/workspace/Workspace.svelte | 70 +++- frontend/src/io-managers/input.ts | 26 +- frontend/src/messages.ts | 49 ++- frontend/wasm/src/editor_api.rs | 17 +- 38 files changed, 1031 insertions(+), 452 deletions(-) delete mode 100644 frontend/src/components/widgets/buttons/ColorButton.svelte create mode 100644 frontend/src/components/widgets/inputs/ColorInput.svelte diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index b80301ecf2..a5b130b529 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -80,7 +80,7 @@ impl LayoutMessageHandler { }; responses.add(callback_message); } - Widget::ColorButton(color_button) => { + Widget::ColorInput(color_button) => { let callback_message = match action { WidgetValueAction::Commit => (color_button.on_commit.callback)(&()), WidgetValueAction::Update => { @@ -99,7 +99,7 @@ impl LayoutMessageHandler { }; (|| { - let update_value = value.as_object().expect("ColorButton update was not of type: object"); + let update_value = value.as_object().expect("ColorInput update was not of type: object"); // None let is_none = update_value.get("none").and_then(|x| x.as_bool()); @@ -139,7 +139,7 @@ impl LayoutMessageHandler { return (color_button.on_update.callback)(color_button); } - panic!("ColorButton update was not able to be parsed with color data: {color_button:?}"); + panic!("ColorInput update was not able to be parsed with color data: {color_button:?}"); })() } }; diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 4b0acd3808..9b580b1b7e 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -327,7 +327,7 @@ impl LayoutGroup { for widget in &mut widgets { let val = match &mut widget.widget { Widget::CheckboxInput(x) => &mut x.tooltip, - Widget::ColorButton(x) => &mut x.tooltip, + Widget::ColorInput(x) => &mut x.tooltip, Widget::CurveInput(x) => &mut x.tooltip, Widget::DropdownInput(x) => &mut x.tooltip, Widget::FontInput(x) => &mut x.tooltip, @@ -498,7 +498,7 @@ impl Default for WidgetCallback { pub enum Widget { BreadcrumbTrailButtons(BreadcrumbTrailButtons), CheckboxInput(CheckboxInput), - ColorButton(ColorButton), + ColorInput(ColorInput), CurveInput(CurveInput), DropdownInput(DropdownInput), FontInput(FontInput), @@ -571,7 +571,7 @@ impl DiffUpdate { let mut tooltip_shortcut = match &mut widget_holder.widget { Widget::BreadcrumbTrailButtons(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::ColorButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index e4ec11eb3c..8854667ecf 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -148,7 +148,7 @@ pub struct ImageButton { #[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] #[derivative(Debug, PartialEq, Default)] -pub struct ColorButton { +pub struct ColorInput { #[widget_builder(constructor)] pub value: FillChoice, @@ -174,7 +174,7 @@ pub struct ColorButton { // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub on_update: WidgetCallback, #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message.rs b/editor/src/messages/portfolio/document/navigation/navigation_message.rs index 13e22d274a..b5c4095222 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message.rs @@ -11,6 +11,8 @@ pub enum NavigationMessage { BeginCanvasTilt { was_dispatched_from_menu: bool }, BeginCanvasZoom, CanvasPan { delta: DVec2 }, + CanvasPanAbortPrepare { x_not_y_axis: bool }, + CanvasPanAbort { x_not_y_axis: bool }, CanvasPanByViewportFraction { delta: DVec2 }, CanvasPanMouseWheel { use_y_as_x: bool }, CanvasTiltResetAndZoomTo100Percent, diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index 9bff84b35d..10e4564a26 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -29,6 +29,7 @@ pub struct NavigationMessageHandler { navigation_operation: NavigationOperation, mouse_position: ViewportPosition, finish_operation_with_click: bool, + abortable_pan_start: Option, } impl MessageHandler> for NavigationMessageHandler { @@ -141,6 +142,28 @@ impl MessageHandler> for Navigation responses.add(BroadcastEvent::CanvasTransformed); responses.add(DocumentMessage::PTZUpdate); } + NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => { + let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else { + log::error!("Could not get PTZ in CanvasPanAbortPrepare"); + return; + }; + self.abortable_pan_start = Some(if x_not_y_axis { ptz.pan.x } else { ptz.pan.y }); + } + NavigationMessage::CanvasPanAbort { x_not_y_axis } => { + let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else { + log::error!("Could not get PTZ in CanvasPanAbort"); + return; + }; + if let Some(abortable_pan_start) = self.abortable_pan_start { + if x_not_y_axis { + ptz.pan.x = abortable_pan_start; + } else { + ptz.pan.y = abortable_pan_start; + } + } + self.abortable_pan_start = None; + responses.add(DocumentMessage::PTZUpdate); + } NavigationMessage::CanvasPanByViewportFraction { delta } => { let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else { log::error!("Could not get node graph PTZ in CanvasPanByViewportFraction"); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 264ee3c9da..8d6ab11b8f 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -3303,7 +3303,7 @@ fn static_input_properties() -> InputProperties { "assign_colors_gradient".to_string(), Box::new(|node_id, index, context| { let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; - let gradient_row = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default().allow_none(false), true); + let gradient_row = node_properties::color_widget(document_node, node_id, index, input_name, ColorInput::default().allow_none(false), true); Ok(vec![gradient_row]) }), ); @@ -3329,7 +3329,7 @@ fn static_input_properties() -> InputProperties { "mask_stencil".to_string(), Box::new(|node_id, index, context| { let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; - let mask = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default(), true); + let mask = node_properties::color_widget(document_node, node_id, index, input_name, ColorInput::default(), true); Ok(vec![mask]) }), ); @@ -3403,7 +3403,7 @@ fn static_input_properties() -> InputProperties { node_id, index, input_name, - ColorButton::default().allow_none(false), + ColorInput::default().allow_none(false), true, )]) }), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 8a5075397d..4a452864ca 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1822,7 +1822,7 @@ impl NodeGraphMessageHandler { // If only one node is selected then show the preview or stop previewing button if let Some(node_id) = previewing { let button = TextButton::new("End Preview") - .icon(Some("Rescale".to_string())) + .icon(Some("FrameAll".to_string())) .tooltip("Restore preview to the graph output") .on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into()) .widget_holder(); @@ -1834,7 +1834,7 @@ impl NodeGraphMessageHandler { .any(|export| matches!(export, NodeInput::Node { node_id: export_node_id, .. } if *export_node_id == node_id)); if selection_is_not_already_the_output && no_other_selections { let button = TextButton::new("Preview") - .icon(Some("Rescale".to_string())) + .icon(Some("FrameAll".to_string())) .tooltip("Preview selected node/layer (Shortcut: Alt-click node/layer)") .on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into()) .widget_holder(); diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index c17f8bf931..8e137f8a74 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -140,8 +140,8 @@ pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, numbe Some(x) if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX))), true).into(), Some(x) if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)), true).into(), Some(x) if x == TypeId::of::() => text_widget(document_node, node_id, index, name, true).into(), - Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), - Some(x) if x == TypeId::of::>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(true), true), + Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(false), true), + Some(x) if x == TypeId::of::>() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(true), true), Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", Some(0.), add_blank_assist), Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), @@ -152,7 +152,7 @@ pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, numbe font_widgets.into_iter().chain(style_widgets.unwrap_or_default()).collect::>().into() } Some(x) if x == TypeId::of::() => curves_widget(document_node, node_id, index, name, true), - Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), + Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(false), true), Some(x) if x == TypeId::of::() => vector_widget(document_node, node_id, index, name, true).into(), Some(x) if x == TypeId::of::() || x == TypeId::of::>() || x == TypeId::of::() => { raster_widget(document_node, node_id, index, name, true).into() @@ -1067,7 +1067,7 @@ pub fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: us LayoutGroup::Row { widgets } } -pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorButton, blank_assist: bool) -> LayoutGroup { +pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorInput, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); // Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel @@ -1080,7 +1080,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, TaggedValue::Color(color) => widgets.push( color_button .value(FillChoice::Solid(*color)) - .on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index)) + .on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index)) .on_commit(commit_value) .widget_holder(), ), @@ -1090,7 +1090,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, Some(color) => FillChoice::Solid(*color), None => FillChoice::None, }) - .on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index)) + .on_update(update_value(|x: &ColorInput| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index)) .on_commit(commit_value) .widget_holder(), ), @@ -1098,7 +1098,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, color_button .value(FillChoice::Gradient(x.clone())) .on_update(update_value( - |x: &ColorButton| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()), + |x: &ColorInput| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()), node_id, index, )) @@ -1776,7 +1776,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties // if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() { // widgets.extend_from_slice(&[ // Separator::new(SeparatorType::Unrelated).widget_holder(), -// IconButton::new("Regenerate", 24) +// IconButton::new("Resync", 24) // .tooltip("Set a new random seed") // .on_update({ // let imaginate_node = imaginate_node.clone(); @@ -1856,7 +1856,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties // widgets.extend_from_slice(&[ // Separator::new(SeparatorType::Unrelated).widget_holder(), -// IconButton::new("Rescale", 24) +// IconButton::new("FrameAll", 24) // .tooltip("Set the layer dimensions to this resolution") // .on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into()) // .widget_holder(), @@ -2211,9 +2211,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets_first_row.push( - ColorButton::default() + ColorInput::default() .value(fill.clone().into()) - .on_update(move |x: &ColorButton| { + .on_update(move |x: &ColorInput| { Message::Batched(Box::new([ match &fill2 { Fill::None => NodeGraphMessage::SetInputValue { @@ -2380,7 +2380,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - let line_join_index = 6; let miter_limit_index = 7; - let color = color_widget(document_node, node_id, color_index, "Color", ColorButton::default(), true); + let color = color_widget(document_node, node_id, color_index, "Color", ColorInput::default(), true); let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit(" px").min(0.), true); let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() { diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index ad6eb18b57..2bf84526c1 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -229,7 +229,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }) }; let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| { - update_val::(grid, move |grid, color| { + update_val::(grid, move |grid, color| { if let FillChoice::Solid(color) = color.value { if let Some(update_color) = update(grid) { *update_color = color; @@ -280,7 +280,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { Separator::new(SeparatorType::Related).widget_holder(), ]); color_widgets.push( - ColorButton::new(FillChoice::Solid(grid.grid_color)) + ColorInput::new(FillChoice::Solid(grid.grid_color)) .tooltip("Grid display color") .allow_none(false) .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index ce42df044b..2b73b4f1f8 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -79,7 +79,7 @@ impl ToolColorOptions { color_allow_none: bool, reset_callback: impl Fn(&IconButton) -> Message + 'static + Send + Sync, radio_callback: fn(ToolColorType) -> WidgetCallback<()>, - color_callback: impl Fn(&ColorButton) -> Message + 'static + Send + Sync, + color_callback: impl Fn(&ColorInput) -> Message + 'static + Send + Sync, ) -> Vec { let mut widgets = vec![TextLabel::new(label_text).widget_holder()]; @@ -112,7 +112,7 @@ impl ToolColorOptions { widgets.push(radio); widgets.push(Separator::new(SeparatorType::Related).widget_holder()); - let color_button = ColorButton::new(FillChoice::from_optional_color(self.active_color())) + let color_button = ColorInput::new(FillChoice::from_optional_color(self.active_color())) .allow_none(color_allow_none) .on_update(color_callback); widgets.push(color_button.widget_holder()); diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 4cacebe355..383e54d59d 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -156,7 +156,7 @@ impl LayoutHolder for BrushTool { false, |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), - |color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(), + |color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Related).widget_holder()); diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index ecbd2cbb5d..858c177eb5 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -91,7 +91,7 @@ impl LayoutHolder for EllipseTool { true, |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -101,7 +101,7 @@ impl LayoutHolder for EllipseTool { true, |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index c75092a87c..7d48148acd 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -99,7 +99,7 @@ impl LayoutHolder for FreehandTool { true, |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -109,7 +109,7 @@ impl LayoutHolder for FreehandTool { true, |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index b866129944..65bfa2a45d 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -85,7 +85,7 @@ impl LayoutHolder for LineTool { true, |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 278771c310..9ec62caebc 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -114,7 +114,7 @@ impl LayoutHolder for PenTool { true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -124,7 +124,7 @@ impl LayoutHolder for PenTool { true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs index 1d27f38317..0097c22a18 100644 --- a/editor/src/messages/tool/tool_messages/polygon_tool.rs +++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs @@ -135,7 +135,7 @@ impl LayoutHolder for PolygonTool { true, |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -145,7 +145,7 @@ impl LayoutHolder for PolygonTool { true, |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index 09b589024f..fb34ed88eb 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -77,7 +77,7 @@ impl LayoutHolder for RectangleTool { true, |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -87,7 +87,7 @@ impl LayoutHolder for RectangleTool { true, |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 7f1ab31fb4..f615c69cc7 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -102,7 +102,7 @@ impl LayoutHolder for SplineTool { true, |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -112,7 +112,7 @@ impl LayoutHolder for SplineTool { true, |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 14da9427fc..69b1e2e8d8 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -160,7 +160,7 @@ impl LayoutHolder for TextTool { true, |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(), + |color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(), )); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 36c0c6a04a..634ddda395 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -138,6 +138,7 @@ linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff); --color-transparent-checkered-background-size: 16px 16px, 16px 16px, 16px 16px; --color-transparent-checkered-background-position: 0 0, 8px 8px, 8px 8px; + --color-transparent-checkered-background-position-plus-one: 1px 1px, 9px 9px, 9px 9px; --color-transparent-checkered-background-size-mini: 8px 8px, 8px 8px, 8px 8px; --color-transparent-checkered-background-position-mini: 0 0, 4px 4px, 4px 4px; --color-transparent-checkered-background-repeat: repeat, repeat, repeat; diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index bf70447f71..6167897b16 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -3,10 +3,11 @@ import type { Editor } from "@graphite/editor"; import type { HSV, RGB, FillChoice } from "@graphite/messages"; - import { Color, Gradient } from "@graphite/messages"; + import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages"; import { clamp } from "@graphite/utility-functions/math"; import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte"; + import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; @@ -65,7 +66,19 @@ // Transient state let draggingPickerTrack: HTMLDivElement | undefined = undefined; let strayCloses = true; + let gradientSpectrumDragging = false; + let shiftPressed = false; + let alignedAxis: "saturation" | "value" | undefined = undefined; + let hueBeforeDrag = 0; + let saturationBeforeDrag = 0; + let valueBeforeDrag = 0; + let alphaBeforeDrag = 0; + let saturationStartOfAxisAlign: number | undefined = undefined; + let valueStartOfAxisAlign: number | undefined = undefined; + let saturationRestoreWhenShiftReleased: number | undefined = undefined; + let valueRestoreWhenShiftReleased: number | undefined = undefined; + let self: FloatingMenu | undefined; let hexCodeInputWidget: TextInput | undefined; let gradientSpectrumInputWidget: SpectrumInput | undefined; @@ -77,6 +90,9 @@ $: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; $: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][]; $: opaqueHueColor = new Color({ h: hue, s: 1, v: 1, a: 1 }); + $: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01)); + $: outlined = outlineFactor > 0.0001; + $: transparency = newColor.alpha < 1 || oldColor.alpha < 1; function generateColor(h: number, s: number, v: number, a: number, none: boolean) { if (none) return new Color("none"); @@ -119,6 +135,14 @@ const target = (e.target || undefined) as HTMLElement | undefined; draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined; + hueBeforeDrag = hue; + saturationBeforeDrag = saturation; + valueBeforeDrag = value; + alphaBeforeDrag = alpha; + + saturationStartOfAxisAlign = undefined; + valueStartOfAxisAlign = undefined; + addEvents(); onPointerMove(e); @@ -134,6 +158,8 @@ saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1); value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1); strayCloses = false; + + if (shiftPressed) updateAxisLock(); } else if (draggingPickerTrack?.hasAttribute("data-hue-picker")) { const rectangle = draggingPickerTrack.getBoundingClientRect(); @@ -148,25 +174,102 @@ const color = new Color({ h: hue, s: saturation, v: value, a: alpha }); setColor(color); + + if (!e.shiftKey) { + shiftPressed = false; + alignedAxis = undefined; + } else if (!shiftPressed && draggingPickerTrack) { + shiftPressed = true; + saturationStartOfAxisAlign = saturation; + valueStartOfAxisAlign = value; + } } function onPointerUp() { removeEvents(); } + function onMouseDown(e: MouseEvent) { + const BUTTONS_RIGHT = 0b0000_0010; + if (e.buttons & BUTTONS_RIGHT) abortDrag(); + } + + function onKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + const element = self?.div(); + if (element) preventEscapeClosingParentFloatingMenu(element); + + abortDrag(); + } + } + + function onKeyUp(e: KeyboardEvent) { + if (e.key === "Shift") { + shiftPressed = false; + alignedAxis = undefined; + + if (saturationRestoreWhenShiftReleased !== undefined && valueRestoreWhenShiftReleased !== undefined) { + saturation = saturationRestoreWhenShiftReleased; + value = valueRestoreWhenShiftReleased; + + const color = new Color({ h: hue, s: saturation, v: value, a: alpha }); + setColor(color); + } + } + } + function addEvents() { document.addEventListener("pointermove", onPointerMove); document.addEventListener("pointerup", onPointerUp); + document.addEventListener("mousedown", onMouseDown); + document.addEventListener("keydown", onKeyDown); + document.addEventListener("keyup", onKeyUp); dispatch("startHistoryTransaction"); } function removeEvents() { draggingPickerTrack = undefined; - strayCloses = true; + // The setTimeout is necessary to prevent the FloatingMenu's `escapeCloses` from becoming true immediately upon pressing the Escape key, and thus closing + setTimeout(() => (strayCloses = true), 0); + shiftPressed = false; + alignedAxis = undefined; document.removeEventListener("pointermove", onPointerMove); document.removeEventListener("pointerup", onPointerUp); + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("keyup", onKeyUp); + } + + function updateAxisLock() { + if (!saturationStartOfAxisAlign || !valueStartOfAxisAlign) return; + + const deltaSaturation = saturation - saturationStartOfAxisAlign; + const deltaValue = value - valueStartOfAxisAlign; + + saturationRestoreWhenShiftReleased = saturation; + valueRestoreWhenShiftReleased = value; + + if (Math.abs(deltaSaturation) < Math.abs(deltaValue)) { + alignedAxis = "saturation"; + saturation = saturationStartOfAxisAlign; + } else { + alignedAxis = "value"; + value = valueStartOfAxisAlign; + } + } + + function abortDrag() { + removeEvents(); + + hue = hueBeforeDrag; + saturation = saturationBeforeDrag; + value = valueBeforeDrag; + alpha = alphaBeforeDrag; + + const color = new Color({ h: hue, s: saturation, v: value, a: alpha }); + setColor(color); } function setColor(color?: Color) { @@ -299,7 +402,7 @@ }); - + {/if} + {#if alignedAxis} +
+ {/if} {#if !isNone} @@ -340,6 +452,7 @@ }} on:activeMarkerIndexChange={gradientActiveMarkerIndexChange} activeMarkerIndex={activeIndex} + on:dragging={({ detail }) => (gradientSpectrumDragging = detail)} bind:this={gradientSpectrumInputWidget} /> {#if gradientSpectrumInputWidget && activeIndex !== undefined} @@ -360,6 +473,8 @@ {#if !newColor.equals(oldColor)} @@ -453,7 +568,7 @@ - Alpha + Alpha {#if allowNone && !gradient} - {/if} - - + (open = detail)} + colorOrGradient={value} + on:colorOrGradient={({ detail }) => { + value = detail; + dispatch("value", detail); + }} + on:startHistoryTransaction={() => { + // This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation: + // + dispatch("startHistoryTransaction"); + }} + {allowNone} + /> + + + diff --git a/frontend/src/components/widgets/inputs/FieldInput.svelte b/frontend/src/components/widgets/inputs/FieldInput.svelte index 4a92fc71e3..2efc1353b9 100644 --- a/frontend/src/components/widgets/inputs/FieldInput.svelte +++ b/frontend/src/components/widgets/inputs/FieldInput.svelte @@ -3,6 +3,7 @@ import { platformIsMac } from "@graphite/utility-functions/platform"; + import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; const dispatch = createEventDispatcher<{ @@ -65,6 +66,12 @@ export function element(): HTMLInputElement | HTMLTextAreaElement | undefined { return inputOrTextarea; } + + function cancel() { + dispatch("textChangeCanceled"); + + if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea); + } @@ -83,7 +90,7 @@ on:blur={() => dispatch("textChanged")} on:change={() => dispatch("textChanged")} on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")} - on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")} + on:keydown={(e) => e.key === "Escape" && cancel()} on:pointerdown on:contextmenu={(e) => hideContextMenu && e.preventDefault()} data-input-element @@ -102,7 +109,7 @@ on:blur={() => dispatch("textChanged")} on:change={() => dispatch("textChanged")} on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")} - on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")} + on:keydown={(e) => e.key === "Escape" && cancel()} on:pointerdown on:contextmenu={(e) => hideContextMenu && e.preventDefault()} /> diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index 08b75a5ef4..ae26c20ee8 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -1,9 +1,11 @@ -
dispatch("pointerup")}> - +
+
-
diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index 0fabdb3e54..cabc70b455 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -3,21 +3,27 @@ import { Color, type Gradient } from "@graphite/messages"; + import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; - const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient }>(); + const BUTTON_LEFT = 0; + const BUTTON_RIGHT = 2; + + const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>(); export let gradient: Gradient; export let activeMarkerIndex = 0 as number | undefined; // export let disabled = false; // export let tooltip: string | undefined = undefined; - let markerTrack: LayoutRow | undefined; + let markerTrack: LayoutRow | undefined = undefined; + let positionRestore: number | undefined = undefined; + let deletionRestore: boolean | undefined = undefined; function markerPointerDown(e: PointerEvent, index: number) { // Left-click to select and begin potentially dragging - if (e.button === 0) { + if (e.button === BUTTON_LEFT) { activeMarkerIndex = index; dispatch("activeMarkerIndexChange", index); addEvents(); @@ -25,7 +31,7 @@ } // Right-click to delete - if (e.button === 2) { + if (e.button === BUTTON_RIGHT && deletionRestore === undefined) { deleteStopByIndex(index); return; } @@ -41,6 +47,8 @@ } function insertStop(e: MouseEvent) { + if (e.button !== BUTTON_LEFT) return; + let position = markerPosition(e); if (position === undefined) return; @@ -62,14 +70,20 @@ gradient.stops.splice(index, 0, { position, color }); activeMarkerIndex = index; + deletionRestore = true; dispatch("activeMarkerIndexChange", index); dispatch("gradient", gradient); + + addEvents(); } function deleteStop(e: KeyboardEvent) { - if (e.key.toLowerCase() !== "delete" && e.key.toLowerCase() !== "backspace") return; + if (e.key !== "Delete" && e.key !== "Backspace") return; if (activeMarkerIndex === undefined) return; + + if (positionRestore !== undefined) stopDrag(); + deleteStopByIndex(activeMarkerIndex); } @@ -82,6 +96,7 @@ } else { activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index)); } + deletionRestore = undefined; dispatch("activeMarkerIndexChange", activeMarkerIndex); dispatch("gradient", gradient); @@ -89,10 +104,18 @@ function moveMarker(e: PointerEvent, index: number) { // Just in case the mouseup event is lost - if (e.buttons === 0) removeEvents(); + if (e.buttons === 0) stopDrag(); let position = markerPosition(e); if (position === undefined) return; + + if (positionRestore === undefined) positionRestore = position; + if (deletionRestore === undefined) { + deletionRestore = false; + + dispatch("dragging", true); + } + setPosition(index, position); } @@ -107,24 +130,61 @@ dispatch("gradient", gradient); } - function onPointerMove(e: PointerEvent) { - if (activeMarkerIndex !== undefined) { - moveMarker(e, activeMarkerIndex); + function abortDrag() { + if (activeMarkerIndex === undefined) return; + + if (deletionRestore) { + deleteStopByIndex(activeMarkerIndex); + } else if (positionRestore !== undefined) { + setPosition(activeMarkerIndex, positionRestore); } + + stopDrag(); + } + + function stopDrag() { + removeEvents(); + + positionRestore = undefined; + deletionRestore = undefined; + + dispatch("dragging", false); + } + + function onPointerMove(e: PointerEvent) { + if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex); } function onPointerUp() { - removeEvents(); + stopDrag(); + } + + function onMouseDown(e: MouseEvent) { + const BUTTONS_RIGHT = 0b0000_0010; + if (e.buttons & BUTTONS_RIGHT) abortDrag(); + } + + function onKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + const element = markerTrack?.div(); + if (element) preventEscapeClosingParentFloatingMenu(element); + + abortDrag(); + } } function addEvents() { document.addEventListener("pointermove", onPointerMove); document.addEventListener("pointerup", onPointerUp); + document.addEventListener("mousedown", onMouseDown); + document.addEventListener("keydown", onKeyDown); } function removeEvents() { document.removeEventListener("pointermove", onPointerMove); document.removeEventListener("pointerup", onPointerUp); + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("keydown", onKeyDown); } document.addEventListener("keydown", deleteStop); @@ -158,24 +218,22 @@ "--gradient-stops": gradient.toLinearGradientCSS(), }} > - + {#each gradient.stops as marker, index} markerPointerDown(e, index)} data-gradient-marker xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" > + markerPointerDown(e, index)} - d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" - /> - markerPointerDown(e, index)} + class="outer-border" d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z" /> @@ -216,6 +274,7 @@ margin-left: var(--marker-half-width); width: calc(100% - 2 * var(--marker-half-width)); position: relative; + pointer-events: none; .marker { position: absolute; @@ -223,30 +282,40 @@ left: calc(var(--marker-position) * 100%); width: 12px; height: 12px; + pointer-events: auto; + overflow: visible; + padding-top: 12px; + margin-top: -12px; - // Inner fill - path:first-child { + .inner-fill { fill: var(--marker-color); } - // Outer border - path:last-child { + .outer-border { fill: var(--color-5-dullgray); } - &:not(.active) path:first-child:hover + path:last-child, - &:not(.active) path:last-child:hover { - fill: var(--color-6-lowergray); + &:not(.active) { + .inner-fill:hover + .outer-border, + .outer-border:hover { + fill: var(--color-6-lowergray); + } } - // Outer border when active - &.active path:last-child { - fill: var(--color-e-nearwhite); - } + &.active { + .inner-fill { + filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack)); + } - &.active path:first-child:hover + path:last-child, - &.active path:last-child:hover { - fill: var(--color-f-white); + // Outer border when active + .outer-border { + fill: var(--color-e-nearwhite); + } + + .inner-fill:hover + .outer-border, + .outer-border:hover { + fill: var(--color-f-white); + } } } } diff --git a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte index c88c373b6b..cff3beacba 100644 --- a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte +++ b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte @@ -37,7 +37,7 @@ - (primaryOpen = detail)} @@ -47,7 +47,7 @@ /> - (secondaryOpen = detail)} @@ -70,27 +70,55 @@ > button { --swatch-color: #ffffff; + --ring-color: var(--color-5-dullgray); width: 100%; height: 100%; - border-radius: 50%; - border: 2px var(--color-5-dullgray) solid; - box-shadow: 0 0 0 2px var(--color-3-darkgray); margin: 0; padding: 0; + border: none; + outline: none; + border-radius: 50%; box-sizing: border-box; - background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background); - background-size: - 100% 100%, - var(--color-transparent-checkered-background-size); - background-position: - 0 0, - var(--color-transparent-checkered-background-position); - background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat); overflow: hidden; + position: relative; + // Color of the panel background, used to extend outside the ring and appear to cut out a crescent from the lower circle (by covering it up with the panel background color) + box-shadow: 0 0 0 2px var(--color-3-darkgray); + background: var(--color-3-darkgray); + + // Main color and checked transparency pattern (inset by 1px to begin inside/below the ring to avoid antialiasing artifacts) + &::before { + content: ""; + position: absolute; + top: 1px; + bottom: 1px; + left: 1px; + right: 1px; + border-radius: 50%; + background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background); + background-size: + 100% 100%, + var(--color-transparent-checkered-background-size); + background-position: + 0 0, + var(--color-transparent-checkered-background-position-plus-one); + background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat); + } + + // Gray ring outline + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-radius: 50%; + box-shadow: inset 0 0 0 2px var(--ring-color); + } &:hover, &.open { - border-color: var(--color-6-lowergray); + --ring-color: var(--color-6-lowergray); } } diff --git a/frontend/src/components/widgets/labels/IconLabel.svelte b/frontend/src/components/widgets/labels/IconLabel.svelte index f031763019..ab69928b60 100644 --- a/frontend/src/components/widgets/labels/IconLabel.svelte +++ b/frontend/src/components/widgets/labels/IconLabel.svelte @@ -12,7 +12,14 @@ export let tooltip: string | undefined = undefined; $: iconSizeClass = ((icon: IconName) => { - return `size-${iconSizeOverride || ICONS[icon].size}`; + const iconData = ICONS[icon]; + // eslint-disable-next-line no-console + if (!iconData) { + console.warn(`Icon "${icon}" does not exist.`); + return "size-24"; + } + if (iconData.size === undefined) return ""; + return `size-${iconSizeOverride || iconData.size}`; })(icon); $: extraClasses = Object.entries(classes) .flatMap(([className, stateName]) => (stateName ? [className] : [])) @@ -20,7 +27,7 @@ - {@html ICON_SVG_STRINGS[icon]} + {@html ICON_SVG_STRINGS[icon] || "�"}