@@ -2,9 +2,10 @@ use crate::messages::color_picker::color_picker_message::{HsvChannel, RgbChannel
22use crate :: messages:: layout:: utility_types:: widget_prelude:: * ;
33use crate :: messages:: layout:: utility_types:: widgets:: input_widgets:: { ColorPresetsInputUpdate , SpectrumInputUpdate , SpectrumMarker , VisualColorPickersInputUpdate } ;
44use crate :: messages:: prelude:: * ;
5- use color:: { AlphaColor , Srgb } ;
65use graphene_std:: Color ;
7- use graphene_std:: vector:: style:: { FillChoice , GradientStops } ;
6+ use graphene_std:: color:: SRGBA8 ;
7+ use graphene_std:: core_types:: misc:: parse_css_color;
8+ use graphene_std:: vector:: style:: { FillChoice , FillChoiceUI , GradientStops , GradientStopsUI } ;
89
910/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
1011const MIN_MIDPOINT : f64 = 0.01 ;
@@ -105,10 +106,13 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
105106 ColorPickerMessage :: SetChannelRgb { channel, value } => {
106107 let Some ( strength) = value else { return } ;
107108 let Some ( current) = self . current_color ( ) else { return } ;
109+ // The RGB inputs are 0..255 sRGB display values; substitute the new channel into the gamma triple and lift back to linear for storage.
110+ let new_gamma_channel = ( strength / 255. ) as f32 ;
111+ let [ cur_r, cur_g, cur_b, cur_a] = current. to_gamma_srgb_channels ( ) ;
108112 let updated = match channel {
109- RgbChannel :: Red => Color :: from_rgbaf32_unchecked ( ( strength / 255. ) as f32 , current . g ( ) , current . b ( ) , current . a ( ) ) ,
110- RgbChannel :: Green => Color :: from_rgbaf32_unchecked ( current . r ( ) , ( strength / 255. ) as f32 , current . b ( ) , current . a ( ) ) ,
111- RgbChannel :: Blue => Color :: from_rgbaf32_unchecked ( current . r ( ) , current . g ( ) , ( strength / 255. ) as f32 , current . a ( ) ) ,
113+ RgbChannel :: Red => Color :: from_gamma_srgb_channels ( new_gamma_channel , cur_g , cur_b , cur_a ) ,
114+ RgbChannel :: Green => Color :: from_gamma_srgb_channels ( cur_r , new_gamma_channel , cur_b , cur_a ) ,
115+ RgbChannel :: Blue => Color :: from_gamma_srgb_channels ( cur_r , cur_g , new_gamma_channel , cur_a ) ,
112116 } ;
113117 self . adopt_color ( updated) ;
114118 self . emit_color ( responses) ;
@@ -150,7 +154,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
150154 match preset {
151155 FillChoice :: None => {
152156 self . set_new_hsva ( 0. , 0. , 0. , 1. , true ) ;
153- responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoice :: None } ) ;
157+ responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoiceUI :: None } ) ;
154158 }
155159 FillChoice :: Solid ( color) => {
156160 self . adopt_color ( color) ;
@@ -175,7 +179,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
175179 self . set_old_hsva ( temp. 0 , temp. 1 , temp. 2 , temp. 3 , temp. 4 ) ;
176180
177181 if self . is_none {
178- responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoice :: None } ) ;
182+ responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoiceUI :: None } ) ;
179183 } else {
180184 self . emit_color ( responses) ;
181185 }
@@ -197,6 +201,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
197201}
198202
199203impl ColorPickerMessageHandler {
204+ // The picker's internal HSV state is HSV of sRGB display values
200205 fn current_color ( & self ) -> Option < Color > {
201206 if self . is_none {
202207 None
@@ -239,7 +244,8 @@ impl ColorPickerMessageHandler {
239244
240245 /// Set HSV state from a Color, preserving hue and saturation in degenerate cases.
241246 fn adopt_color ( & mut self , color : Color ) {
242- let [ target_h, target_s, target_v] = rgb_to_hsv ( color. r ( ) as f64 , color. g ( ) as f64 , color. b ( ) as f64 ) ;
247+ let [ target_h, target_s, target_v, target_a] = color. to_hsva ( ) ;
248+ let ( target_h, target_s, target_v, target_a) = ( target_h as f64 , target_s as f64 , target_v as f64 , target_a as f64 ) ;
243249
244250 // Preserve hue: avoid jumping from 360° (top) to 0° (bottom) and don't reset hue when the color is desaturated or fully dark.
245251 if !( target_h == 0. && self . hue == 1. ) && target_s > 0. && target_v > 0. {
@@ -250,7 +256,7 @@ impl ColorPickerMessageHandler {
250256 self . saturation = target_s;
251257 }
252258 self . value = target_v;
253- self . alpha = color . a ( ) as f64 ;
259+ self . alpha = target_a ;
254260 self . is_none = false ;
255261 }
256262
@@ -264,9 +270,15 @@ impl ColorPickerMessageHandler {
264270 {
265271 * stop_color = color;
266272 let stops = gradient. clone ( ) ;
267- responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoice :: Gradient ( stops) } ) ;
273+ let fill_choice = FillChoice :: Gradient ( stops) ;
274+ responses. add ( FrontendMessage :: ColorPickerColorChanged {
275+ value : FillChoiceUI :: from ( & fill_choice) ,
276+ } ) ;
268277 } else {
269- responses. add ( FrontendMessage :: ColorPickerColorChanged { value : FillChoice :: Solid ( color) } ) ;
278+ let fill_choice = FillChoice :: Solid ( color) ;
279+ responses. add ( FrontendMessage :: ColorPickerColorChanged {
280+ value : FillChoiceUI :: from ( & fill_choice) ,
281+ } ) ;
270282 }
271283 }
272284
@@ -364,8 +376,9 @@ impl ColorPickerMessageHandler {
364376 }
365377
366378 self . gradient = Some ( gradient. clone ( ) ) ;
379+ let fill_choice = FillChoice :: Gradient ( gradient) ;
367380 responses. add ( FrontendMessage :: ColorPickerColorChanged {
368- value : FillChoice :: Gradient ( gradient ) ,
381+ value : FillChoiceUI :: from ( & fill_choice ) ,
369382 } ) ;
370383 self . send_layouts ( responses) ;
371384 }
@@ -392,7 +405,7 @@ impl ColorPickerMessageHandler {
392405 // For gradient editing, the markers' handle colors mirror their gradient stop colors
393406 let markers = gradient. iter ( ) . map ( |stop| SpectrumMarker :: new ( stop. position , stop. midpoint , stop. color ) ) . collect ( ) ;
394407 let mut row_widgets = vec ! [
395- SpectrumInput :: new( gradient . clone ( ) )
408+ SpectrumInput :: new( GradientStopsUI :: from ( gradient ) )
396409 . markers( markers)
397410 . active_marker_index( self . active_marker_index)
398411 . active_marker_is_midpoint( self . active_marker_is_midpoint)
@@ -453,7 +466,11 @@ impl ColorPickerMessageHandler {
453466 let old_color = self . old_color ( ) ;
454467
455468 let hex_value = new_color. map ( |c| color_to_hex_optional_alpha ( & c) ) . unwrap_or_else ( || "-" . to_string ( ) ) ;
456- let rgb_255 = new_color. map ( |c| ( c. r ( ) as f64 * 255. , c. g ( ) as f64 * 255. , c. b ( ) as f64 * 255. ) ) ;
469+ // RGB readouts display sRGB byte values to the user, so we convert from linear-light to gamma here before quantizing.
470+ let rgb_255 = new_color. map ( |c| {
471+ let [ r, g, b, _] = c. to_gamma_srgb_channels ( ) ;
472+ ( r as f64 * 255. , g as f64 * 255. , b as f64 * 255. )
473+ } ) ;
457474
458475 // Epsilon comparison since the picker round-trips through HSV
459476 let differs = match ( new_color, old_color) {
@@ -470,7 +487,7 @@ impl ColorPickerMessageHandler {
470487
471488 // New/old comparison swatch with swap button
472489 groups. push ( LayoutGroup :: row ( vec ! [
473- ColorComparisonInput :: new( new_color, old_color)
490+ ColorComparisonInput :: new( new_color. map ( SRGBA8 :: from ) , old_color. map ( SRGBA8 :: from ) )
474491 . is_none( self . is_none)
475492 . old_is_none( self . old_is_none)
476493 . disabled( self . disabled)
@@ -566,7 +583,10 @@ impl ColorPickerMessageHandler {
566583 . disabled( self . disabled)
567584 . show_none_option( self . allow_none && self . gradient. is_none( ) )
568585 . on_update( |update: & ColorPresetsInputUpdate | match update {
569- ColorPresetsInputUpdate :: Preset ( fill_choice) => ColorPickerMessage :: PickPreset { preset: fill_choice. clone( ) } . into( ) ,
586+ ColorPresetsInputUpdate :: Preset ( fill_choice) => ColorPickerMessage :: PickPreset {
587+ preset: FillChoice :: from( fill_choice) ,
588+ }
589+ . into( ) ,
570590 ColorPresetsInputUpdate :: EyedropperColorCode ( code) => ColorPickerMessage :: EyedropperColorCode { code: code. clone( ) } . into( ) ,
571591 } )
572592 . widget_instance( ) ,
@@ -611,33 +631,10 @@ const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color
611631const VALUE_DESCRIPTION : & str = "The brightness from black to full color." ;
612632const ALPHA_DESCRIPTION : & str = "The level of translucency, from transparent (0%) to opaque (100%)." ;
613633
614- /// Convert an `rgb(0..1)` triple to `hsv(0..1)`. Mirrors the legacy frontend `colorToHSV`.
615- fn rgb_to_hsv ( red : f64 , green : f64 , blue : f64 ) -> [ f64 ; 3 ] {
616- let max = red. max ( green) . max ( blue) ;
617- let min = red. min ( green) . min ( blue) ;
618- let delta = max - min;
619-
620- let mut hue = if delta == 0. {
621- 0.
622- } else if max == red {
623- ( ( green - blue) / delta) . rem_euclid ( 6. )
624- } else if max == green {
625- ( blue - red) / delta + 2.
626- } else {
627- ( red - green) / delta + 4.
628- } ;
629- hue = ( hue * 60. + 360. ) . rem_euclid ( 360. ) / 360. ;
630-
631- let saturation = if max == 0. { 0. } else { delta / max } ;
632- let value = max;
633-
634- [ hue, saturation, value]
635- }
636-
637- /// The popover's background color (the `--color-2-mildblack` design token, `#222`). Used by the comparison swatch's
638- /// outline computation to brighten the inset border for colors close to this background.
639- const POPOVER_BACKGROUND : Color = Color :: from_rgbaf32_unchecked ( 0x22 as f32 / 255. , 0x22 as f32 / 255. , 0x22 as f32 / 255. , 1. ) ;
640- /// The luminance window (in linear-light) within which a color is considered close enough to the popover background
634+ /// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
635+ /// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
636+ const POPOVER_BACKGROUND_GAMMA_CHANNELS : [ f32 ; 4 ] = [ 0x22 as f32 / 255. , 0x22 as f32 / 255. , 0x22 as f32 / 255. , 1. ] ;
637+ /// The luminance window within which a color is considered close enough to the popover background
641638/// to warrant an outline. Mirrors the `proximityRange` argument the legacy frontend passed to `contrastingOutlineFactor`.
642639const OUTLINE_PROXIMITY_RANGE : f64 = 0.01 ;
643640
@@ -646,61 +643,22 @@ const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
646643fn contrasting_outline_factor ( color : Option < Color > ) -> f64 {
647644 let Some ( color) = color else { return 0. } ;
648645
649- // WCAG-style relative luminance, with alpha composited over white in gamma space
650- let luminance = |color : Color | {
651- // TODO: Remove the `.to_linear_srgb()` once we move to correctly treating `Color` as linear.
652- Color :: WHITE
653- . alpha_blend ( Color :: from_unassociated_alpha ( color. r ( ) , color. g ( ) , color. b ( ) , color. a ( ) ) )
654- . to_linear_srgb ( )
655- . luminance_srgb ( ) as f64
646+ // WCAG-style relative luminance, with alpha composited over white in sRGB gamma space (matching the perceptual intent of `SRGBA8::contrasting_text_color`).
647+ let luminance_from_gamma_channels = |[ r, g, b, a] : [ f32 ; 4 ] | -> f64 {
648+ let inv_a = 1. - a;
649+ Color :: from_gamma_srgb_channels ( inv_a + r * a, inv_a + g * a, inv_a + b * a, 1. ) . luminance_rec_709 ( ) as f64
656650 } ;
657651
658- let distance = ( luminance ( POPOVER_BACKGROUND ) - luminance ( color) ) . abs ( ) . max ( 0. ) ;
652+ let color_gamma_channels = color. to_gamma_srgb_channels ( ) ;
653+ let distance = ( luminance_from_gamma_channels ( POPOVER_BACKGROUND_GAMMA_CHANNELS ) - luminance_from_gamma_channels ( color_gamma_channels) )
654+ . abs ( )
655+ . max ( 0. ) ;
659656 let proximity = 1. - ( distance / OUTLINE_PROXIMITY_RANGE ) . min ( 1. ) ;
660- let [ _, saturation, _] = rgb_to_hsv ( color. r ( ) as f64 , color . g ( ) as f64 , color . b ( ) as f64 ) ;
661- proximity * ( 1. - saturation)
657+ let [ _, saturation, _, _ ] = color. to_hsva ( ) ;
658+ proximity * ( 1. - saturation as f64 )
662659}
663660
664- /// Format a Color as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
661+ /// Format a linear ` Color` as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
665662fn color_to_hex_optional_alpha ( color : & Color ) -> String {
666- format ! (
667- "#{}" ,
668- if color. a( ) >= 1. {
669- color. to_rgb_hex_srgb_from_gamma( )
670- } else {
671- color. to_rgba_hex_srgb_from_gamma( )
672- }
673- )
674- }
675-
676- /// Parse a CSS color string (named color, hex, `rgb(...)`, etc.) into a `Color` using the `color` crate's CSS Color 4 parser.
677- /// Tries the input as-is first (catches CSS named colors like `red`, `rgb(...)`, and well-formed hex like `#abcdef`), then falls back to treating the input as bare hex with length-based expansion to a CSS-parseable form:
678- /// - 1 char `f` → `#fff` (CSS 3-char shorthand)
679- /// - 2 char `ab` → `#ababab` (repeated to 6 chars)
680- /// - 4 char `abcd` → `#00abcd` (left-padded with `00`)
681- /// - 5 char `abcde` → `#0abcde` (left-padded with `0`)
682- /// - 3, 6, 8 char inputs are passed through with a `#` prefix.
683- fn parse_css_color ( input : & str ) -> Option < Color > {
684- let trimmed = input. trim ( ) ;
685-
686- let parsed = color:: parse_color ( trimmed) . ok ( ) . or_else ( || {
687- let bare = trimmed. strip_prefix ( '#' ) . unwrap_or ( trimmed) ;
688- if bare. is_empty ( ) || !bare. chars ( ) . all ( |c| c. is_ascii_hexdigit ( ) ) {
689- return None ;
690- }
691- let expanded = match bare. len ( ) {
692- 1 => bare. repeat ( 3 ) ,
693- 2 => bare. repeat ( 3 ) ,
694- 4 => format ! ( "00{bare}" ) ,
695- 5 => format ! ( "0{bare}" ) ,
696- _ => bare. to_string ( ) ,
697- } ;
698- let candidate = format ! ( "#{expanded}" ) ;
699- // Avoid retrying the exact same string we just failed to parse.
700- ( candidate != trimmed) . then ( || color:: parse_color ( & candidate) . ok ( ) ) . flatten ( )
701- } ) ?;
702-
703- let srgb: AlphaColor < Srgb > = parsed. to_alpha_color ( ) ;
704- let [ red, green, blue, alpha] = srgb. components ;
705- Color :: from_rgbaf32 ( red, green, blue, alpha)
663+ SRGBA8 :: from ( * color) . to_css_hex ( )
706664}
0 commit comments