From bfed2b4195bffd5d30e9a1c9c355701cb355688d Mon Sep 17 00:00:00 2001 From: Ivan <68190772+IVAN-MK7@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:12:59 +0100 Subject: [PATCH] Add `DragValue`s for RGB(A) in the color picker (#2734) Added some DragValue widgets in the color_picker widget as input fields for managing the RGBA values. In case the provided values result in a not valid premultiplied alpha RGBA color, a button will appear next to the input fields, to be used to multiply the values with the alpha channel. ![image](https://user-images.githubusercontent.com/68190772/218438019-8b46936f-d025-4287-ac27-2b937f8f3d7c.png) Closes . --------- Co-authored-by: IVANMK-7 <68190772+IVANMK-7@users.noreply.github.com> Co-authored-by: Emil Ernerfeldt Co-authored-by: Brian Janssen --- crates/ecolor/src/hsva.rs | 32 ++-- crates/egui/src/style.rs | 54 ++++++ crates/egui/src/widgets/color_picker.rs | 223 +++++++++++++++++++----- 3 files changed, 251 insertions(+), 58 deletions(-) diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 1fa54a24e10..69f8f0a59b0 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -28,23 +28,23 @@ impl Hsva { /// From `sRGBA` with premultiplied alpha #[inline] - pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { + pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self { Self::from_rgba_premultiplied( - linear_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + linear_f32_from_linear_u8(a), ) } /// From `sRGBA` without premultiplied alpha #[inline] - pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { + pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self { Self::from_rgba_unmultiplied( - linear_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + linear_f32_from_linear_u8(a), ) } @@ -83,6 +83,15 @@ impl Hsva { } } + #[inline] + pub fn from_additive_srgb([r, g, b]: [u8; 3]) -> Self { + Self::from_additive_rgb([ + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + ]) + } + #[inline] pub fn from_rgb(rgb: [f32; 3]) -> Self { let (h, s, v) = hsv_from_rgb(rgb); @@ -131,6 +140,8 @@ impl Hsva { } } + /// To linear space rgba in 0-1 range. + /// /// Represents additive colors using a negative alpha. #[inline] pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { @@ -150,6 +161,7 @@ impl Hsva { ] } + /// To gamma-space 0-255. #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index fb03bcfd805..7eb32a06ef2 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -829,6 +829,9 @@ pub struct Visuals { /// Show a spinner when loading an image. pub image_loading_spinners: bool, + + /// How to display numeric color values. + pub numeric_color_space: NumericColorSpace, } impl Visuals { @@ -1149,6 +1152,8 @@ impl Visuals { interact_cursor: None, image_loading_spinners: true, + + numeric_color_space: NumericColorSpace::GammaByte, } } @@ -1711,6 +1716,8 @@ impl Visuals { interact_cursor, image_loading_spinners, + + numeric_color_space, } = self; ui.collapsing("Background Colors", |ui| { @@ -1791,6 +1798,11 @@ impl Visuals { ui.checkbox(image_loading_spinners, "Image loading spinners") .on_hover_text("Show a spinner when an Image is loading"); + ui.horizontal(|ui| { + ui.label("Color picker type:"); + numeric_color_space.toggle_button_ui(ui); + }); + ui.vertical_centered(|ui| reset_button(ui, self)); } } @@ -1918,3 +1930,45 @@ impl HandleShape { }); } } + +/// How to display numeric color values. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum NumericColorSpace { + /// RGB is 0-255 in gamma space. + /// + /// Alpha is 0-255 in linear space . + GammaByte, + + /// 0-1 in linear space. + Linear, + // TODO(emilk): add Hex as an option +} + +impl NumericColorSpace { + pub fn toggle_button_ui(&mut self, ui: &mut Ui) -> crate::Response { + let tooltip = match self { + Self::GammaByte => "Showing color values in 0-255 gamma space", + Self::Linear => "Showing color values in 0-1 linear space", + }; + + let mut response = ui.button(self.to_string()).on_hover_text(tooltip); + if response.clicked() { + *self = match self { + Self::GammaByte => Self::Linear, + Self::Linear => Self::GammaByte, + }; + response.mark_changed(); + } + response + } +} + +impl std::fmt::Display for NumericColorSpace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NumericColorSpace::GammaByte => write!(f, "U8"), + NumericColorSpace::Linear => write!(f, "F"), + } + } +} diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a3752ef684c..8426adee968 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -216,50 +216,92 @@ fn color_slider_2d( response } +/// We use a negative alpha for additive colors within this file (a bit ironic). +/// +/// We use alpha=0 to mean "transparent". +fn is_additive_alpha(a: f32) -> bool { + a < 0.0 +} + /// What options to show for alpha #[derive(Clone, Copy, PartialEq, Eq)] pub enum Alpha { - // Set alpha to 1.0, and show no option for it. + /// Set alpha to 1.0, and show no option for it. Opaque, - // Only show normal blend options for it. + + /// Only show normal blend options for alpha. OnlyBlend, - // Show both blend and additive options. + + /// Show both blend and additive options. BlendOrAdditive, } -fn color_text_ui(ui: &mut Ui, color: impl Into, alpha: Alpha) { - let color = color.into(); - ui.horizontal(|ui| { - let [r, g, b, a] = color.to_array(); +fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) { + use crate::style::NumericColorSpace; - if ui.button("📋").on_hover_text("Click to copy").clicked() { - if alpha == Alpha::Opaque { - ui.ctx().copy_text(format!("{r}, {g}, {b}")); - } else { - ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}")); + let alpha_control = if is_additive_alpha(hsvag.a) { + Alpha::Opaque // no alpha control for additive colors + } else { + alpha + }; + + match ui.style().visuals.numeric_color_space { + NumericColorSpace::GammaByte => { + let mut srgba_unmultiplied = Hsva::from(*hsvag).to_srgba_unmultiplied(); + // Only update if changed to avoid rounding issues. + if srgba_edit_ui(ui, &mut srgba_unmultiplied, alpha_control) { + if is_additive_alpha(hsvag.a) { + let alpha = hsvag.a; + + *hsvag = HsvaGamma::from(Hsva::from_additive_srgb([ + srgba_unmultiplied[0], + srgba_unmultiplied[1], + srgba_unmultiplied[2], + ])); + + // Don't edit the alpha: + hsvag.a = alpha; + } else { + // Normal blending. + *hsvag = HsvaGamma::from(Hsva::from_srgba_unmultiplied(srgba_unmultiplied)); + } } } - if alpha == Alpha::Opaque { - ui.label(format!("rgb({r}, {g}, {b})")) - .on_hover_text("Red Green Blue"); - } else { - ui.label(format!("rgba({r}, {g}, {b}, {a})")) - .on_hover_text("Red Green Blue with premultiplied Alpha"); + NumericColorSpace::Linear => { + let mut rgba_unmultiplied = Hsva::from(*hsvag).to_rgba_unmultiplied(); + // Only update if changed to avoid rounding issues. + if rgba_edit_ui(ui, &mut rgba_unmultiplied, alpha_control) { + if is_additive_alpha(hsvag.a) { + let alpha = hsvag.a; + + *hsvag = HsvaGamma::from(Hsva::from_rgb([ + rgba_unmultiplied[0], + rgba_unmultiplied[1], + rgba_unmultiplied[2], + ])); + + // Don't edit the alpha: + hsvag.a = alpha; + } else { + // Normal blending. + *hsvag = HsvaGamma::from(Hsva::from_rgba_unmultiplied( + rgba_unmultiplied[0], + rgba_unmultiplied[1], + rgba_unmultiplied[2], + rgba_unmultiplied[3], + )); + } + } } - }); -} + } -fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) { let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y); - show_color(ui, *hsva, current_color_size).on_hover_text("Selected color"); - - color_text_ui(ui, *hsva, alpha); + show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color"); if alpha == Alpha::BlendOrAdditive { - // We signal additive blending by storing a negative alpha (a bit ironic). - let a = &mut hsva.a; - let mut additive = *a < 0.0; + let a = &mut hsvag.a; + let mut additive = is_additive_alpha(*a); ui.horizontal(|ui| { ui.label("Blending:"); ui.radio_value(&mut additive, false, "Normal"); @@ -274,26 +316,20 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) { } }); } - let additive = hsva.a < 0.0; - let opaque = HsvaGamma { a: 1.0, ..*hsva }; + let opaque = HsvaGamma { a: 1.0, ..*hsvag }; - if alpha == Alpha::Opaque { - hsva.a = 1.0; - } else { - let a = &mut hsva.a; + let HsvaGamma { h, s, v, a: _ } = hsvag; - if alpha == Alpha::OnlyBlend { - if *a < 0.0 { - *a = 0.5; // was additive, but isn't allowed to be - } - color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha"); - } else if !additive { - color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha"); - } + if false { + color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation"); + } + + if false { + color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value"); } - let HsvaGamma { h, s, v, a: _ } = hsva; + color_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..opaque }.into()); color_slider_1d(ui, h, |h| { HsvaGamma { @@ -306,15 +342,106 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) { }) .on_hover_text("Hue"); - if false { - color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation"); + let additive = is_additive_alpha(hsvag.a); + + if alpha == Alpha::Opaque { + hsvag.a = 1.0; + } else { + let a = &mut hsvag.a; + + if alpha == Alpha::OnlyBlend { + if is_additive_alpha(*a) { + *a = 0.5; // was additive, but isn't allowed to be + } + color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha"); + } else if !additive { + color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha"); + } } +} - if false { - color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value"); +fn input_type_button_ui(ui: &mut Ui) { + let mut input_type = ui.ctx().style().visuals.numeric_color_space; + if input_type.toggle_button_ui(ui).changed() { + ui.ctx().style_mut(|s| { + s.visuals.numeric_color_space = input_type; + }); } +} - color_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..opaque }.into()); +/// Shows 4 `DragValue` widgets to be used to edit the RGBA u8 values. +/// Alpha's `DragValue` is hidden when `Alpha::Opaque`. +/// +/// Returns `true` on change. +fn srgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [u8; 4], alpha: Alpha) -> bool { + let mut edited = false; + + ui.horizontal(|ui| { + input_type_button_ui(ui); + + if ui + .button("📋") + .on_hover_text("Click to copy color values") + .clicked() + { + if alpha == Alpha::Opaque { + ui.ctx().copy_text(format!("{r}, {g}, {b}")); + } else { + ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}")); + } + } + edited |= DragValue::new(r).speed(0.5).prefix("R ").ui(ui).changed(); + edited |= DragValue::new(g).speed(0.5).prefix("G ").ui(ui).changed(); + edited |= DragValue::new(b).speed(0.5).prefix("B ").ui(ui).changed(); + if alpha != Alpha::Opaque { + edited |= DragValue::new(a).speed(0.5).prefix("A ").ui(ui).changed(); + } + }); + + edited +} + +/// Shows 4 `DragValue` widgets to be used to edit the RGBA f32 values. +/// Alpha's `DragValue` is hidden when `Alpha::Opaque`. +/// +/// Returns `true` on change. +fn rgba_edit_ui(ui: &mut Ui, [r, g, b, a]: &mut [f32; 4], alpha: Alpha) -> bool { + fn drag_value(ui: &mut Ui, prefix: &str, value: &mut f32) -> Response { + DragValue::new(value) + .speed(0.003) + .prefix(prefix) + .clamp_range(0.0..=1.0) + .custom_formatter(|n, _| format!("{n:.03}")) + .ui(ui) + } + + let mut edited = false; + + ui.horizontal(|ui| { + input_type_button_ui(ui); + + if ui + .button("📋") + .on_hover_text("Click to copy color values") + .clicked() + { + if alpha == Alpha::Opaque { + ui.ctx().copy_text(format!("{r:.03}, {g:.03}, {b:.03}")); + } else { + ui.ctx() + .copy_text(format!("{r:.03}, {g:.03}, {b:.03}, {a:.03}")); + } + } + + edited |= drag_value(ui, "R ", r).changed(); + edited |= drag_value(ui, "G ", g).changed(); + edited |= drag_value(ui, "B ", b).changed(); + if alpha != Alpha::Opaque { + edited |= drag_value(ui, "A ", a).changed(); + } + }); + + edited } /// Shows a color picker where the user can change the given [`Hsva`] color. @@ -357,7 +484,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res ui.memory_mut(|mem| mem.toggle_popup(popup_id)); } - const COLOR_SLIDER_WIDTH: f32 = 210.0; + const COLOR_SLIDER_WIDTH: f32 = 275.0; // TODO(emilk): make it easier to show a temporary popup that closes when you click outside it if ui.memory(|mem| mem.is_popup_open(popup_id)) {