Wrap serialized gradient stops in a new GradientRamp struct and unify FillChoice (#4400)

* Introduce the GradientRamp exchange struct as the serialized TaggedValue::Gradient payload

* Unify FillChoice and FillChoiceUI into one enum generic over color format, carrying GradientRamp stops

* Rename the TaggedValue::Gradient variant to GradientRamp to match its payload

* Move the Color variant into the tagged_value macro list since its stored and wire forms match
This commit is contained in:
Keavon Chambers
2026-08-03 23:23:41 -07:00
committed by Dennis Kobert
parent 4d8729d87c
commit 360321e0ce
32 changed files with 411 additions and 288 deletions

View File

@@ -5,14 +5,14 @@
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
import WidgetLayout from "/src/components/widgets/WidgetLayout.svelte";
import type { ColorPickerCallbacks, ColorPickerStore } from "/src/stores/color-picker";
import type { EditorWrapper, FillChoiceUI, MenuDirection } from "/wrapper/pkg/graphite_wasm_wrapper";
import type { EditorWrapper, FillChoice, MenuDirection, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoiceUI; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice<SRGBA8>; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
const editor = getContext<EditorWrapper>("editor");
const colorPickerStore = getContext<ColorPickerStore>("colorPicker");
export let colorOrGradient: FillChoiceUI;
export let colorOrGradient: FillChoice<SRGBA8>;
export let allowNone = false;
// export let allowTransparency = false; // TODO: Implement
export let disabled = false;

View File

@@ -13,7 +13,7 @@
import type { DocumentStore } from "/src/stores/document";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import type { MessageBody } from "/src/subscriptions-router";
import { fillChoiceUIColor, createSRgba8 } from "/src/utility-functions/colors";
import { fillChoiceColor, createSRgba8 } from "/src/utility-functions/colors";
import { pasteFile } from "/src/utility-functions/files";
import { textInputCleanup } from "/src/utility-functions/keyboard-entry";
import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization";
@@ -680,7 +680,7 @@
}}
colorOrGradient={{ Solid: gradientStopPickerColor || createSRgba8(0, 0, 0, 255) }}
on:colorOrGradient={({ detail }) => {
const color = fillChoiceUIColor(detail);
const color = fillChoiceColor(detail);
if (color) editor.updateGradientStopColor(color);
}}
on:startHistoryTransaction={() => editor.startGradientStopColorTransaction()}

View File

@@ -26,7 +26,7 @@
import ShortcutLabel from "/src/components/widgets/labels/ShortcutLabel.svelte";
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
import type { ColorPickerStore } from "/src/stores/color-picker";
import { parseFillChoiceUI } from "/src/utility-functions/colors";
import { parseFillChoice } from "/src/utility-functions/colors";
import type { EditorWrapper, LayoutTarget, Widget, WidgetInstance } from "/wrapper/pkg/graphite_wasm_wrapper";
// Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...)
@@ -138,7 +138,7 @@
component: ColorInput,
getProps: (props, index) => ({
...props,
value: parseFillChoiceUI(props.value),
value: parseFillChoice(props.value),
$$events: {
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
startHistoryTransaction: () => widgetValueCommit(index, props.value),
@@ -150,7 +150,7 @@
getProps: (props, index) => ({
...props,
$$events: {
// The widget dispatches `"None"` or a bare `SRGBA8`, wrap the color in `{ Solid: ... }` so the payload matches Rust's `FillChoiceUI` shape (which the `Preset` variant expects).
// The widget dispatches `"None"` or a bare `SRGBA8`, wrap the color in `{ Solid: ... }` so the payload matches the Rust `FillChoice` shape the `Preset` variant expects
preset: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { Preset: e.detail === "None" ? "None" : { Solid: e.detail } }, true),
eyedropperColorCode: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { EyedropperColorCode: e.detail }, true),
},

View File

@@ -2,13 +2,13 @@
import { createEventDispatcher } from "svelte";
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
import { contrastingOutlineFactor, fillChoiceUIColor, fillChoiceUIGradient } from "/src/utility-functions/colors";
import type { FillChoiceUI, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradient } from "/src/utility-functions/colors";
import type { FillChoice, MenuDirection, ActionShortcut, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
const dispatch = createEventDispatcher<{ value: FillChoiceUI; startHistoryTransaction: undefined }>();
const dispatch = createEventDispatcher<{ value: FillChoice<SRGBA8>; startHistoryTransaction: undefined }>();
// Content
export let value: FillChoiceUI;
export let value: FillChoice<SRGBA8>;
export let chosenGradient: string | undefined = undefined;
export let allowNone = false;
// export let allowTransparency = false; // TODO: Implement
@@ -29,8 +29,8 @@
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
$: outlined = outlineFactor > 0.0001;
$: gradient = fillChoiceUIGradient(value);
$: solidColor = fillChoiceUIColor(value);
$: gradient = fillChoiceGradient(value);
$: solidColor = fillChoiceColor(value);
$: none = value === "None";
$: transparency = gradient ? gradient.color.some((color) => color.alpha < 255) : solidColor ? solidColor.alpha < 255 : false;
</script>

View File

@@ -3,7 +3,7 @@
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
import { fillChoiceUIColor, sRgba8ToRgbaCSS } from "/src/utility-functions/colors";
import { fillChoiceColor, sRgba8ToRgbaCSS } from "/src/utility-functions/colors";
import type { SRGBA8, EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
const editor = getContext<EditorWrapper>("editor");
@@ -42,7 +42,7 @@
on:open={({ detail }) => (primaryOpen = detail)}
colorOrGradient={{ Solid: primary }}
on:colorOrGradient={({ detail }) => {
const color = fillChoiceUIColor(detail);
const color = fillChoiceColor(detail);
if (color) primaryColorChanged(color);
}}
direction="Right"
@@ -55,7 +55,7 @@
on:open={({ detail }) => (secondaryOpen = detail)}
colorOrGradient={{ Solid: secondary }}
on:colorOrGradient={({ detail }) => {
const color = fillChoiceUIColor(detail);
const color = fillChoiceColor(detail);
if (color) secondaryColorChanged(color);
}}
direction="Right"

View File

@@ -2,10 +2,10 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "/src/utility-functions/widgets";
import type { FillChoiceUI, Layout } from "/wrapper/pkg/graphite_wasm_wrapper";
import type { FillChoice, Layout, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
export type ColorPickerCallbacks = {
onColorChanged?: (value: FillChoiceUI) => void;
onColorChanged?: (value: FillChoice<SRGBA8>) => void;
onStartTransaction?: () => void;
onCommitTransaction?: () => void;
};

View File

@@ -1,4 +1,4 @@
import type { FillChoiceUI, GradientStops, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
import type { FillChoice, GradientRamp, GradientStops, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
export type HSV = { h: number; s: number; v: number };
@@ -150,7 +150,7 @@ export function sRgba8ContrastingColor(color: SRGBA8 | undefined): "black" | "wh
return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
}
export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: string | [string, string], proximityRange: number): number {
export function contrastingOutlineFactor(value: FillChoice<SRGBA8>, proximityColor: string | [string, string], proximityRange: number): number {
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
const [range1, range2] = pair.map((color) => sRgba8FromCSS(window.getComputedStyle(document.body).getPropertyValue(color)));
@@ -167,7 +167,7 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st
return (1 - Math.min(distance / proximityRange, 1)) * (1 - sRgba8ToHSV(color).s);
};
const gradient = fillChoiceUIGradient(value);
const gradient = fillChoiceGradient(value);
if (gradient) {
if (gradient.color.length === 0) return 0;
@@ -177,7 +177,7 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st
return Math.min(first, last);
}
return contrast(fillChoiceUIColor(value));
return contrast(fillChoiceColor(value));
}
// GRADIENT UTILITY FUNCTIONS
@@ -186,21 +186,25 @@ export function isGradientStops(value: unknown): value is GradientStops<SRGBA8>
return typeof value === "object" && value !== null && "color" in value && Array.isArray(value.color);
}
export function isGradientRamp(value: unknown): value is GradientRamp<SRGBA8> {
return typeof value === "object" && value !== null && "stops" in value && isGradientStops(value.stops);
}
// FILL CHOICE UTILITY FUNCTIONS
export function fillChoiceUIColor(value: FillChoiceUI): SRGBA8 | undefined {
export function fillChoiceColor(value: FillChoice<SRGBA8>): SRGBA8 | undefined {
if (typeof value === "object" && "Solid" in value) return value.Solid;
return undefined;
}
export function fillChoiceUIGradient(value: FillChoiceUI): GradientStops<SRGBA8> | undefined {
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
export function fillChoiceGradient(value: FillChoice<SRGBA8>): GradientStops<SRGBA8> | undefined {
if (typeof value === "object" && "Gradient" in value) return value.Gradient.stops;
return undefined;
}
export function parseFillChoiceUI(value: unknown): FillChoiceUI {
export function parseFillChoice(value: unknown): FillChoice<SRGBA8> {
if (value === "None" || value === undefined || value === null) return "None";
if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid };
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient };
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientRamp(value.Gradient)) return { Gradient: value.Gradient };
return "None";
}

View File

@@ -390,9 +390,9 @@ mod editor_commands {
}
/// Initialize the Rust color picker handler with a starting value (used when the frontend `<ColorPicker />` opens).
fn open_color_picker(initial_value: FillChoiceUI, allow_none: bool, disabled: bool) -> Message {
fn open_color_picker(initial_value: FillChoiceSRGBA8, allow_none: bool, disabled: bool) -> Message {
ColorPickerMessage::Open {
initial_value: FillChoice::from(&initial_value),
initial_value: FillChoice::from(&initial_value.0),
allow_none,
disabled,
}
@@ -659,6 +659,17 @@ mod editor_commands {
}
}
#[cfg(feature = "editor")]
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct FillChoiceSRGBA8(
/// Concrete wasm boundary form of the generic [`FillChoice`], since a `#[wasm_bindgen]` argument's TS declaration names its type without the generic's argument.
#[tsify(type = "FillChoice<SRGBA8>")]
pub graphene_std::vector::style::FillChoice<graphene_std::color::SRGBA8>,
);
#[cfg(not(feature = "editor"))]
pub type FillChoiceSRGBA8 = Any;
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct Any(#[tsify(type = "any")] serde_json::Value);
@@ -685,5 +696,4 @@ editor_proxy_types! {
DockingSplitDirection = editor::messages::portfolio::utility_types::DockingSplitDirection;
PanelTypes = Vec<editor::messages::portfolio::utility_types::PanelType>;
SRGBA8 = graphene_std::color::SRGBA8;
FillChoiceUI = graphene_std::vector::style::FillChoiceUI;
}