From 782f528279f6d111e15a71fc92ef86ff9cd62f11 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 5 Nov 2022 22:57:19 -0700 Subject: [PATCH] Add the range slider design to the NumberInput widget (#839) * Add range slider to NumberInput * Cleanup * Fix event ordering causing bug in Firefox * Polish the code * Switch number input modes to range in relevant places --- .../new_document_dialog_message_handler.rs | 14 +- .../utility_types/widgets/input_widgets.rs | 36 +- .../document/document_message_handler.rs | 7 +- .../properties_panel/utility_functions.rs | 17 +- .../components/floating-menus/ColorPicker.vue | 67 ++-- .../components/widgets/inputs/FieldInput.vue | 8 +- .../components/widgets/inputs/NumberInput.vue | 360 +++++++++++++++--- .../components/widgets/inputs/TextInput.vue | 13 +- frontend/src/wasm-communication/messages.ts | 33 +- 9 files changed, 435 insertions(+), 120 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 11058091..f7f5f39b 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -99,12 +99,13 @@ impl PropertyHolder for NewDocumentDialogMessageHandler { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(self.dimensions.x as f64), label: "W".into(), unit: " px".into(), - disabled: self.infinite, - is_integer: true, + value: Some(self.dimensions.x as f64), min: Some(0.), + is_integer: true, + disabled: self.infinite, + min_width: 100, on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into()), ..NumberInput::default() })), @@ -113,12 +114,13 @@ impl PropertyHolder for NewDocumentDialogMessageHandler { direction: SeparatorDirection::Horizontal, })), WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(self.dimensions.y as f64), label: "H".into(), unit: " px".into(), - disabled: self.infinite, - is_integer: true, + value: Some(self.dimensions.y as f64), min: Some(0.), + is_integer: true, + disabled: self.infinite, + min_width: 100, on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into()), ..NumberInput::default() })), 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 b348ea8b..6f87a7c4 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -153,8 +153,18 @@ pub struct InvisibleStandinInput { #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { + // Label pub label: String, + pub tooltip: String, + + #[serde(skip)] + pub tooltip_shortcut: Option, + + // Disabled + pub disabled: bool, + + // Value pub value: Option, pub min: Option, @@ -164,6 +174,7 @@ pub struct NumberInput { #[serde(rename = "isInteger")] pub is_integer: bool, + // Number presentation #[serde(rename = "displayDecimalPlaces")] #[derivative(Default(value = "3"))] pub display_decimal_places: u32, @@ -174,23 +185,25 @@ pub struct NumberInput { #[derivative(Default(value = "true"))] pub unit_is_hidden_when_editing: bool, + // Mode behavior + pub mode: NumberInputMode, + #[serde(rename = "incrementBehavior")] pub increment_behavior: NumberInputIncrementBehavior, - #[serde(rename = "incrementFactor")] #[derivative(Default(value = "1."))] - pub increment_factor: f64, + pub step: f64, - pub disabled: bool, + #[serde(rename = "rangeMin")] + pub range_min: Option, + #[serde(rename = "rangeMax")] + pub range_max: Option, + + // Styling #[serde(rename = "minWidth")] pub min_width: u32, - pub tooltip: String, - - #[serde(skip)] - pub tooltip_shortcut: Option, - // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] @@ -213,6 +226,13 @@ pub enum NumberInputIncrementBehavior { Callback, } +#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] +pub enum NumberInputMode { + #[default] + Increment, + Range, +} + #[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct OptionalInput { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index dcd906dc..43b3690f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -8,7 +8,9 @@ use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{DropdownEntryData, DropdownInput, NumberInput, NumberInputIncrementBehavior, OptionalInput, RadioEntryData, RadioInput}; +use crate::messages::layout::utility_types::widgets::input_widgets::{ + DropdownEntryData, DropdownInput, NumberInput, NumberInputIncrementBehavior, NumberInputMode, OptionalInput, RadioEntryData, RadioInput, +}; use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType}; use crate::messages::portfolio::document::properties_panel::utility_types::PropertiesPanelMessageHandlerData; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; @@ -1684,7 +1686,7 @@ impl DocumentMessageHandler { WidgetHolder::new(Widget::NumberInput(NumberInput { unit: "°".into(), value: Some(rotation_value), - increment_factor: 15., + step: 15., on_update: WidgetCallback::new(|number_input: &NumberInput| { NavigationMessage::SetCanvasRotation { angle_radians: number_input.value.unwrap() * (std::f64::consts::PI / 180.), @@ -1834,6 +1836,7 @@ impl DocumentMessageHandler { value: opacity.map(|opacity| opacity * 100.), min: Some(0.), max: Some(100.), + mode: NumberInputMode::Range, on_update: WidgetCallback::new(|number_input: &NumberInput| { if let Some(value) = number_input.value { DocumentMessage::SetOpacityForSelectedLayers { opacity: value / 100. }.into() 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 02622c22..7850810e 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -5,7 +5,7 @@ use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton}; use crate::messages::layout::utility_types::widgets::input_widgets::{ - CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput, + CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput, }; use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, Separator, SeparatorDirection, SeparatorType, TextLabel}; use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData}; @@ -405,8 +405,10 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La })), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.rotation() * 180. / PI), - label: "".into(), unit: "°".into(), + mode: NumberInputMode::Range, + range_min: Some(-180.), + range_max: Some(180.), on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() / 180. * PI, @@ -776,6 +778,10 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi })), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(imaginate_layer.samples.into()), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(150.), + is_integer: true, min: Some(0.), max: Some(150.), tooltip, @@ -862,8 +868,12 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi })), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(imaginate_layer.denoising_strength), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(1.), min: Some(0.), max: Some(1.), + display_decimal_places: 2, disabled: !imaginate_layer.use_img2img, tooltip, on_update: WidgetCallback::new(move |number_input: &NumberInput| { @@ -894,6 +904,9 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi })), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(imaginate_layer.cfg_scale), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(30.), min: Some(0.), max: Some(30.), tooltip, diff --git a/frontend/src/components/floating-menus/ColorPicker.vue b/frontend/src/components/floating-menus/ColorPicker.vue index 7f306531..856d78c4 100644 --- a/frontend/src/components/floating-menus/ColorPicker.vue +++ b/frontend/src/components/floating-menus/ColorPicker.vue @@ -18,8 +18,8 @@
- -
+ +
@@ -98,12 +98,13 @@ @@ -141,14 +142,14 @@ .saturation-value-picker, .hue-picker, - .opacity-picker { + .alpha-picker { height: 256px; position: relative; overflow: hidden; } .hue-picker, - .opacity-picker { + .alpha-picker { width: 24px; margin-left: 8px; position: relative; @@ -161,7 +162,7 @@ --selection-pincers-color: var(--hue-color-contrasting); } - .opacity-picker { + .alpha-picker { background: linear-gradient(to bottom, var(--opaque-color), transparent); &::before { @@ -403,12 +404,12 @@ export default defineComponent({ hue: hsva.h, saturation: hsva.s, value: hsva.v, - opacity: hsva.a, + alpha: hsva.a, isNone: hsvaOrNone === undefined, initialHue: hsva.h, initialSaturation: hsva.s, initialValue: hsva.v, - initialOpacity: hsva.a, + initialAlpha: hsva.a, initialIsNone: hsvaOrNone === undefined, draggingPickerTrack: undefined as HTMLDivElement | undefined, colorSpaceChoices: COLOR_SPACE_CHOICES, @@ -421,11 +422,11 @@ export default defineComponent({ }, newColor(): Color { if (this.isNone) return new Color("none"); - return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.opacity }); + return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha }); }, initialColor(): Color { if (this.initialIsNone) return new Color("none"); - return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialOpacity }); + return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialAlpha }); }, black(): Color { return new Color(0, 0, 0, 1); @@ -434,7 +435,7 @@ export default defineComponent({ watch: { // Called only when `open` is changed from outside this component (with v-model) open(isOpen: boolean) { - if (isOpen) this.setInitialHsvAndOpacity(this.hue, this.saturation, this.value, this.opacity, this.isNone); + if (isOpen) this.setInitialHSVA(this.hue, this.saturation, this.value, this.alpha, this.isNone); }, // Called only when `color` is changed from outside this component (with v-model) color(color: Color) { @@ -451,19 +452,19 @@ export default defineComponent({ if (hsva.v !== 0) this.saturation = hsva.s; // Update the value this.value = hsva.v; - // Update the opacity - this.opacity = hsva.a; + // Update the alpha + this.alpha = hsva.a; // Update the status of this not being a color this.isNone = false; } else { - this.setNewHsvAndOpacity(0, 0, 0, 1, true); + this.setNewHSVA(0, 0, 0, 1, true); } }, }, methods: { onPointerDown(e: PointerEvent) { const target = (e.target || undefined) as HTMLElement | undefined; - this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-opacity-picker]") || undefined; + this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined; this.addEvents(); @@ -484,14 +485,14 @@ export default defineComponent({ this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1); this.strayCloses = false; - } else if (this.draggingPickerTrack?.hasAttribute("data-opacity-picker")) { + } else if (this.draggingPickerTrack?.hasAttribute("data-alpha-picker")) { const rectangle = this.draggingPickerTrack.getBoundingClientRect(); - this.opacity = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1); + this.alpha = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1); this.strayCloses = false; } - const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.opacity }); + const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha }); this.setColor(color); }, onPointerUp() { @@ -512,7 +513,7 @@ export default defineComponent({ this.$emit("update:open", isOpen); }, setColor(color?: Color) { - const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.opacity }); + const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha }); this.$emit("update:color", colorToEmit); }, swapNewWithInitial() { @@ -521,11 +522,11 @@ export default defineComponent({ const tempHue = this.hue; const tempSaturation = this.saturation; const tempValue = this.value; - const tempOpacity = this.opacity; + const tempAlpha = this.alpha; const tempIsNone = this.isNone; - this.setNewHsvAndOpacity(this.initialHue, this.initialSaturation, this.initialValue, this.initialOpacity, this.initialIsNone); - this.setInitialHsvAndOpacity(tempHue, tempSaturation, tempValue, tempOpacity, tempIsNone); + this.setNewHSVA(this.initialHue, this.initialSaturation, this.initialValue, this.initialAlpha, this.initialIsNone); + this.setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone); this.setColor(initial); }, @@ -545,8 +546,8 @@ export default defineComponent({ this.setColor(); }, - setColorOpacityPercent(opacity: number) { - this.opacity = opacity / 100; + setColorAlphaPercent(alpha: number) { + this.alpha = alpha / 100; this.setColor(); }, setColorPresetSubtile(e: MouseEvent) { @@ -557,7 +558,7 @@ export default defineComponent({ }, setColorPreset(preset: PresetColors) { if (preset === "none") { - this.setNewHsvAndOpacity(0, 0, 0, 1, true); + this.setNewHSVA(0, 0, 0, 1, true); this.setColor(new Color("none")); return; } @@ -565,21 +566,21 @@ export default defineComponent({ const presetColor = new Color(...PURE_COLORS[preset], 1); const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 }; - this.setNewHsvAndOpacity(hsva.h, hsva.s, hsva.v, hsva.a, false); + this.setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false); this.setColor(presetColor); }, - setNewHsvAndOpacity(hue: number, saturation: number, value: number, opacity: number, isNone: boolean) { + setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) { this.hue = hue; this.saturation = saturation; this.value = value; - this.opacity = opacity; + this.alpha = alpha; this.isNone = isNone; }, - setInitialHsvAndOpacity(hue: number, saturation: number, value: number, opacity: number, isNone: boolean) { + setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) { this.initialHue = hue; this.initialSaturation = saturation; this.initialValue = value; - this.initialOpacity = opacity; + this.initialAlpha = alpha; this.initialIsNone = isNone; }, async activateEyedropperSample() { diff --git a/frontend/src/components/widgets/inputs/FieldInput.vue b/frontend/src/components/widgets/inputs/FieldInput.vue index d1d7305a..e8a17843 100644 --- a/frontend/src/components/widgets/inputs/FieldInput.vue +++ b/frontend/src/components/widgets/inputs/FieldInput.vue @@ -2,11 +2,11 @@