Skip to content

Commit 871468d

Browse files
committed
feat: implemented stroke gradient feature
1 parent b1933e3 commit 871468d

File tree

10 files changed

+431
-72
lines changed

10 files changed

+431
-72
lines changed

editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ impl TableRowLayout for Vector {
389389
}
390390

391391
if let Some(stroke) = self.style.stroke.clone() {
392-
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
392+
let color = if let Some(color) = stroke.color() { FillChoice::Solid(color) } else { FillChoice::None };
393393
table_rows.push(vec![
394394
TextLabel::new("Stroke").narrow(true).widget_instance(),
395395
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ fn import_usvg_node(
496496
fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
497497
if let usvg::Paint::Color(color) = &stroke.paint() {
498498
modify_inputs.stroke_set(Stroke {
499-
color: Some(usvg_color(*color, stroke.opacity().get())),
499+
paint: Fill::Solid(usvg_color(*color, stroke.opacity().get())),
500500
weight: stroke.width().get() as f64,
501501
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
502502
dash_offset: stroke.dashoffset() as f64,

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,23 @@ impl<'a> ModifyInputsContext<'a> {
401401
return;
402402
};
403403

404-
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
404+
match &stroke.paint {
405+
Fill::None => {
406+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupColorInput::INDEX);
407+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
408+
}
409+
Fill::Solid(color) => {
410+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupColorInput::INDEX);
411+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
412+
}
413+
Fill::Gradient(gradient) => {
414+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupGradientInput::INDEX);
415+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
416+
}
417+
}
405418

406-
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
407-
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
419+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Fill>::INDEX);
420+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(stroke.paint.clone()), false), true);
408421
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
409422
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
410423
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 206 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use graphene_std::text::{Font, TextAlign};
2727
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2828
use graphene_std::vector::QRCodeErrorCorrectionLevel;
2929
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
30-
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
30+
use graphene_std::vector::style::{Fill, FillChoice, FillType, Gradient, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
3131

3232
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
3333
let widget = TextLabel::new(text).widget_instance();
@@ -1817,7 +1817,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
18171817
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
18181818
use graphene_std::vector::fill::*;
18191819

1820-
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<Color>::INDEX, true, context));
1820+
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, ColorInput::<Color>::INDEX, true, context));
18211821

18221822
let document_node = match get_document_node(node_id, context) {
18231823
Ok(document_node) => document_node,
@@ -1828,7 +1828,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
18281828
};
18291829

18301830
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
1831-
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
1831+
&document_node.inputs[ColorInput::<Color>::INDEX].as_value(),
18321832
&document_node.inputs[BackupColorInput::INDEX].as_value(),
18331833
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
18341834
) {
@@ -1842,9 +1842,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
18421842

18431843
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
18441844
widgets_first_row.push(
1845-
ColorInput::default()
1845+
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default()
18461846
.value(fill.clone().into())
1847-
.on_update(move |x: &ColorInput| Message::Batched {
1847+
.on_update(move |x: &crate::messages::layout::utility_types::widgets::button_widgets::ColorInput| Message::Batched {
18481848
messages: Box::new([
18491849
match &fill2 {
18501850
Fill::None => NodeGraphMessage::SetInputValue {
@@ -1868,7 +1868,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
18681868
},
18691869
NodeGraphMessage::SetInputValue {
18701870
node_id,
1871-
input_index: FillInput::<Color>::INDEX,
1871+
input_index: ColorInput::<Color>::INDEX,
18721872
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
18731873
}
18741874
.into(),
@@ -1896,7 +1896,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
18961896
}
18971897
},
18981898
node_id,
1899-
FillInput::<Color>::INDEX,
1899+
ColorInput::<Color>::INDEX,
19001900
))
19011901
.widget_instance();
19021902
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
@@ -1907,11 +1907,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
19071907
let entries = vec![
19081908
RadioEntryData::new("solid")
19091909
.label("Solid")
1910-
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, FillInput::<Color>::INDEX))
1910+
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, ColorInput::<Color>::INDEX))
19111911
.on_commit(commit_value),
19121912
RadioEntryData::new("gradient")
19131913
.label("Gradient")
1914-
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, FillInput::<Color>::INDEX))
1914+
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, ColorInput::<Color>::INDEX))
19151915
.on_commit(commit_value),
19161916
];
19171917

@@ -1946,7 +1946,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
19461946
}
19471947
},
19481948
node_id,
1949-
FillInput::<Color>::INDEX,
1949+
ColorInput::<Color>::INDEX,
19501950
))
19511951
.widget_instance();
19521952
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
@@ -1967,7 +1967,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
19671967
TaggedValue::Fill(Fill::Gradient(new_gradient))
19681968
},
19691969
node_id,
1970-
FillInput::<Color>::INDEX,
1970+
ColorInput::<Color>::INDEX,
19711971
);
19721972
RadioEntryData::new(format!("{:?}", grad_type))
19731973
.label(format!("{:?}", grad_type))
@@ -2009,18 +2009,201 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
20092009
Some(TaggedValue::StrokeJoin(x)) => x,
20102010
_ => &StrokeJoin::Miter,
20112011
};
2012-
20132012
let dash_lengths_val = match &document_node.inputs[DashLengthsInput::<Vec<f64>>::INDEX].as_value() {
20142013
Some(TaggedValue::VecF64(x)) => x,
20152014
_ => &vec![],
20162015
};
2016+
20172017
let has_dash_lengths = dash_lengths_val.is_empty();
20182018
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
20192019

2020-
let color = color_widget(
2021-
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
2022-
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
2020+
let fill = document_node
2021+
.inputs
2022+
.get(ColorInput::<Fill>::INDEX)
2023+
.and_then(|i| i.as_non_exposed_value())
2024+
.and_then(|t| match t {
2025+
TaggedValue::Fill(f) => Some(f.clone()),
2026+
_ => None,
2027+
})
2028+
.unwrap_or(Fill::None);
2029+
let backup_color = document_node
2030+
.inputs
2031+
.get(BackupColorInput::INDEX)
2032+
.and_then(|i| i.as_non_exposed_value())
2033+
.and_then(|t| match t {
2034+
TaggedValue::Color(c) => Some(c.clone()),
2035+
_ => None,
2036+
})
2037+
.unwrap_or(Table::new());
2038+
let backup_gradient = document_node
2039+
.inputs
2040+
.get(BackupGradientInput::INDEX)
2041+
.and_then(|i| i.as_non_exposed_value())
2042+
.and_then(|t| match t {
2043+
TaggedValue::Gradient(g) => Some(g.clone()),
2044+
_ => None,
2045+
})
2046+
.unwrap_or(Gradient::default());
2047+
2048+
let mut widgets = Vec::new();
2049+
2050+
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, ColorInput::<Fill>::INDEX, true, context));
2051+
let fill2 = fill.clone();
2052+
let backup_color_fill: Fill = backup_color.clone().into();
2053+
let backup_gradient_fill: Fill = backup_gradient.clone().into();
2054+
2055+
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2056+
widgets_first_row.push(
2057+
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default()
2058+
.value(fill.clone().into())
2059+
.on_update(move |x: &crate::messages::layout::utility_types::widgets::button_widgets::ColorInput| {
2060+
let new_fill = x.value.to_fill(fill2.as_gradient());
2061+
let backup_msg = match &new_fill {
2062+
Fill::None => NodeGraphMessage::SetInputValue {
2063+
node_id,
2064+
input_index: BackupColorInput::INDEX,
2065+
value: TaggedValue::Color(Table::new()),
2066+
}
2067+
.into(),
2068+
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
2069+
node_id,
2070+
input_index: BackupColorInput::INDEX,
2071+
value: TaggedValue::Color(Table::new_from_element(*color)),
2072+
}
2073+
.into(),
2074+
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
2075+
node_id,
2076+
input_index: BackupGradientInput::INDEX,
2077+
value: TaggedValue::Gradient(gradient.clone()),
2078+
}
2079+
.into(),
2080+
};
2081+
Message::Batched {
2082+
messages: Box::new([
2083+
backup_msg,
2084+
NodeGraphMessage::SetInputValue {
2085+
node_id,
2086+
input_index: ColorInput::<Fill>::INDEX,
2087+
value: TaggedValue::Fill(new_fill),
2088+
}
2089+
.into(),
2090+
]),
2091+
}
2092+
})
2093+
.on_commit(commit_value)
2094+
.widget_instance(),
20232095
);
2096+
widgets.push(LayoutGroup::row(widgets_first_row));
2097+
2098+
let mut fill_type_row = vec![TextLabel::new("").widget_instance()];
2099+
match fill {
2100+
Fill::Solid(_) | Fill::None => add_blank_assist(&mut fill_type_row),
2101+
Fill::Gradient(ref gradient) => {
2102+
let reverse_button = IconButton::new("Reverse", 24)
2103+
.tooltip_description("Reverse the gradient color stops.")
2104+
.on_update(update_value(
2105+
{
2106+
let gradient = gradient.clone();
2107+
move |_| {
2108+
let mut gradient = gradient.clone();
2109+
gradient.stops = gradient.stops.reversed();
2110+
TaggedValue::Fill(Fill::Gradient(gradient))
2111+
}
2112+
},
2113+
node_id,
2114+
ColorInput::<Color>::INDEX,
2115+
))
2116+
.widget_instance();
2117+
fill_type_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2118+
fill_type_row.push(reverse_button);
2119+
}
2120+
}
2121+
2122+
let entries = vec![
2123+
RadioEntryData::new("solid")
2124+
.label("Solid")
2125+
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, ColorInput::<Color>::INDEX))
2126+
.on_commit(commit_value),
2127+
RadioEntryData::new("gradient")
2128+
.label("Gradient")
2129+
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, ColorInput::<Color>::INDEX))
2130+
.on_commit(commit_value),
2131+
];
2132+
2133+
fill_type_row.extend_from_slice(&[
2134+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
2135+
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_instance(),
2136+
]);
2137+
widgets.push(LayoutGroup::row(fill_type_row));
2138+
2139+
if let Fill::Gradient(gradient) = fill.clone() {
2140+
let mut row = vec![TextLabel::new("").widget_instance()];
2141+
match gradient.gradient_type {
2142+
GradientType::Linear => add_blank_assist(&mut row),
2143+
GradientType::Radial => {
2144+
let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 {
2145+
gradient.end.x > gradient.start.x
2146+
} else {
2147+
(gradient.start.x + gradient.start.y) < (gradient.end.x + gradient.end.y)
2148+
};
2149+
let reverse_radial_gradient_button = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
2150+
.tooltip_description("Reverse which end the gradient radiates from.")
2151+
.on_update(update_value(
2152+
{
2153+
let gradient = gradient.clone();
2154+
move |_| {
2155+
let mut gradient = gradient.clone();
2156+
std::mem::swap(&mut gradient.start, &mut gradient.end);
2157+
TaggedValue::Fill(Fill::Gradient(gradient))
2158+
}
2159+
},
2160+
node_id,
2161+
ColorInput::<Fill>::INDEX,
2162+
))
2163+
.widget_instance();
2164+
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2165+
row.push(reverse_radial_gradient_button);
2166+
}
2167+
}
2168+
2169+
let gradient_for_closure = gradient.clone();
2170+
2171+
let entries = [GradientType::Linear, GradientType::Radial]
2172+
.iter()
2173+
.map(|&grad_type| {
2174+
let gradient = gradient_for_closure.clone();
2175+
let set_input_value = update_value(
2176+
move |_: &()| {
2177+
let mut new_gradient = gradient.clone();
2178+
new_gradient.gradient_type = grad_type;
2179+
TaggedValue::Fill(Fill::Gradient(new_gradient))
2180+
},
2181+
node_id,
2182+
ColorInput::<Fill>::INDEX,
2183+
);
2184+
RadioEntryData::new(format!("{:?}", grad_type))
2185+
.label(format!("{:?}", grad_type))
2186+
.on_update(move |_| Message::Batched {
2187+
messages: Box::new([
2188+
set_input_value(&()),
2189+
GradientToolMessage::UpdateOptions {
2190+
options: GradientOptionsUpdate::Type(grad_type),
2191+
}
2192+
.into(),
2193+
]),
2194+
})
2195+
.on_commit(commit_value)
2196+
})
2197+
.collect();
2198+
2199+
row.extend_from_slice(&[
2200+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
2201+
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_instance(),
2202+
]);
2203+
2204+
widgets.push(LayoutGroup::row(row));
2205+
}
2206+
20242207
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
20252208
let align = enum_choice::<StrokeAlign>()
20262209
.for_socket(ParameterWidgetsInfo::new(node_id, AlignInput::INDEX, true, context))
@@ -2029,24 +2212,23 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
20292212
let join = enum_choice::<StrokeJoin>()
20302213
.for_socket(ParameterWidgetsInfo::new(node_id, JoinInput::INDEX, true, context))
20312214
.property_row();
2032-
20332215
let miter_limit = number_widget(
20342216
ParameterWidgetsInfo::new(node_id, MiterLimitInput::INDEX, true, context),
20352217
NumberInput::default().min(0.).disabled(miter_limit_disabled),
20362218
);
20372219
let paint_order = enum_choice::<PaintOrder>()
20382220
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context))
20392221
.property_row();
2040-
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
20412222
let dash_lengths = array_of_number_widget(
20422223
ParameterWidgetsInfo::new(node_id, DashLengthsInput::<Vec<f64>>::INDEX, true, context),
20432224
TextInput::default().centered(true),
20442225
);
2045-
let number_input = disabled_number_input;
2046-
let dash_offset = number_widget(ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context), number_input);
2226+
let dash_offset = number_widget(
2227+
ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context),
2228+
NumberInput::default().unit(" px").disabled(has_dash_lengths),
2229+
);
20472230

2048-
vec![
2049-
color,
2231+
widgets.extend([
20502232
LayoutGroup::row(weight),
20512233
align,
20522234
cap,
@@ -2055,7 +2237,9 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
20552237
paint_order,
20562238
LayoutGroup::row(dash_lengths),
20572239
LayoutGroup::row(dash_offset),
2058-
]
2240+
]);
2241+
2242+
widgets
20592243
}
20602244

20612245
pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

editor/src/messages/tool/tool_messages/fill_tool.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ mod test_fill {
180180
Err(e) => panic!("Failed to evaluate graph: {e}"),
181181
};
182182

183-
instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect()
184-
}
183+
instrumented.grab_all_input::<fill::ColorInput<Fill>>(&editor.runtime).collect()
184+
}
185185

186186
#[tokio::test]
187187
async fn ignore_artboard() {

0 commit comments

Comments
 (0)