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
+43 -83
View File
@@ -16,14 +16,17 @@ use editor::consts::FILE_EXTENSION;
use editor::messages::clipboard::utility_types::ClipboardContentRaw;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
use editor::messages::layout::utility_types::layout_widget::LayoutTarget;
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType};
use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
use graph_craft::document::NodeId;
use graphene_std::color::SRGBA8;
use graphene_std::graphene_hash::CacheHashWrapper;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, FillChoiceUI};
use serde::Serialize;
use serde_wasm_bindgen::{self, from_value};
use std::cell::RefCell;
@@ -319,20 +322,20 @@ impl EditorWrapper {
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
#[wasm_bindgen(js_name = widgetValueUpdate)]
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
pub fn widget_value_update(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)
}
/// Commit the value of a given UI widget to the history
#[wasm_bindgen(js_name = widgetValueCommit)]
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
pub fn widget_value_commit(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
self.widget_value_commit_helper(layout_target, widget_id, value)
}
/// Update the value of a given UI widget, and commit it to the history
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?;
pub fn widget_value_commit_and_update(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
self.widget_value_commit_helper(layout_target, widget_id, value.clone())?;
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
// Close out a transaction that the widget's `on_commit` opened (if any), so a single click on widgets like the
// NumberInput's increment buttons collapses into one history step instead of leaving the transaction in `Modified`
@@ -346,34 +349,24 @@ impl EditorWrapper {
self.dispatch(DocumentMessage::EndTransaction);
}
pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
pub fn widget_value_update_helper(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
let widget_id = WidgetId(widget_id);
match (from_value(layout_target), from_value(value)) {
(Ok(layout_target), Ok(value)) => {
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
self.dispatch(message);
if resend_widget {
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
self.dispatch(resend_message);
}
Ok(())
}
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
let value: serde_json::Value = from_value(value).map_err(|e| Error::new(&format!("Could not update UI: {e}")))?;
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
self.dispatch(message);
if resend_widget {
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
self.dispatch(resend_message);
}
Ok(())
}
pub fn widget_value_commit_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
pub fn widget_value_commit_helper(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
let widget_id = WidgetId(widget_id);
match (from_value(layout_target), from_value(value)) {
(Ok(layout_target), Ok(value)) => {
let message = LayoutMessage::WidgetValueCommit { layout_target, widget_id, value };
self.dispatch(message);
Ok(())
}
(target, val) => Err(Error::new(&format!("Could not commit UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
}
let value: serde_json::Value = from_value(value).map_err(|e| Error::new(&format!("Could not commit UI: {e}")))?;
let message = LayoutMessage::WidgetValueCommit { layout_target, widget_id, value };
self.dispatch(message);
Ok(())
}
#[wasm_bindgen(js_name = loadPreferences)]
@@ -389,14 +382,8 @@ impl EditorWrapper {
}
#[wasm_bindgen(js_name = loadPersistedState)]
pub fn load_persisted_state(&self, state: JsValue) {
let Ok(state) = serde_wasm_bindgen::from_value(state) else {
log::error!("Failed to deserialize persisted state");
return;
};
let message = PersistentStateMessage::LoadState { state };
self.dispatch(message);
pub fn load_persisted_state(&self, state: editor::messages::frontend::utility_types::PersistedState) {
self.dispatch(PersistentStateMessage::LoadState { state });
}
#[wasm_bindgen(js_name = loadDocumentContent)]
@@ -494,9 +481,7 @@ impl EditorWrapper {
}
#[wasm_bindgen(js_name = splitPanelGroup)]
pub fn split_panel_group(&self, target_group: u64, direction: String, tabs: JsValue, active_tab_index: usize) {
let direction: DockingSplitDirection = serde_wasm_bindgen::from_value(JsValue::from_str(&direction)).unwrap();
let tabs: Vec<PanelType> = serde_wasm_bindgen::from_value(tabs).unwrap();
pub fn split_panel_group(&self, target_group: u64, direction: DockingSplitDirection, tabs: Vec<PanelType>, active_tab_index: usize) {
let message = PortfolioMessage::SplitPanelGroup {
target_group: PanelGroupId(target_group),
direction,
@@ -507,9 +492,8 @@ impl EditorWrapper {
}
#[wasm_bindgen(js_name = setPanelGroupSizes)]
pub fn set_panel_group_sizes(&self, split_path: JsValue, sizes: JsValue) {
let split_path: Vec<usize> = serde_wasm_bindgen::from_value(split_path).unwrap();
let sizes: Vec<f64> = serde_wasm_bindgen::from_value(sizes).unwrap();
pub fn set_panel_group_sizes(&self, split_path: Vec<u32>, sizes: Vec<f64>) {
let split_path = split_path.into_iter().map(|i| i as usize).collect();
let message = PortfolioMessage::SetPanelGroupSizes { split_path, sizes };
self.dispatch(message);
}
@@ -645,13 +629,8 @@ impl EditorWrapper {
/// The font catalog has been loaded
#[wasm_bindgen(js_name = onFontCatalogLoad)]
pub fn on_font_catalog_load(&self, catalog: JsValue) -> Result<(), JsValue> {
// Deserializing from TS type: `{ name: string; styles: { weight: number, italic: boolean, url: string }[] }[]`
let families = serde_wasm_bindgen::from_value::<Vec<FontCatalogFamily>>(catalog)?;
let message = PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(families) };
self.dispatch(message);
Ok(())
pub fn on_font_catalog_load(&self, catalog: Vec<FontCatalogFamily>) {
self.dispatch(PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(catalog) });
}
/// A font has been downloaded
@@ -679,44 +658,29 @@ impl EditorWrapper {
Ok(())
}
/// Update primary color with values on a scale from 0 to 1.
/// Update primary color from sRGB bytes (the wire format at the JS boundary).
#[wasm_bindgen(js_name = updatePrimaryColor)]
pub fn update_primary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let Some(primary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
return Err(Error::new("Invalid color").into());
};
let message = ToolMessage::SelectWorkingColor {
color: primary_color.to_linear_srgb(),
pub fn update_primary_color(&self, color: SRGBA8) {
self.dispatch(ToolMessage::SelectWorkingColor {
color: Color::from(color),
primary: true,
};
self.dispatch(message);
Ok(())
});
}
/// Update secondary color with values on a scale from 0 to 1.
/// Update secondary color from sRGB bytes (the wire format at the JS boundary).
#[wasm_bindgen(js_name = updateSecondaryColor)]
pub fn update_secondary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let Some(secondary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
return Err(Error::new("Invalid color").into());
};
let message = ToolMessage::SelectWorkingColor {
color: secondary_color.to_linear_srgb(),
pub fn update_secondary_color(&self, color: SRGBA8) {
self.dispatch(ToolMessage::SelectWorkingColor {
color: Color::from(color),
primary: false,
};
self.dispatch(message);
Ok(())
});
}
/// Initialize the Rust color picker handler with a starting value (used when the frontend `<ColorPicker>` opens).
/// Initialize the Rust color picker handler with a starting value (used when the frontend `<ColorPicker />` opens).
#[wasm_bindgen(js_name = openColorPicker)]
pub fn open_color_picker(&self, initial_value: JsValue, allow_none: bool, disabled: bool) -> Result<(), JsValue> {
let initial_value = serde_wasm_bindgen::from_value(initial_value).map_err(|e| Error::new(&format!("Invalid initial picker value: {e}")))?;
pub fn open_color_picker(&self, initial_value: FillChoiceUI, allow_none: bool, disabled: bool) {
let initial_value = FillChoice::from(&initial_value);
self.dispatch(ColorPickerMessage::Open { initial_value, allow_none, disabled });
Ok(())
}
/// Tell the Rust color picker handler that the popover is closing.
@@ -725,14 +689,10 @@ impl EditorWrapper {
self.dispatch(ColorPickerMessage::Close);
}
/// Update the color of the currently-edited gradient stop
/// Update the color of the currently-edited gradient stop, from sRGB bytes (the wire format at the JS boundary).
#[wasm_bindgen(js_name = updateGradientStopColor)]
pub fn update_gradient_stop_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) else {
return Err(Error::new("Invalid color").into());
};
self.dispatch(GradientToolMessage::UpdateStopColor { color: color.to_linear_srgb() });
Ok(())
pub fn update_gradient_stop_color(&self, color: SRGBA8) {
self.dispatch(GradientToolMessage::UpdateStopColor { color: Color::from(color) });
}
/// Start a new undo transaction for gradient stop color editing
+5 -4
View File
@@ -6,8 +6,8 @@ use crate::{EDITOR_HAS_CRASHED, EDITOR_WRAPPER};
use editor::application::Editor;
use editor::messages::input_mapper::utility_types::input_keyboard::Key;
use editor::messages::prelude::*;
use graphene_std::color::SRGBA8;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
use js_sys::{Object, Reflect};
use std::sync::atomic::Ordering;
use std::time::Duration;
@@ -119,7 +119,7 @@ pub(crate) fn auto_save_all_documents() {
});
}
pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)]) {
pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<SRGBA8>)]) {
let window = match window() {
Some(window) => window,
None => {
@@ -167,8 +167,9 @@ pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)])
.expect("2d context was not found")
.dyn_into::<CanvasRenderingContext2d>()
.expect("Failed to cast context to CanvasRenderingContext2d");
let u8_data: Vec<u8> = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect();
let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]);
// `SRGBA8` is `#[repr(C)]` of four `u8`s, so the data buffer is already the byte format the canvas expects
let u8_data: &[u8] = bytemuck::cast_slice(&image.data);
let clamped_u8_data = wasm_bindgen::Clamped(u8_data);
match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) {
Ok(image_data_obj) => {
if context.put_image_data(&image_data_obj, 0., 0.).is_err() {