Add a gradient interpolation space attribute with sRGB Gamma (existing) and sRGB Linear (new) (#4412)

* Build dropdown menu entries from choice type metadata

* Add a gradient interpolation color space attribute, blending stops in linear light by default

* Add a Space interpolation dropdown to the color picker popover

* Stamp legacy gradient ramps with their implicit gamma interpolation during deserialization

* Resolve color-interpolation from SVG style blocks and fix cascade order among repeated declarations
This commit is contained in:
Keavon Chambers
2026-08-05 15:58:25 -07:00
committed by Dennis Kobert
parent ea6f45ddc0
commit f8975b7d94
25 changed files with 817 additions and 120 deletions

View File

@@ -44,6 +44,7 @@ tsify = { workspace = true }
dyn-any = { workspace = true }
num_enum = { workspace = true }
usvg = { workspace = true }
simplecss = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true }
vello = { workspace = true }

View File

@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
use crate::messages::prelude::*;
use graphene_std::vector::style::{FillChoice, GradientSpread};
use graphene_std::vector::style::{FillChoice, GradientInterpolation, GradientSpread};
/// Identifies which RGB channel a numeric input change targets.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -49,6 +49,8 @@ pub enum ColorPickerMessage {
GradientUpdate { update: SpectrumInputUpdate },
/// Gradient spread choice from the gradient "Ends" selection.
SetGradientSpread { gradient_spread: GradientSpread },
/// Gradient interpolation choice: the color space the stops blend in, from the "Space" dropdown.
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
StartTransaction,

View File

@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpread, GradientStops};
use graphene_std::vector::style::{FillChoice, Gradient, GradientInterpolation, GradientRamp, GradientSpread, GradientStops};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01;
@@ -30,6 +30,7 @@ pub struct ColorPickerMessageHandler {
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
gradient: Option<Gradient>,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
active_marker_index: Option<u32>,
active_marker_is_midpoint: bool,
@@ -52,6 +53,7 @@ impl Default for ColorPickerMessageHandler {
old_is_none: true,
gradient: None,
gradient_spread: GradientSpread::default(),
gradient_interpolation: GradientInterpolation::default(),
active_marker_index: None,
active_marker_is_midpoint: false,
allow_none: true,
@@ -73,12 +75,14 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.set_new_hsva(0., 0., 0., 1., true);
self.gradient = None;
self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
}
FillChoice::Solid(color) => {
self.gradient = None;
self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
self.adopt_color(color);
@@ -87,6 +91,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.active_marker_index = Some(0);
self.active_marker_is_midpoint = false;
self.gradient_spread = ramp.gradient_spread;
self.gradient_interpolation = ramp.gradient_interpolation;
let gradient = Gradient::from(ramp);
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
self.gradient = Some(gradient);
@@ -199,6 +204,20 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread,
gradient_interpolation: self.gradient_interpolation,
..GradientRamp::from(gradient)
}),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
let Some(gradient) = &self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_interpolation = gradient_interpolation;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation,
..GradientRamp::from(gradient)
}),
});
@@ -290,6 +309,7 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation,
..GradientRamp::from(&*gradient)
}),
});
@@ -420,6 +440,7 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation,
..GradientRamp::from(&gradient)
}),
});
@@ -450,6 +471,7 @@ impl ColorPickerMessageHandler {
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
let mut row_widgets = vec![
SpectrumInput::new(GradientStops::from(gradient))
.track_interpolation(self.gradient_interpolation)
.markers(markers)
.active_marker_index(self.active_marker_index)
.active_marker_is_midpoint(self.active_marker_is_midpoint)
@@ -632,6 +654,20 @@ impl ColorPickerMessageHandler {
]));
}
// Gradient interpolation color space (only present when the picker is in gradient mode)
if self.gradient.is_some() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Space").tooltip_label("Gradient Interpolation").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries)
.selected_index(Some(self.gradient_interpolation as u32))
.disabled(self.disabled)
.widget_instance(),
]));
}
// Color presets (None / Black / White / pure colors / eyedropper)
groups.push(LayoutGroup::row(vec![
ColorPresetsInput::default()
@@ -686,6 +722,7 @@ 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%).";
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends.";
const SPACE_DESCRIPTION: &str = "The color space where stops blend into their neighbors.";
/// 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.

View File

@@ -531,7 +531,7 @@ fn populate_computed_display_fields(layout: &mut Layout) {
color_input.chosen_gradient = color_input.value.to_css_background_image();
}
Widget::SpectrumInput(spectrum_input) => {
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_interpolation);
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());
}

View File

@@ -7,7 +7,7 @@ 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::{FillChoice, GradientInterpolation, GradientStops};
use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -151,6 +151,35 @@ impl std::hash::Hash for MenuListEntry {
}
}
impl MenuListEntry {
/// One entry per variant of a choice type enum, keeping the choice type's section groupings, shown as the variant's label alongside its icon when it has one.
pub fn sections_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> MenuListEntrySections
where
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
{
E::list()
.iter()
.map(|section| {
section
.iter()
.map(|(variant, metadata)| {
let to_message = to_message.clone();
let variant = *variant;
let entry = MenuListEntry::new(metadata.name)
.label(metadata.label)
.tooltip_label(metadata.label)
.tooltip_description(metadata.description.unwrap_or_default())
.on_update(move |_| to_message(variant));
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry }
})
.collect()
})
.collect()
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)]
@@ -557,6 +586,9 @@ pub struct SpectrumInput {
/// The colored gradient drawn behind the markers (display-only, caller-owned).
#[widget_builder(constructor)]
pub track: GradientStops<SRGBA8>,
/// The interpolation color space the track's stops blend in, used to compute `track_css`. Not sent to the frontend.
#[serde(skip)]
pub track_interpolation: GradientInterpolation,
/// 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)]

View File

@@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
use graphene_std::{Artboard, Color, Graphic};
use std::any::Any;
@@ -218,6 +218,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<BlendMode>,
List<GradientForm>,
List<GradientSpread>,
List<GradientInterpolation>,
List<DashPattern>,
List<BoxCorners>,
List<StrokeJoin>,
@@ -270,6 +271,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
DAffine2,
BlendMode,
GradientForm,
GradientInterpolation,
GradientSpread,
DashPattern,
BoxCorners,
@@ -1009,6 +1011,7 @@ impl_table_item_layout_for_choice_enum!(
BlendMode,
GradientForm,
GradientSpread,
GradientInterpolation,
StrokeJoin,
StrokeAlign,
StrokeCap,
@@ -1221,6 +1224,7 @@ macro_rules! known_item_types {
BlendMode,
GradientForm,
GradientSpread,
GradientInterpolation,
StrokeJoin,
StrokeAlign,
StrokeCap,

View File

@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientSpread, Stroke};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -30,6 +30,7 @@ pub enum GraphOperationMessage {
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
transform: DAffine2,
},
BlendingFillSet {
@@ -61,6 +62,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
gradient_spread: GradientSpread,
},
GradientInterpolationSet {
layer: LayerNodeIdentifier,
gradient_interpolation: GradientInterpolation,
},
OpacitySet {
layer: LayerNodeIdentifier,
opacity: f64,

View File

@@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::list::List;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Gradient, GradientForm, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Gradient, GradientForm, GradientInterpolation, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color};
#[derive(ExtractField)]
@@ -50,10 +50,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
gradient,
gradient_form,
gradient_spread,
gradient_interpolation,
transform,
} => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, transform);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
}
}
GraphOperationMessage::BlendingFillSet { layer, fill } => {
@@ -91,6 +92,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.gradient_spread_set(gradient_spread);
}
}
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_interpolation_set(gradient_interpolation);
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.opacity_set(opacity);
@@ -481,18 +487,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
};
placement_transform.translation = placement_transform.translation.round();
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
let gradient_info = SvgGradientInfo {
graphite_stops: extract_graphite_gradient_stops(&svg),
interpolations: extract_gradient_interpolations(&svg),
};
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
// The placement offset is then applied once to the root group layer below.
import_usvg_node(
&mut modify_inputs,
&usvg::Node::Group(Box::new(tree.root().clone())),
id,
parent,
insert_index,
&graphite_gradient_stops,
);
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), id, parent, insert_index, &gradient_info);
// After import, `layer_node` is set to the root group. Apply the placement transform to it
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
@@ -524,6 +526,131 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops.
struct SvgGradientInfo {
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
graphite_stops: HashMap<String, Gradient>,
/// Interpolation spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
interpolations: HashMap<String, GradientInterpolation>,
}
/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's
/// tree does not carry. Only `linearRGB` selects linear interpolation; `auto` and `sRGB` (browsers treat the
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpolation> {
let mut result = HashMap::new();
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
if !svg.contains("color-interpolation") {
return result;
}
let doc = match usvg::roxmltree::Document::parse(svg) {
Ok(doc) => doc,
Err(_) => return result,
};
// The document's `<style>` blocks apply to every element, so parse them once up front
let mut stylesheet = simplecss::StyleSheet::new();
for style_element in doc.descendants().filter(|node| node.tag_name().name() == "style") {
if !matches!(style_element.attribute("type"), None | Some("") | Some("text/css")) {
continue;
}
for text in style_element.children().filter(|child| child.is_text()).filter_map(|child| child.text()) {
stylesheet.parse_more(text);
}
}
for node in doc.descendants() {
match node.tag_name().name() {
"linearGradient" | "radialGradient" => {}
_ => continue,
}
if let Some(gradient_id) = node.attribute("id")
&& let Some(gradient_interpolation) = resolve_color_interpolation(node, &stylesheet)
{
result.insert(gradient_id.to_string(), gradient_interpolation);
}
}
result
}
/// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each
/// element's own winning declaration per [`declared_color_interpolation`]'s cascade order.
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientInterpolation> {
let mut next = Some(element);
while let Some(element) = next {
match declared_color_interpolation(element, stylesheet) {
Some("linearRGB") => return Some(GradientInterpolation::SrgbLinear),
// `inherit` defers to the ancestors like an undeclared element
Some("inherit") | None => {}
Some(_) => return Some(GradientInterpolation::SrgbGamma),
}
next = element.parent_element();
}
None
}
/// The winning `color-interpolation` declaration on a single element per the CSS cascade: `!important` declarations
/// beat normal ones, the inline `style` beats the `<style>` rules (already specificity-sorted, so their last match
/// wins), and the presentation attribute yields to them all. Later declarations win priority ties.
fn declared_color_interpolation<'a>(element: usvg::roxmltree::Node<'a, '_>, stylesheet: &simplecss::StyleSheet<'a>) -> Option<&'a str> {
let mut winner: Option<(u8, &'a str)> = None;
let mut consider = |priority: u8, value: &'a str| {
if winner.is_none_or(|(existing, _)| priority >= existing) {
winner = Some((priority, value));
}
};
if let Some(value) = element.attribute("color-interpolation") {
consider(0, value.trim());
}
for rule in stylesheet.rules.iter().filter(|rule| rule.selector.matches(&CssElement(element))) {
for declaration in rule.declarations.iter().filter(|declaration| declaration.name == "color-interpolation") {
consider(if declaration.important { 3 } else { 1 }, declaration.value);
}
}
if let Some(style) = element.attribute("style") {
for declaration in simplecss::DeclarationTokenizer::from(style).filter(|declaration| declaration.name == "color-interpolation") {
consider(if declaration.important { 4 } else { 2 }, declaration.value);
}
}
winner.map(|(_, value)| value)
}
/// Adapts a roxmltree element to simplecss's selector-matching interface.
struct CssElement<'a, 'input>(usvg::roxmltree::Node<'a, 'input>);
impl simplecss::Element for CssElement<'_, '_> {
fn parent_element(&self) -> Option<Self> {
self.0.parent_element().map(CssElement)
}
fn prev_sibling_element(&self) -> Option<Self> {
self.0.prev_sibling_element().map(CssElement)
}
fn has_local_name(&self, local_name: &str) -> bool {
self.0.tag_name().name() == local_name
}
fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
self.0.attribute(local_name).is_some_and(|value| operator.matches(value))
}
fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
matches!(class, simplecss::PseudoClass::FirstChild) && self.0.prev_sibling_element().is_none()
}
}
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
@@ -598,7 +725,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
/// O(n) import path that skips collision detection and instead calculates positions directly from
/// the known tree structure.
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, graphite_gradient_stops: &HashMap<String, Gradient>) {
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, gradient_info: &SvgGradientInfo) {
let layer = modify_inputs.create_layer(id);
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
@@ -618,7 +745,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
modify_inputs.import = true;
for child in group.children() {
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, &mut group_extents_map);
child_extents_svg_order.push(extent);
}
@@ -637,7 +764,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
}
usvg::Node::Path(path) => {
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
}
usvg::Node::Image(_image) => {
warn!("Skip image");
@@ -661,7 +788,7 @@ fn import_usvg_node_inner(
id: NodeId,
parent: LayerNodeIdentifier,
insert_index: usize,
graphite_gradient_stops: &HashMap<String, Gradient>,
gradient_info: &SvgGradientInfo,
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
) -> u32 {
let layer = modify_inputs.create_layer(id);
@@ -672,7 +799,7 @@ fn import_usvg_node_inner(
usvg::Node::Group(group) => {
let mut child_extents: Vec<u32> = Vec::new();
for child in group.children() {
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, group_extents_map);
child_extents.push(extent);
}
modify_inputs.layer_node = Some(layer);
@@ -687,7 +814,7 @@ fn import_usvg_node_inner(
total_extent
}
usvg::Node::Path(path) => {
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
0
}
usvg::Node::Image(_image) => {
@@ -704,7 +831,7 @@ fn import_usvg_node_inner(
}
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, Gradient>) {
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
let subpaths = convert_usvg_path(path);
// Skip creating a Transform node entirely when the SVG-native transform is identity.
@@ -718,7 +845,7 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
}
if let Some(fill) = path.fill() {
apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops);
apply_usvg_fill(fill, modify_inputs, gradient_info);
}
if let Some(stroke) = path.stroke() {
apply_usvg_stroke(stroke, modify_inputs, node_transform);
@@ -819,7 +946,7 @@ fn convert_gradient_spread(spread_method: usvg::SpreadMethod) -> GradientSpread
}
}
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, Gradient>) {
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, gradient_info: &SvgGradientInfo) {
match &fill.paint() {
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
usvg::Paint::LinearGradient(linear) => {
@@ -831,7 +958,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
let gradient_form = GradientForm::Linear;
let gradient = match graphite_gradient_stops.get(linear.id()) {
let gradient = match gradient_info.graphite_stops.get(linear.id()) {
Some(graphite_stops) => graphite_stops.clone(),
None => {
let stops = linear.stops().iter().map(|stop| GradientStop {
@@ -843,7 +970,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}
};
let gradient_spread = convert_gradient_spread(linear.spread_method());
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, transform);
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
let gradient_interpolation = gradient_info.interpolations.get(linear.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
}
usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform());
@@ -855,7 +984,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
let gradient_form = GradientForm::Radial;
let gradient = match graphite_gradient_stops.get(radial.id()) {
let gradient = match gradient_info.graphite_stops.get(radial.id()) {
Some(graphite_stops) => graphite_stops.clone(),
None => {
let stops = radial.stops().iter().map(|stop| GradientStop {
@@ -867,9 +996,127 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}
};
let gradient_spread = convert_gradient_spread(radial.spread_method());
let gradient_interpolation = gradient_info.interpolations.get(radial.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, transform);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
}
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
<defs>
<linearGradient id="inherited"/>
<linearGradient id="attribute" color-interpolation="sRGB"/>
<linearGradient id="styled" color-interpolation="sRGB" style="fill: red; color-interpolation: linearRGB"/>
<radialGradient id="auto" color-interpolation="auto"/>
</defs>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
assert_eq!(
interpolations.get("inherited"),
Some(&GradientInterpolation::SrgbLinear),
"an undeclared gradient should inherit from its ancestors"
);
assert_eq!(
interpolations.get("attribute"),
Some(&GradientInterpolation::SrgbGamma),
"an sRGB declaration should beat the inherited linearRGB"
);
assert_eq!(
interpolations.get("styled"),
Some(&GradientInterpolation::SrgbLinear),
"the inline style should beat the presentation attribute"
);
assert_eq!(
interpolations.get("auto"),
Some(&GradientInterpolation::SrgbGamma),
"auto should mean gamma like browsers treat it, not defer to ancestors"
);
}
#[test]
fn color_interpolation_reads_style_blocks_with_selector_specificity() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
<style>
linearGradient { color-interpolation: linearRGB }
.classy { color-interpolation: sRGB }
#exact { color-interpolation: linearRGB }
</style>
<defs>
<linearGradient id="from-type-rule"/>
<linearGradient id="from-class-rule" class="classy"/>
<linearGradient id="exact" class="classy"/>
<linearGradient id="inline-beats-rules" class="classy" style="color-interpolation: linearRGB"/>
<linearGradient id="rule-beats-attribute" class="classy" color-interpolation="linearRGB"/>
</defs>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
assert_eq!(
interpolations.get("from-type-rule"),
Some(&GradientInterpolation::SrgbLinear),
"a type rule in a style block should reach the gradient"
);
assert_eq!(
interpolations.get("from-class-rule"),
Some(&GradientInterpolation::SrgbGamma),
"the class rule should outrank the type rule by specificity"
);
assert_eq!(interpolations.get("exact"), Some(&GradientInterpolation::SrgbLinear), "the ID rule should outrank the class rule");
assert_eq!(
interpolations.get("inline-beats-rules"),
Some(&GradientInterpolation::SrgbLinear),
"the inline style should beat every style block rule"
);
assert_eq!(
interpolations.get("rule-beats-attribute"),
Some(&GradientInterpolation::SrgbGamma),
"a style block rule should beat the presentation attribute"
);
}
#[test]
fn repeated_and_important_declarations_resolve_by_cascade_order() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
<style>.forced { color-interpolation: linearRGB !important }</style>
<linearGradient id="last-declaration-wins" style="color-interpolation: sRGB; color-interpolation: linearRGB"/>
<linearGradient id="important-beats-later" style="color-interpolation: linearRGB !important; color-interpolation: sRGB"/>
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
assert_eq!(
interpolations.get("last-declaration-wins"),
Some(&GradientInterpolation::SrgbLinear),
"the last of repeated inline declarations should win"
);
assert_eq!(
interpolations.get("important-beats-later"),
Some(&GradientInterpolation::SrgbLinear),
"an `!important` declaration should beat a later normal one"
);
assert_eq!(
interpolations.get("important-rule-beats-inline"),
Some(&GradientInterpolation::SrgbLinear),
"an `!important` style block rule should beat the inline style"
);
}
#[test]
fn color_interpolation_yields_nothing_when_never_declared() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
assert!(
extract_gradient_interpolations(svg).is_empty(),
"gradients without any declaration should fall back to the caller's gamma default"
);
}
}

View File

@@ -19,7 +19,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientSpread, Stroke};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic};
@@ -433,14 +433,18 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
}
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, gradient_spread: GradientSpread, transform: DAffine2) {
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation, transform: DAffine2) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
let ramp = GradientRamp::from(gradient);
let ramp = GradientRamp { gradient_spread, ..ramp };
let ramp = GradientRamp {
gradient_spread,
gradient_interpolation,
..ramp
};
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
// Skip the rerender on all but the last input so the whole update triggers a single graph run
@@ -605,10 +609,9 @@ impl<'a> ModifyInputsContext<'a> {
};
// Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them
let gradient_spread = self.gradient_value_ramp(gradient_value_id).unwrap_or_default().gradient_spread;
let ramp = GradientRamp {
gradient_spread,
..GradientRamp::from(stops)
stops: (&stops).into(),
..self.gradient_value_ramp(gradient_value_id).unwrap_or_default()
};
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
@@ -766,6 +769,20 @@ impl<'a> ModifyInputsContext<'a> {
Some(ramp.clone())
}
/// Set the interpolation on the chain's gradient value, which is where the ramp carries it. Never touches a
/// 'Gradient Interpolation' node: that one is a user-authored procedural override, not something the tools manage.
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
let Some(output_layer) = self.get_output_layer() else { return };
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
return;
};
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
let ramp = GradientRamp { gradient_interpolation, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
}
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.unwrap_or(false);
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {

View File

@@ -34,7 +34,7 @@ use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{
FillChoice, Gradient, GradientForm, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef};
@@ -294,6 +294,7 @@ pub(crate) fn property_from_type(
// =========================
Some(x) if id_is::<GradientForm>(x) => enum_choice::<GradientForm>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientInterpolation>(x) => enum_choice::<GradientInterpolation>().for_socket(default_info).property_row(),
Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
@@ -1389,6 +1390,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(GradientStops::from(&bw_track()))
.track_interpolation(GradientInterpolation::SrgbGamma)
.markers(spectrum_markers)
.show_midpoints(false)
.allow_insert(false)
@@ -1561,6 +1563,7 @@ fn spectrum_slider_row(
let position_to_value = move |position: f64| value_min + position * value_range;
row.push(
SpectrumInput::new(GradientStops::from(&track))
.track_interpolation(GradientInterpolation::SrgbGamma)
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
.show_midpoints(false)
.allow_insert(false)
@@ -2907,24 +2910,22 @@ pub mod choice {
U: Fn(&E) -> Message + 'static + Send + Sync,
C: Fn(&()) -> Message + 'static + Send + Sync,
{
let items = E::list()
.iter()
let updater = std::sync::Arc::new(updater_factory());
let committer = std::sync::Arc::new(committer_factory());
let items = MenuListEntry::sections_from_choice_type(move |variant: E| updater(&variant))
.into_iter()
.map(|section| {
section
.iter()
.map(|(item, metadata)| {
let updater = updater_factory();
let committer = committer_factory();
MenuListEntry::new(metadata.name)
.label(metadata.label)
.tooltip_label(metadata.label)
.tooltip_description(metadata.description.unwrap_or_default())
.on_update(move |_| updater(item))
.on_commit(committer)
.into_iter()
.map(|entry| {
let committer = committer.clone();
entry.on_commit(move |value| committer(value))
})
.collect()
})
.collect();
DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_instance()
}

View File

@@ -807,6 +807,11 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
};
assert_eq!(
ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
);
let stops = graphene_std::vector::Gradient::from(ramp);
assert_eq!(stops.len(), 2);
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
@@ -844,12 +849,22 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
};
assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp");
assert_eq!(
ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
);
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
};
assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp");
assert_eq!(
backup_ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
"the backup ramp should carry the explicit gamma interpolation too"
);
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
assert!(

View File

@@ -14,7 +14,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{Gradient, GradientForm, GradientSpread, PointId, SegmentId, VectorModificationType};
use graphene_std::vector::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PointId, SegmentId, VectorModificationType};
use graphene_std::{NodeParameter, ParameterRef};
use std::collections::VecDeque;
@@ -388,13 +388,23 @@ pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &No
Some(*node_id)
}
/// The spread baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpread> {
/// The ramp held by the 'Gradient Value' node feeding a layer's chain, which carries the whole-ramp settings.
fn get_chain_source_gradient_ramp<'a>(layer: LayerNodeIdentifier, network_interface: &'a NodeNetworkInterface) -> Option<&'a GradientRamp> {
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
return None;
};
Some(ramp.gradient_spread)
Some(ramp)
}
/// The spread baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpread> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread)
}
/// The interpolation baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_interpolation(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientInterpolation> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_interpolation)
}
/// Get the gradient stops of a layer, if any.
@@ -752,6 +762,7 @@ pub struct FillNodeGradient {
pub stops: Gradient,
pub gradient_form: GradientForm,
pub gradient_spread: GradientSpread,
pub gradient_interpolation: GradientInterpolation,
pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
pub transform_is_value: bool,
@@ -765,6 +776,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
return None;
};
let gradient_spread = ramp.gradient_spread;
let gradient_interpolation = ramp.gradient_interpolation;
let stops = Gradient::from(ramp);
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientForm(value)) => value,
@@ -782,6 +794,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
stops,
gradient_form,
gradient_spread,
gradient_interpolation,
transform,
transform_is_value: transform_input.is_some(),
})
@@ -931,6 +944,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
gradient: Gradient::from(ramp),
gradient_form,
gradient_spread: ramp.gradient_spread,
gradient_interpolation: ramp.gradient_interpolation,
transform,
});
}

View File

@@ -9,15 +9,15 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{
self, NodeGraphLayer, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input,
replaceable_paint_chain, reverse_direction_tooltip_description,
self, NodeGraphLayer, get_chain_source_gradient_interpolation, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id,
gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use glam::DMat2;
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation};
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation};
#[derive(Default, ExtractField)]
pub struct GradientTool {
@@ -30,6 +30,7 @@ pub struct GradientTool {
pub struct GradientOptions {
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
}
#[impl_message(Message, ToolMessage, Gradient)]
@@ -138,7 +139,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
let ramp = GradientRamp::from(&ramp);
self.options.gradient_spread = ramp.gradient_spread;
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread);
self.options.gradient_interpolation = ramp.gradient_interpolation;
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread, ramp.gradient_interpolation);
}
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open {
@@ -176,6 +178,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
self.options.gradient_spread = appearance.gradient_spread;
needs_refresh = true;
}
if self.options.gradient_interpolation != appearance.gradient_interpolation {
self.options.gradient_interpolation = appearance.gradient_interpolation;
needs_refresh = true;
}
}
let has_gradient = current_gradient.is_some();
@@ -256,6 +262,7 @@ impl LayoutHolder for GradientTool {
});
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
gradient_spread: self.options.gradient_spread,
gradient_interpolation: self.options.gradient_interpolation,
..GradientRamp::from(&stops_value)
}))
.allow_none(false)
@@ -356,6 +363,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
GradientAppearance {
gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread,
gradient_interpolation: gradient.gradient_interpolation,
transform: gradient.transform,
},
GradientSource::Direct,
@@ -375,6 +383,7 @@ struct GradientAppearance {
transform: DAffine2,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
}
/// Resolve the gradient transform, form, and spread by walking the chain feeding the layer.
@@ -415,6 +424,7 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
transform: composed_transform,
gradient_form: gradient_form.unwrap_or_default(),
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
gradient_interpolation: get_chain_source_gradient_interpolation(layer, network_interface).unwrap_or_default(),
}
}
@@ -751,6 +761,7 @@ impl SelectedGradient {
gradient: self.gradient.clone(),
gradient_form: self.appearance.gradient_form,
gradient_spread: self.appearance.gradient_spread,
gradient_interpolation: self.appearance.gradient_interpolation,
transform: self.appearance.transform,
});
}
@@ -789,6 +800,10 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
layer,
gradient_spread: appearance.gradient_spread,
});
responses.add(GraphOperationMessage::GradientInterpolationSet {
layer,
gradient_interpolation: appearance.gradient_interpolation,
});
}
impl GradientTool {
@@ -1468,6 +1483,7 @@ impl Fsm for GradientToolFsmState {
transform: DAffine2::IDENTITY,
gradient_form: tool_options.gradient_form,
gradient_spread: tool_options.gradient_spread,
gradient_interpolation: tool_options.gradient_interpolation,
},
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
if replaceable_paint_chain(layer, &document.network_interface).is_some() {
@@ -1826,6 +1842,7 @@ fn apply_gradient_update(
gradient,
gradient_form: appearance.gradient_form,
gradient_spread: appearance.gradient_spread,
gradient_interpolation: appearance.gradient_interpolation,
transform: appearance.transform,
});
}
@@ -1849,7 +1866,14 @@ fn apply_gradient_update(
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
/// single undo entry by the surrounding 'on_commit' callback.
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient, gradient_spread: GradientSpread) {
fn apply_stops_update(
data: &mut GradientToolData,
context: &mut ToolActionMessageContext,
responses: &mut VecDeque<Message>,
new_gradient: Gradient,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
) {
let selected_layers: Vec<_> = context
.document
.network_interface
@@ -1866,6 +1890,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread });
responses.add(GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation });
updated_any_layer = true;
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
responses.add(GraphOperationMessage::FillGradientSet {
@@ -1873,6 +1898,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
gradient: new_gradient.clone(),
gradient_form: appearance.gradient_form,
gradient_spread,
gradient_interpolation,
transform: appearance.transform,
});
updated_any_layer = true;
@@ -1882,6 +1908,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.gradient_spread = gradient_spread;
selected_gradient.appearance.gradient_interpolation = gradient_interpolation;
}
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.