Add support for setting the spread method for gradient fills (#3953)

* Add spread method support for gradients

* Add GradientSpreadMethod enum (Pad, Repeat, Reflect) to vector-types

* Add radio buttons to gradient tool and fill properties panel

* Convert spread method when importing SVGs via usvg

* Sync backup gradient input when changing spread method

* Table<GradientStops> rendering is not yet updated for spread method

* Sync gradient tool options with layer's gradient

* Sync gradient_type and spread_method from the selected
  layer's existing gradient to the tool options bar when
  switching to the gradient tool

* Refactor has_gradient_on_selected_layers
  to reuse a new get_gradient_on_selected_layer helper

* Swap Reflect and Repeat order in UI radio buttons

* Fix alignment of the radio buttons in right panel

* Fix the position of the radio buttons in the tool

* Rename SpreadMethod to SetSpreadMethod

* Move default spread method omission logic
This commit is contained in:
YohYamasaki
2026-04-14 08:09:57 +00:00
committed by GitHub
parent da45ab2f87
commit 79d778a535
6 changed files with 232 additions and 14 deletions
@@ -15,7 +15,7 @@ use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
#[derive(ExtractField)]
pub struct GraphOperationMessageContext<'a> {
@@ -765,6 +765,14 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
}
}
fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMethod {
match spread_method {
usvg::SpreadMethod::Pad => GradientSpreadMethod::Pad,
usvg::SpreadMethod::Reflect => GradientSpreadMethod::Reflect,
usvg::SpreadMethod::Repeat => GradientSpreadMethod::Repeat,
}
}
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
modify_inputs.fill_set(match &fill.paint() {
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
@@ -787,8 +795,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
GradientStops::new(stops)
}
};
let spread_method = convert_spread_method(linear.spread_method());
Fill::Gradient(Gradient { start, end, gradient_type, stops })
Fill::Gradient(Gradient {
start,
end,
gradient_type,
stops,
spread_method,
})
}
usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform());
@@ -810,8 +825,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
GradientStops::new(stops)
}
};
let spread_method = convert_spread_method(radial.spread_method());
Fill::Gradient(Gradient { start, end, gradient_type, stops })
Fill::Gradient(Gradient {
start,
end,
gradient_type,
stops,
spread_method,
})
}
usvg::Paint::Pattern(_) => {
warn!("SVG patterns are not currently supported");
@@ -27,7 +27,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::QRCodeErrorCorrectionLevel;
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, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_instance();
@@ -2006,6 +2006,55 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
]);
widgets.push(LayoutGroup::row(row));
let mut spread_methods_row: Vec<WidgetInstance> = vec![TextLabel::new("").widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance()];
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
.iter()
.map(|&spread_method| {
let gradient_for_input = gradient_for_closure.clone();
let gradient_for_backup = gradient_for_closure.clone();
let set_input_value = update_value(
move |_: &()| {
let mut new_gradient = gradient_for_input.clone();
new_gradient.spread_method = spread_method;
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
FillInput::<Color>::INDEX,
);
let set_backup_value = update_value(
move |_: &()| {
let mut new_gradient = gradient_for_backup.clone();
new_gradient.spread_method = spread_method;
TaggedValue::Gradient(new_gradient)
},
node_id,
BackupGradientInput::INDEX,
);
RadioEntryData::new(format!("{:?}", spread_method))
.label(format!("{:?}", spread_method))
.on_update(move |_| Message::Batched {
messages: Box::new([
set_input_value(&()),
set_backup_value(&()),
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(spread_method),
}
.into(),
]),
})
.on_commit(commit_value)
})
.collect();
add_blank_assist(&mut spread_methods_row);
spread_methods_row.extend_from_slice(&[RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance()]);
widgets.push(LayoutGroup::row(spread_methods_row));
}
widgets