Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs (#4149)

* Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs

* Code review fixes
This commit is contained in:
Keavon Chambers
2026-05-14 22:48:33 -07:00
committed by GitHub
parent 456a7c868d
commit a56746c6bf
67 changed files with 1210 additions and 941 deletions
@@ -21,7 +21,7 @@ pub enum HsvChannel {
#[impl_message(Message, ColorPicker)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ColorPickerMessage {
/// Initialize the picker state from an external color/gradient and announce its options. Called by the frontend when a `<ColorPicker>` opens.
/// Initialize the picker state from an external color/gradient and announce its options. Called by the frontend when a `<ColorPicker />` opens.
Open { initial_value: FillChoice, allow_none: bool, disabled: bool },
/// Clear the picker state. Called by the frontend when the popover closes.
Close,
@@ -2,9 +2,10 @@ use crate::messages::color_picker::color_picker_message::{HsvChannel, RgbChannel
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorPresetsInputUpdate, SpectrumInputUpdate, SpectrumMarker, VisualColorPickersInputUpdate};
use crate::messages::prelude::*;
use color::{AlphaColor, Srgb};
use graphene_std::Color;
use graphene_std::vector::style::{FillChoice, GradientStops};
use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientStops, GradientStopsUI};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01;
@@ -105,10 +106,13 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
ColorPickerMessage::SetChannelRgb { channel, value } => {
let Some(strength) = value else { return };
let Some(current) = self.current_color() else { return };
// The RGB inputs are 0..255 sRGB display values; substitute the new channel into the gamma triple and lift back to linear for storage.
let new_gamma_channel = (strength / 255.) as f32;
let [cur_r, cur_g, cur_b, cur_a] = current.to_gamma_srgb_channels();
let updated = match channel {
RgbChannel::Red => Color::from_rgbaf32_unchecked((strength / 255.) as f32, current.g(), current.b(), current.a()),
RgbChannel::Green => Color::from_rgbaf32_unchecked(current.r(), (strength / 255.) as f32, current.b(), current.a()),
RgbChannel::Blue => Color::from_rgbaf32_unchecked(current.r(), current.g(), (strength / 255.) as f32, current.a()),
RgbChannel::Red => Color::from_gamma_srgb_channels(new_gamma_channel, cur_g, cur_b, cur_a),
RgbChannel::Green => Color::from_gamma_srgb_channels(cur_r, new_gamma_channel, cur_b, cur_a),
RgbChannel::Blue => Color::from_gamma_srgb_channels(cur_r, cur_g, new_gamma_channel, cur_a),
};
self.adopt_color(updated);
self.emit_color(responses);
@@ -150,7 +154,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
match preset {
FillChoice::None => {
self.set_new_hsva(0., 0., 0., 1., true);
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::None });
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
}
FillChoice::Solid(color) => {
self.adopt_color(color);
@@ -175,7 +179,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
if self.is_none {
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::None });
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
} else {
self.emit_color(responses);
}
@@ -197,6 +201,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
}
impl ColorPickerMessageHandler {
// The picker's internal HSV state is HSV of sRGB display values
fn current_color(&self) -> Option<Color> {
if self.is_none {
None
@@ -239,7 +244,8 @@ impl ColorPickerMessageHandler {
/// Set HSV state from a Color, preserving hue and saturation in degenerate cases.
fn adopt_color(&mut self, color: Color) {
let [target_h, target_s, target_v] = rgb_to_hsv(color.r() as f64, color.g() as f64, color.b() as f64);
let [target_h, target_s, target_v, target_a] = color.to_hsva();
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);
// Preserve hue: avoid jumping from 360° (top) to 0° (bottom) and don't reset hue when the color is desaturated or fully dark.
if !(target_h == 0. && self.hue == 1.) && target_s > 0. && target_v > 0. {
@@ -250,7 +256,7 @@ impl ColorPickerMessageHandler {
self.saturation = target_s;
}
self.value = target_v;
self.alpha = color.a() as f64;
self.alpha = target_a;
self.is_none = false;
}
@@ -264,9 +270,15 @@ impl ColorPickerMessageHandler {
{
*stop_color = color;
let stops = gradient.clone();
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::Gradient(stops) });
let fill_choice = FillChoice::Gradient(stops);
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoiceUI::from(&fill_choice),
});
} else {
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::Solid(color) });
let fill_choice = FillChoice::Solid(color);
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoiceUI::from(&fill_choice),
});
}
}
@@ -364,8 +376,9 @@ impl ColorPickerMessageHandler {
}
self.gradient = Some(gradient.clone());
let fill_choice = FillChoice::Gradient(gradient);
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(gradient),
value: FillChoiceUI::from(&fill_choice),
});
self.send_layouts(responses);
}
@@ -392,7 +405,7 @@ impl ColorPickerMessageHandler {
// For gradient editing, the markers' handle colors mirror their gradient stop colors
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
let mut row_widgets = vec![
SpectrumInput::new(gradient.clone())
SpectrumInput::new(GradientStopsUI::from(gradient))
.markers(markers)
.active_marker_index(self.active_marker_index)
.active_marker_is_midpoint(self.active_marker_is_midpoint)
@@ -453,7 +466,11 @@ impl ColorPickerMessageHandler {
let old_color = self.old_color();
let hex_value = new_color.map(|c| color_to_hex_optional_alpha(&c)).unwrap_or_else(|| "-".to_string());
let rgb_255 = new_color.map(|c| (c.r() as f64 * 255., c.g() as f64 * 255., c.b() as f64 * 255.));
// RGB readouts display sRGB byte values to the user, so we convert from linear-light to gamma here before quantizing.
let rgb_255 = new_color.map(|c| {
let [r, g, b, _] = c.to_gamma_srgb_channels();
(r as f64 * 255., g as f64 * 255., b as f64 * 255.)
});
// Epsilon comparison since the picker round-trips through HSV
let differs = match (new_color, old_color) {
@@ -470,7 +487,7 @@ impl ColorPickerMessageHandler {
// New/old comparison swatch with swap button
groups.push(LayoutGroup::row(vec![
ColorComparisonInput::new(new_color, old_color)
ColorComparisonInput::new(new_color.map(SRGBA8::from), old_color.map(SRGBA8::from))
.is_none(self.is_none)
.old_is_none(self.old_is_none)
.disabled(self.disabled)
@@ -566,7 +583,10 @@ impl ColorPickerMessageHandler {
.disabled(self.disabled)
.show_none_option(self.allow_none && self.gradient.is_none())
.on_update(|update: &ColorPresetsInputUpdate| match update {
ColorPresetsInputUpdate::Preset(fill_choice) => ColorPickerMessage::PickPreset { preset: fill_choice.clone() }.into(),
ColorPresetsInputUpdate::Preset(fill_choice) => ColorPickerMessage::PickPreset {
preset: FillChoice::from(fill_choice),
}
.into(),
ColorPresetsInputUpdate::EyedropperColorCode(code) => ColorPickerMessage::EyedropperColorCode { code: code.clone() }.into(),
})
.widget_instance(),
@@ -611,33 +631,10 @@ const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
/// Convert an `rgb(0..1)` triple to `hsv(0..1)`. Mirrors the legacy frontend `colorToHSV`.
fn rgb_to_hsv(red: f64, green: f64, blue: f64) -> [f64; 3] {
let max = red.max(green).max(blue);
let min = red.min(green).min(blue);
let delta = max - min;
let mut hue = if delta == 0. {
0.
} else if max == red {
((green - blue) / delta).rem_euclid(6.)
} else if max == green {
(blue - red) / delta + 2.
} else {
(red - green) / delta + 4.
};
hue = (hue * 60. + 360.).rem_euclid(360.) / 360.;
let saturation = if max == 0. { 0. } else { delta / max };
let value = max;
[hue, saturation, value]
}
/// The popover's background color (the `--color-2-mildblack` design token, `#222`). Used by the comparison swatch's
/// outline computation to brighten the inset border for colors close to this background.
const POPOVER_BACKGROUND: Color = Color::from_rgbaf32_unchecked(0x22 as f32 / 255., 0x22 as f32 / 255., 0x22 as f32 / 255., 1.);
/// The luminance window (in linear-light) within which a color is considered close enough to the popover background
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
const POPOVER_BACKGROUND_GAMMA_CHANNELS: [f32; 4] = [0x22 as f32 / 255., 0x22 as f32 / 255., 0x22 as f32 / 255., 1.];
/// The luminance window within which a color is considered close enough to the popover background
/// to warrant an outline. Mirrors the `proximityRange` argument the legacy frontend passed to `contrastingOutlineFactor`.
const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
@@ -646,61 +643,22 @@ const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
fn contrasting_outline_factor(color: Option<Color>) -> f64 {
let Some(color) = color else { return 0. };
// WCAG-style relative luminance, with alpha composited over white in gamma space
let luminance = |color: Color| {
// TODO: Remove the `.to_linear_srgb()` once we move to correctly treating `Color` as linear.
Color::WHITE
.alpha_blend(Color::from_unassociated_alpha(color.r(), color.g(), color.b(), color.a()))
.to_linear_srgb()
.luminance_srgb() as f64
// WCAG-style relative luminance, with alpha composited over white in sRGB gamma space (matching the perceptual intent of `SRGBA8::contrasting_text_color`).
let luminance_from_gamma_channels = |[r, g, b, a]: [f32; 4]| -> f64 {
let inv_a = 1. - a;
Color::from_gamma_srgb_channels(inv_a + r * a, inv_a + g * a, inv_a + b * a, 1.).luminance_rec_709() as f64
};
let distance = (luminance(POPOVER_BACKGROUND) - luminance(color)).abs().max(0.);
let color_gamma_channels = color.to_gamma_srgb_channels();
let distance = (luminance_from_gamma_channels(POPOVER_BACKGROUND_GAMMA_CHANNELS) - luminance_from_gamma_channels(color_gamma_channels))
.abs()
.max(0.);
let proximity = 1. - (distance / OUTLINE_PROXIMITY_RANGE).min(1.);
let [_, saturation, _] = rgb_to_hsv(color.r() as f64, color.g() as f64, color.b() as f64);
proximity * (1. - saturation)
let [_, saturation, _, _] = color.to_hsva();
proximity * (1. - saturation as f64)
}
/// Format a Color as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
/// Format a linear `Color` as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
fn color_to_hex_optional_alpha(color: &Color) -> String {
format!(
"#{}",
if color.a() >= 1. {
color.to_rgb_hex_srgb_from_gamma()
} else {
color.to_rgba_hex_srgb_from_gamma()
}
)
}
/// Parse a CSS color string (named color, hex, `rgb(...)`, etc.) into a `Color` using the `color` crate's CSS Color 4 parser.
/// 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:
/// - 1 char `f` → `#fff` (CSS 3-char shorthand)
/// - 2 char `ab` → `#ababab` (repeated to 6 chars)
/// - 4 char `abcd` → `#00abcd` (left-padded with `00`)
/// - 5 char `abcde` → `#0abcde` (left-padded with `0`)
/// - 3, 6, 8 char inputs are passed through with a `#` prefix.
fn parse_css_color(input: &str) -> Option<Color> {
let trimmed = input.trim();
let parsed = color::parse_color(trimmed).ok().or_else(|| {
let bare = trimmed.strip_prefix('#').unwrap_or(trimmed);
if bare.is_empty() || !bare.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let expanded = match bare.len() {
1 => bare.repeat(3),
2 => bare.repeat(3),
4 => format!("00{bare}"),
5 => format!("0{bare}"),
_ => bare.to_string(),
};
let candidate = format!("#{expanded}");
// Avoid retrying the exact same string we just failed to parse.
(candidate != trimmed).then(|| color::parse_color(&candidate).ok()).flatten()
})?;
let srgb: AlphaColor<Srgb> = parsed.to_alpha_color();
let [red, green, blue, alpha] = srgb.components;
Color::from_rgbaf32(red, green, blue, alpha)
SRGBA8::from(*color).to_css_hex()
}
@@ -13,10 +13,10 @@ use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
use graph_craft::document::NodeId;
use graphene_std::color::SRGBA8;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::style::FillChoiceUI;
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
@@ -167,16 +167,16 @@ pub enum FrontendMessage {
document_id: DocumentId,
},
UpdateGradientStopColorPickerPosition {
color: Color, // TODO: Color (without `none`) -> Color (with `none`)
color: SRGBA8, // TODO: Color (without `none`) -> Color (with `none`)
position: (f64, f64),
},
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker>` forwards this as its `colorOrGradient` event.
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker />` forwards this as its `colorOrGradient` event.
ColorPickerColorChanged {
value: FillChoice,
value: FillChoiceUI,
},
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker>` forwards this as its `startHistoryTransaction` event.
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker />` forwards this as its `startHistoryTransaction` event.
ColorPickerStartHistoryTransaction,
/// The Rust color picker handler is committing the in-flight undo transaction. The frontend `<ColorPicker>` forwards this as its `commitHistoryTransaction` event.
/// The Rust color picker handler is committing the in-flight undo transaction. The frontend `<ColorPicker />` forwards this as its `commitHistoryTransaction` event.
ColorPickerCommitHistoryTransaction,
UpdateImportsExports {
/// If the primary import is not visible, then it is None.
@@ -241,7 +241,7 @@ pub enum FrontendMessage {
svg: String,
},
UpdateImageData {
image_data: Vec<(u64, Image<Color>)>,
image_data: Vec<(u64, Image<SRGBA8>)>,
},
UpdateDocumentLayerDetails {
data: LayerPanelEntry,
@@ -14,7 +14,7 @@ pub struct DocumentInfo {
pub is_saved: bool,
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints, from_wasm_abi))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedState {
pub documents: Vec<DocumentInfo>,
@@ -1,7 +1,8 @@
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_std::vector::style::FillChoice;
use graphene_std::color::SRGBA8;
use graphene_std::vector::style::FillChoiceUI;
use serde_json::Value;
use std::collections::HashMap;
@@ -179,11 +180,11 @@ impl LayoutMessageHandler {
let callback_message = match action {
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
WidgetValueAction::Update => {
let Ok(fill_choice) = serde_json::from_value::<FillChoice>(value) else {
warn!("ColorInput update was not able to be parsed as FillChoice: {color_button:?}");
let Ok(fill_choice_ui) = serde_json::from_value::<FillChoiceUI>(value) else {
warn!("ColorInput update was not able to be parsed as FillChoiceUI: {color_button:?}");
return;
};
color_button.value = fill_choice;
color_button.value = fill_choice_ui;
(color_button.on_update.callback)(color_button)
}
};
@@ -496,25 +497,14 @@ fn populate_computed_display_fields(layout: &mut Layout) {
}
Widget::SpectrumInput(spectrum_input) => {
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
spectrum_input.track_start_css = spectrum_input
.track
.color
.first()
.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma()))
.unwrap_or_else(|| "black".to_string());
spectrum_input.track_end_css = spectrum_input
.track
.color
.last()
.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma()))
.unwrap_or_else(|| "black".to_string());
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
}
Widget::ColorComparisonInput(comparison) => {
use graphene_std::Color;
let contrasting = |color: Option<Color>| format!("#{}", color.map_or(Color::BLACK, |color| color.contrasting_text_color_from_gamma()).to_rgba_hex_srgb_from_gamma());
comparison.new_color_css = comparison.new_color.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma())).unwrap_or_default();
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
comparison.new_color_css = comparison.new_color.map(|color| color.to_css_hex()).unwrap_or_default();
comparison.new_color_contrasting = contrasting(comparison.new_color);
comparison.old_color_css = comparison.old_color.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma())).unwrap_or_default();
comparison.old_color_css = comparison.old_color.map(|color| color.to_css_hex()).unwrap_or_default();
comparison.old_color_contrasting = contrasting(comparison.old_color);
}
_ => {}
@@ -22,7 +22,7 @@ impl core::fmt::Display for WidgetId {
macro_rules! define_layout_target {
($($(#[$attr:meta])* $variant:ident),* $(,)?) => {
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum LayoutTarget {
@@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
use derivative::*;
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::style::FillChoiceUI;
use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -187,11 +187,10 @@ pub struct ImageButton {
#[derivative(Debug, PartialEq, Default)]
pub struct ColorInput {
// Content
/// WARNING: The colors are gamma, not linear!
#[widget_builder(constructor)]
pub value: FillChoice,
pub value: FillChoiceUI,
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
/// `None` when `value` is `FillChoice::None`, in which case the frontend uses its "none" fallback styling.
/// `None` when `value` is `FillChoiceUI::None`, in which case the frontend uses its "none" fallback styling.
#[serde(rename = "chosenGradient")]
#[widget_builder(skip)]
pub chosen_gradient: Option<String>,
@@ -4,8 +4,9 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use derivative::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
use graphene_std::transform::ReferencePoint;
use graphene_std::vector::style::{FillChoice, GradientStops};
use graphene_std::vector::style::{FillChoiceUI, GradientStopsUI};
use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -349,9 +350,9 @@ pub struct RadioEntryData {
pub struct WorkingColorsInput {
// Content
#[widget_builder(constructor)]
pub primary: Color,
pub primary: SRGBA8,
#[widget_builder(constructor)]
pub secondary: Color,
pub secondary: SRGBA8,
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -484,10 +485,10 @@ pub struct ColorComparisonInput {
// Content
#[widget_builder(constructor)]
#[serde(rename = "newColor")]
pub new_color: Option<Color>,
pub new_color: Option<SRGBA8>,
#[widget_builder(constructor)]
#[serde(rename = "oldColor")]
pub old_color: Option<Color>,
pub old_color: Option<SRGBA8>,
#[serde(rename = "isNone")]
pub is_none: bool,
#[serde(rename = "oldIsNone")]
@@ -544,7 +545,7 @@ pub struct ColorPresetsInput {
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ColorPresetsInputUpdate {
Preset(FillChoice),
Preset(FillChoiceUI),
EyedropperColorCode(String),
}
@@ -555,7 +556,7 @@ pub struct SpectrumInput {
// Content
/// The colored gradient drawn behind the markers (display-only, caller-owned).
#[widget_builder(constructor)]
pub track: GradientStops,
pub track: GradientStopsUI,
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackCSS")]
#[widget_builder(skip)]
@@ -607,14 +608,14 @@ pub struct SpectrumMarker {
position: f64,
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true. The last marker's value is ignored.
midpoint: f64,
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a [`Color`] (gamma space).
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
#[serde(rename = "handleColorCSS")]
handle_color_css: String,
}
impl SpectrumMarker {
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
let handle_color_css = format!("#{}", handle_color.to_rgba_hex_srgb_from_gamma());
let handle_color_css = SRGBA8::from(handle_color).to_css_hex();
Self { position, midpoint, handle_color_css }
}
}
@@ -7,12 +7,13 @@ use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::{Affine2, DAffine2, Vec2};
use graph_craft::document::NodeId;
use graphene_std::blending::BlendMode;
use graphene_std::color::SRGBA8;
use graphene_std::gradient::GradientStops;
use graphene_std::list::List;
use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientType};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
use graphene_std::{Artboard, Color, Context, Graphic};
use std::any::Any;
use std::sync::Arc;
@@ -385,11 +386,15 @@ impl TableItemLayout for Vector {
match self.style.fill.clone() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::None)
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::Solid(color))
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(color)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
@@ -398,7 +403,7 @@ impl TableItemLayout for Vector {
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(gradient.stops)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
@@ -423,7 +428,11 @@ impl TableItemLayout for Vector {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").narrow(true).widget_instance(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::from(&color))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
@@ -561,10 +570,10 @@ impl TableItemLayout for Color {
"Color"
}
fn identifier(&self) -> String {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
format!("Color (#{})", SRGBA8::from(*self).to_rgba_hex())
}
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
ColorInput::new(FillChoice::Solid(*self))
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(*self)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
@@ -584,7 +593,7 @@ impl TableItemLayout for GradientStops {
format!("Gradient ({} stops)", self.len())
}
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
ColorInput::new(FillChoice::Gradient(self.clone()))
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(self.clone())))
.menu_direction(Some(MenuDirection::Top))
.disabled(true)
.narrow(true)
@@ -30,7 +30,7 @@ use graphene_std::text_nodes::StringCapitalization;
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@@ -1164,19 +1164,19 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
match &**tagged_value {
TaggedValue::Color(color) => widgets.push(
color_button
.value(match color {
.value(FillChoiceUI::from(&match color {
Some(color) => FillChoice::Solid(*color),
None => FillChoice::None,
})
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid()), node_id, index))
}))
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid().map(Color::from)), node_id, index))
.on_commit(commit_value)
.widget_instance(),
),
TaggedValue::Gradient(stops) => widgets.push(
color_button
.value(FillChoice::Gradient(stops.clone()))
.value(FillChoiceUI::from(&FillChoice::Gradient(stops.clone())))
.on_update(update_value(
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().cloned().unwrap_or_default()),
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().map(GradientStops::from).unwrap_or_default()),
node_id,
index,
))
@@ -1417,7 +1417,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
// Build the shared spectrum widget (placed on the first non-exposed row)
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
SpectrumInput::new(bw_track())
SpectrumInput::new(GradientStopsUI::from(&bw_track()))
.markers(spectrum_markers)
.show_midpoints(false)
.allow_insert(false)
@@ -1591,7 +1591,7 @@ fn spectrum_slider_row(
let position_to_value = move |position: f64| value_min + position * value_range;
row.push(
SpectrumInput::new(track)
SpectrumInput::new(GradientStopsUI::from(&track))
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
.show_midpoints(false)
.allow_insert(false)
@@ -2482,7 +2482,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push(
ColorInput::default()
.value(fill.clone().into())
.value(FillChoiceUI::from(&FillChoice::from(fill.clone())))
.on_update(move |x: &ColorInput| Message::Batched {
messages: Box::new([
match &fill2 {
@@ -2508,7 +2508,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
NodeGraphMessage::SetInputValue {
node_id,
input_index: FillInput::<Color>::INDEX,
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
value: TaggedValue::Fill(FillChoice::from(&x.value).to_fill(fill2.as_gradient())),
}
.into(),
]),
@@ -3,9 +3,9 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
use crate::messages::prelude::*;
use glam::DVec2;
use graphene_std::raster::color::Color;
use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad;
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::style::FillChoiceUI;
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
let origin = document.snapping_state.grid.origin;
@@ -274,12 +274,12 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
Separator::new(SeparatorStyle::Related).widget_instance(),
]);
color_widgets.push(
ColorInput::new(FillChoice::Solid(Color::from_hex_str(&grid.color).unwrap_or(Color::BLACK)))
ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
.tooltip_label("Grid Display Color")
.allow_none(false)
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
if let Some(color) = color.value.as_solid() {
grid.color = format!("#{}", color.to_rgba_hex_srgb_from_gamma());
if let Some(srgba) = color.value.as_solid() {
grid.color = srgba.to_css_hex();
}
}))
.widget_instance(),
@@ -999,13 +999,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
});
// Add default fill and stroke to the layer
let fill_color = Color::WHITE;
let stroke_color = Color::BLACK;
let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb());
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE);
responses.add(GraphOperationMessage::FillSet { layer, fill });
let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH);
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
// Create new point ids and add those into the existing Vector path
@@ -34,6 +34,7 @@ impl FontCatalog {
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogFamily {
/// The font family name.
@@ -55,6 +56,7 @@ impl FontCatalogFamily {
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogStyle {
pub weight: u32,
@@ -88,7 +90,7 @@ impl FontCatalogStyle {
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum PanelType {
Welcome,
@@ -124,7 +126,7 @@ impl PanelType {
pub struct PanelGroupId(pub u64);
/// Which edge of a panel group to split on when docking a dragged panel.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DockingSplitDirection {
Left,
@@ -6,7 +6,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::utility_types::DocumentToolData;
use graphene_std::Color;
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{FillChoice, FillChoiceUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
pub struct ToolColorOptions {
@@ -47,13 +47,12 @@ impl ToolColorOptions {
self.enabled == Some(true)
}
/// The active solid color in linear sRGB, suitable for storing in a working color or downstream rendering input.
/// `fill_choice` is stored in gamma space (per [`FillChoice`]'s contract), so this method converts to linear before returning.
/// The active solid color, suitable for storing in a working color or downstream rendering input.
pub fn active_color(&self) -> Option<Color> {
if !self.is_active() {
return None;
}
Some(self.fill_choice.as_ref()?.as_solid()?.to_linear_srgb())
self.fill_choice.as_ref()?.as_solid()
}
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
@@ -86,7 +85,8 @@ impl ToolColorOptions {
// In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position.
// `FillChoice::None` gives it a neutral starting point.
let mixed_color = self.fill_choice.is_none();
let widget_value = self.fill_choice.clone().unwrap_or(FillChoice::None);
// Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoiceUI` (with `SRGBA8` colors) for the widget value.
let widget_value = FillChoiceUI::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None));
let mixed_enabled = self.enabled.is_none();
// In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash.
// The frontend's click handler sends `true` when the user resolves the mixed state by clicking.
@@ -191,10 +191,9 @@ impl DrawingToolState {
}
}
/// Builds a `FillChoice::Solid` from a linear-space color, applying gamma conversion to display sRGB.
/// Common helper used throughout the color-syncing code where working colors (linear) flow into swatches that store gamma-encoded colors.
pub fn solid_gamma(color: Color) -> FillChoice {
FillChoice::Solid(color.to_gamma_srgb())
/// Builds a `FillChoice::Solid` from a color.
pub fn solid(color: Color) -> FillChoice {
FillChoice::Solid(color)
}
/// The fill working color (the source for the fill swatch when nothing is selected).
@@ -219,8 +218,8 @@ pub fn sync_color_options(
document: &DocumentMessageHandler,
selection_changed: bool,
) -> bool {
let fill_fallback = solid_gamma(fill_working_color(global, drawing.colors_swapped));
let stroke_fallback = solid_gamma(stroke_working_color(global, drawing.colors_swapped));
let fill_fallback = solid(fill_working_color(global, drawing.colors_swapped));
let stroke_fallback = solid(stroke_working_color(global, drawing.colors_swapped));
let mut changed = false;
@@ -364,7 +363,7 @@ fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessag
/// Same as [`sync_color_options`] but for tools that only have a fill option (e.g., text). The fill follows the given working color when nothing is selected.
pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, fill_color: Color, document: &DocumentMessageHandler, selection_changed: bool) -> bool {
let fill_fallback = solid_gamma(fill_color);
let fill_fallback = solid(fill_color);
let new_fill = if let Some(state) = graph_modification_utils::selected_fill_state(document) {
let active = state.enabled == Some(true);
@@ -417,11 +416,7 @@ pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: Fill
}
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
} else if let FillChoice::Solid(color) = fill_choice {
// Swatch is gamma; working colors are linear.
responses.add(ToolMessage::SelectWorkingColor {
color: color.to_linear_srgb(),
primary: slot_is_primary,
});
responses.add(ToolMessage::SelectWorkingColor { color, primary: slot_is_primary });
}
}
@@ -436,9 +431,8 @@ pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option<Col
}
graph_modification_utils::set_stroke_color_for_selected_layers(color, drawing.effective_line_weight(), document, responses);
} else if let Some(color) = color {
// Swatch is gamma; working colors are linear.
responses.add(ToolMessage::SelectWorkingColor {
color: color.to_linear_srgb(),
color,
primary: !drawing.colors_swapped,
});
}
@@ -459,14 +453,14 @@ pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, worki
// Mixed re-tick has no per-layer color to restore; fall back to the working color and keep tracking it.
let fill_choice = fill.fill_choice.clone().unwrap_or_else(|| {
fill.tracks_working_color = true;
solid_gamma(working_color)
solid(working_color)
});
fill.fill_choice = Some(fill_choice.clone());
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
} else {
// Unticking from mixed: capture the working color as the saved value so the swatch keeps following the link.
if fill.fill_choice.is_none() {
fill.fill_choice = Some(solid_gamma(working_color));
fill.fill_choice = Some(solid(working_color));
fill.tracks_working_color = true;
}
graph_modification_utils::remove_fill_for_selected_layers(document, responses);
@@ -482,13 +476,13 @@ pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, globa
if enabled {
let stroke_choice = drawing.stroke.fill_choice.clone().unwrap_or_else(|| {
drawing.stroke.tracks_working_color = true;
solid_gamma(stroke_working_color(global, drawing.colors_swapped))
solid(stroke_working_color(global, drawing.colors_swapped))
});
drawing.stroke.fill_choice = Some(stroke_choice.clone());
graph_modification_utils::set_stroke_color_for_selected_layers(stroke_choice.as_solid(), drawing.effective_line_weight(), document, responses);
} else {
if drawing.stroke.fill_choice.is_none() {
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
drawing.stroke.fill_choice = Some(solid(stroke_working_color(global, drawing.colors_swapped)));
drawing.stroke.tracks_working_color = true;
}
graph_modification_utils::remove_stroke_for_selected_layers(document, responses);
@@ -513,14 +507,14 @@ pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToo
/// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`].
pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) {
if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) {
slot.fill_choice = Some(solid_gamma(working_color));
slot.fill_choice = Some(solid(working_color));
}
}
/// Resets the tool's swatches to the working colors. Called on tool deactivation and shape-mode changes.
pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &DocumentToolData) {
drawing.fill.fill_choice = Some(solid_gamma(fill_working_color(global, drawing.colors_swapped)));
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
drawing.fill.fill_choice = Some(solid(fill_working_color(global, drawing.colors_swapped)));
drawing.stroke.fill_choice = Some(solid(stroke_working_color(global, drawing.colors_swapped)));
drawing.fill.tracks_working_color = true;
drawing.stroke.tracks_working_color = true;
}
@@ -329,10 +329,10 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
let &TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(color.to_linear_srgb())
Some(color)
}
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
@@ -9,6 +9,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
use crate::messages::tool::utility_types::{HintData, ToolType};
use crate::node_graph_executor::NodeGraphExecutor;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays { context }.into();
@@ -280,7 +281,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
let r = (random_number >> 16) as u8;
let g = (random_number >> 8) as u8;
let b = random_number as u8;
let random_color = Color::from_rgba8_srgb(r, g, b, 255);
let random_color = Color::from(SRGBA8::new(r, g, b, 255));
if primary {
document_data.primary_color = random_color;
@@ -4,13 +4,13 @@ use crate::messages::portfolio::document::graph_operation::transform_utils::get_
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid_gamma};
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid};
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_std::Color;
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
use graphene_std::raster::BlendMode;
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::style::{FillChoice, FillChoiceUI};
const BRUSH_MAX_SIZE: f64 = 5000.;
@@ -104,13 +104,12 @@ impl ToolMetadata for BrushTool {
impl LayoutHolder for BrushTool {
fn layout(&self) -> Layout {
let mut widgets = vec![
ColorInput::new(self.options.color.fill_choice.clone().unwrap_or(FillChoice::None))
ColorInput::new(FillChoiceUI::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
.mixed(self.options.color.fill_choice.is_none())
.narrow(true)
.on_update(|color: &ColorInput| {
BrushToolMessage::UpdateOptions {
// The picker emits gamma-space colors; working colors are stored in linear sRGB.
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|c| c.to_linear_srgb())),
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(Color::from)),
}
.into()
})
@@ -245,7 +244,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
}
}
BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
self.options.color.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
self.options.color.fill_choice = Some(solid(context.global_tool_data.primary_color));
}
}
@@ -1,6 +1,7 @@
use super::tool_prelude::*;
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
use crate::messages::tool::utility_types::DocumentToolData;
use graphene_std::color::SRGBA8;
use graphene_std::vector::style::RenderMode;
#[derive(Default, ExtractField)]
@@ -233,8 +234,8 @@ fn update_cursor_preview_common(
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
image,
mouse_position: Some(input.mouse.position.into()),
primary_color: "#".to_string() + global_tool_data.primary_color.to_rgb_hex_srgb().as_str(),
secondary_color: "#".to_string() + global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(),
primary_color: SRGBA8::from(global_tool_data.primary_color).to_css_hex(),
secondary_color: SRGBA8::from(global_tool_data.secondary_color).to_css_hex(),
set_color_choice,
});
}
@@ -1,9 +1,10 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::color_selector::solid_gamma;
use crate::messages::tool::common_functionality::color_selector::solid;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::Fill;
use graphene_std::vector::style::{Fill, FillChoiceUI};
#[derive(Default, ExtractField)]
pub struct FillTool {
@@ -43,11 +44,11 @@ impl ToolMetadata for FillTool {
impl LayoutHolder for FillTool {
fn layout(&self) -> Layout {
let widgets = vec![
ColorInput::new(solid_gamma(self.primary_color))
ColorInput::new(FillChoiceUI::from(&solid(self.primary_color)))
.narrow(true)
.on_update(|color: &ColorInput| {
FillToolMessage::SetColor {
color: color.value.as_solid().map(|c| c.to_linear_srgb()),
color: color.value.as_solid().map(Color::from),
}
.into()
})
@@ -141,7 +142,7 @@ impl Fsm for FillToolFsmState {
// Get the layer the user is hovering over
if let Some(layer) = document.click(input, viewport) {
let color_hex = format!("#{}", preview_color.to_rgba_hex_srgb());
let color_hex = SRGBA8::from(preview_color).to_css_hex();
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex);
}
@@ -161,8 +162,8 @@ impl Fsm for FillToolFsmState {
return self;
}
let fill = match color_event {
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color.to_gamma_srgb()),
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()),
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color),
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color),
_ => return self,
};
@@ -201,6 +202,7 @@ impl Fsm for FillToolFsmState {
#[cfg(test)]
mod test_fill {
pub use crate::test_utils::test_prelude::*;
use graphene_std::color::SRGBA8;
use graphene_std::vector::fill;
use graphene_std::vector::style::Fill;
@@ -240,7 +242,7 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::GREEN));
}
#[tokio::test]
@@ -252,6 +254,6 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1);
assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::YELLOW.to_rgba8_srgb());
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::YELLOW));
}
}
@@ -97,7 +97,7 @@ impl LayoutHolder for FreehandTool {
},
|color: &ColorInput| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::FillColor(color.value.clone()),
options: FreehandOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
},
@@ -127,7 +127,7 @@ impl LayoutHolder for FreehandTool {
},
|color: &ColorInput| {
FreehandToolMessage::UpdateOptions {
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid()),
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
}
.into()
},
@@ -11,8 +11,9 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::{Fill, FillChoice, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType};
#[derive(Default, ExtractField)]
pub struct GradientTool {
@@ -49,7 +50,7 @@ pub enum GradientToolMessage {
CommitTransactionForColorStop,
CloseStopColorPicker,
UpdateStopColor { color: Color },
UpdateStops { stops: GradientStops },
UpdateStops { stops: GradientStopsUI },
UpdateOptions { options: GradientOptionsUpdate },
}
@@ -120,7 +121,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
}
}
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
apply_stops_update(&mut self.data, context, responses, stops);
apply_stops_update(&mut self.data, context, responses, GradientStops::from(&stops));
}
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open {
@@ -243,7 +244,7 @@ impl LayoutHolder for GradientTool {
},
]))
});
let stops_widget = ColorInput::new(stops_value)
let stops_widget = ColorInput::new(FillChoiceUI::from(&stops_value))
.allow_none(false)
.narrow(true)
.tooltip_label("Gradient Stops")
@@ -856,7 +857,7 @@ impl Fsm for GradientToolFsmState {
let (start, end) = (transform.transform_point2(*start), transform.transform_point2(*end));
fn color_to_hex(color: graphene_std::Color) -> String {
format!("#{}", color.to_rgb_hex_srgb_from_gamma())
SRGBA8::from(color).to_css_hex()
}
let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
@@ -1024,10 +1025,10 @@ impl Fsm for GradientToolFsmState {
let transform = gradient_space_transform(layer, document);
let gradient = &selected_gradient.gradient;
if stop_index < gradient.stops.position.len() {
let color = gradient.stops.color[stop_index].to_gamma_srgb();
let color = gradient.stops.color[stop_index];
let position = gradient.stops.position[stop_index];
let position = transform.transform_point2(gradient.start.lerp(gradient.end, position)).into();
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position });
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color: color.into(), position });
}
}
@@ -1081,9 +1082,9 @@ impl Fsm for GradientToolFsmState {
.transform
.transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos));
let position = viewport_pos.into();
let color = selected_gradient.gradient.stops.color[stop_index].to_gamma_srgb();
let color = selected_gradient.gradient.stops.color[stop_index];
tool_data.color_picker_editing_color_stop = Some(stop_index);
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position });
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color: color.into(), position });
}
}
_ => {}
@@ -1910,6 +1911,7 @@ mod test_gradient {
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
use graphene_std::vector::style::{Fill, Gradient};
use graphene_std::vector::{GradientStop, GradientStops, fill};
@@ -2022,8 +2024,8 @@ mod test_gradient {
let (gradient, transform) = get_gradient(&mut editor).await;
// Gradient goes from primary color to secondary color
let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::<Vec<_>>();
assert_eq!(stops, vec![(0., Color::GREEN.to_rgba8_srgb()), (1., Color::BLUE.to_rgba8_srgb())]);
let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>();
assert_eq!(stops, vec![(0., SRGBA8::from(Color::GREEN)), (1., SRGBA8::from(Color::BLUE))]);
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
}
@@ -2215,7 +2217,7 @@ mod test_gradient {
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
let middle_color = stops.color[1].to_rgba8_srgb();
let middle_color = SRGBA8::from(stops.color[1]);
// Simulate dragging the middle stop to position 0.8
let click_position = DVec2::new(25., 0.);
@@ -2256,9 +2258,9 @@ mod test_gradient {
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
// Colors should maintain their associations with the stop points
assert_eq!(updated_stops.color[0].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
assert_eq!(updated_stops.color[1].to_rgba8_srgb(), middle_color);
assert_eq!(updated_stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
assert_eq!(SRGBA8::from(updated_stops.color[0]), SRGBA8::from(Color::GREEN));
assert_eq!(SRGBA8::from(updated_stops.color[1]), middle_color);
assert_eq!(SRGBA8::from(updated_stops.color[2]), SRGBA8::from(Color::BLUE));
}
#[tokio::test]
@@ -2495,8 +2497,8 @@ mod test_gradient {
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10);
assert_eq!(updated.stops.color[0].to_rgba8_srgb(), Color::RED.to_rgba8_srgb(), "First stop color should be preserved");
assert_eq!(updated.stops.color[1].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb(), "Middle stop color should be preserved");
assert_eq!(updated.stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb(), "Last stop color should be preserved");
assert_eq!(SRGBA8::from(updated.stops.color[0]), SRGBA8::from(Color::RED), "First stop color should be preserved");
assert_eq!(SRGBA8::from(updated.stops.color[1]), SRGBA8::from(Color::GREEN), "Middle stop color should be preserved");
assert_eq!(SRGBA8::from(updated.stops.color[2]), SRGBA8::from(Color::BLUE), "Last stop color should be preserved");
}
}
@@ -2850,13 +2850,10 @@ impl Fsm for PathToolFsmState {
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
// Defaults chosen because the pasted geometry has no inherent associated style
let stroke_color = Color::BLACK;
let fill_color = Color::WHITE;
let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH);
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb());
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE);
responses.add(GraphOperationMessage::FillSet { layer, fill });
new_layers.push(layer);
@@ -154,7 +154,7 @@ impl LayoutHolder for PenTool {
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::FillColor(color.value.clone()),
options: PenOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
},
@@ -184,7 +184,7 @@ impl LayoutHolder for PenTool {
},
|color: &ColorInput| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::StrokeColor(color.value.as_solid()),
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
}
.into()
},
@@ -248,7 +248,7 @@ impl LayoutHolder for SelectTool {
},
|color: &ColorInput| {
SelectToolMessage::SelectOptions {
options: SelectOptionsUpdate::FillColor(color.value.clone()),
options: SelectOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
},
@@ -278,7 +278,7 @@ impl LayoutHolder for SelectTool {
},
|color: &ColorInput| {
SelectToolMessage::SelectOptions {
options: SelectOptionsUpdate::StrokeColor(color.value.as_solid()),
options: SelectOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
}
.into()
},
@@ -467,7 +467,7 @@ impl LayoutHolder for ShapeTool {
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColor(color.value.clone()),
options: ShapeOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
},
@@ -498,7 +498,7 @@ impl LayoutHolder for ShapeTool {
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid()),
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
}
.into()
},
@@ -105,7 +105,7 @@ impl LayoutHolder for SplineTool {
},
|color: &ColorInput| {
SplineToolMessage::UpdateOptions {
options: SplineOptionsUpdate::FillColor(color.value.clone()),
options: SplineOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
},
@@ -135,7 +135,7 @@ impl LayoutHolder for SplineTool {
},
|color: &ColorInput| {
SplineToolMessage::UpdateOptions {
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid()),
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
}
.into()
},
@@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Inpu
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid_gamma, sync_fill_only,
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid, sync_fill_only,
};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
@@ -20,9 +20,10 @@ use crate::messages::tool::utility_types::ToolRefreshOptions;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::choice_type::ChoiceTypeStatic;
use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad;
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
use graphene_std::vector::style::{Fill, FillChoice};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI};
use graphene_std::{Color, NodeInputDecleration};
#[derive(Default, ExtractField)]
@@ -269,12 +270,12 @@ impl TextTool {
fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout {
let mut widgets = vec![
ColorInput::new(self.options.fill.fill_choice.clone().unwrap_or(graphene_std::vector::style::FillChoice::None))
ColorInput::new(FillChoiceUI::from(self.options.fill.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
.mixed(self.options.fill.fill_choice.is_none())
.narrow(true)
.on_update(|color: &ColorInput| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::FillColor(color.value.clone()),
options: TextOptionsUpdate::FillColor(FillChoice::from(&color.value)),
}
.into()
})
@@ -295,7 +296,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
// reset the displayed fill color so the next activation starts fresh from the current working color.
// Guarded on `Ready` so Esc-mid-editing (which also fires Abort) doesn't wipe the user's customized fill option.
if matches!(&message, ToolMessage::Text(TextToolMessage::Abort)) && self.fsm_state == TextToolFsmState::Ready {
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
self.options.fill.fill_choice = Some(solid(context.global_tool_data.primary_color));
}
let options = match message {
@@ -319,7 +320,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
if can_edit_selected(context.document).is_some() {
sync_fill_only(&mut self.options.fill, true, context.global_tool_data.primary_color, context.document, selection_changed);
} else if selection_changed {
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
self.options.fill.fill_choice = Some(solid(context.global_tool_data.primary_color));
self.options.fill.tracks_working_color = true;
}
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
@@ -497,7 +498,7 @@ impl TextToolData {
text: editing_text.text.clone(),
line_height_ratio: editing_text.typesetting.line_height_ratio,
font_size: editing_text.typesetting.font_size,
color: editing_text.color.map_or("#000000".to_string(), |color| format!("#{}", color.to_rgba_hex_srgb())),
color: editing_text.color.map_or("#000000".to_string(), |color| SRGBA8::from(color).to_css_hex()),
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
transform: editing_text.transform.to_cols_array(),
max_width: editing_text.typesetting.max_width,
@@ -574,7 +575,7 @@ impl TextToolData {
});
responses.add(GraphOperationMessage::FillSet {
layer: self.layer,
fill: if let Some(color) = editing_text.color { Fill::Solid(color.to_gamma_srgb()) } else { Fill::None },
fill: if let Some(color) = editing_text.color { Fill::Solid(color) } else { Fill::None },
});
let transform = editing_text.transform;
self.editing_text = Some(editing_text);
+2 -3
View File
@@ -14,6 +14,7 @@ use crate::messages::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
use crate::node_graph_executor::NodeGraphExecutor;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use std::borrow::Cow;
use std::fmt::{self, Debug};
@@ -128,9 +129,7 @@ pub struct DocumentToolData {
impl DocumentToolData {
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
let layout = Layout(vec![
LayoutGroup::row(vec![
WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance(),
]),
LayoutGroup::row(vec![WorkingColorsInput::new(SRGBA8::from(self.primary_color), SRGBA8::from(self.secondary_color)).widget_instance()]),
LayoutGroup::row(vec![
IconButton::new("SwapVertical", 16)
.tooltip_label("Swap Working Colors")
+16 -1
View File
@@ -6,6 +6,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation};
use graphene_std::color::SRGBA8;
use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::RenderMetadata;
use graphene_std::text::FontCache;
@@ -398,7 +399,21 @@ impl NodeGraphExecutor {
match render_output.data {
RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
// Convert each linear-light `Image<Color>` into the JS-boundary `Image<SRGBA8>` form (gamma byte channels) before dispatching.
let image_data = image_data
.into_iter()
.map(|(id, image)| {
(
id,
graphene_std::raster::Image {
width: image.width,
height: image.height,
data: image.data.iter().map(|&c| SRGBA8::from(c)).collect(),
base64_string: image.base64_string,
},
)
})
.collect();
responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}