Move gradient picking into the color picker (#1778)

* Gradient picker

* Fix up color picker layout CSS problems

* Begin hooking up SpectrumInput for gradient in the ColorPicker

* Working gradient picking on the frontend only

* Plumb FillColorChoice into the backend

* Hook everything else up, just with a weird bug remaining

* Fix some svelty reactivity issues

* Add and remove stops

* Cleanup

* Rename type

* Fill node document format upgrading

* Fix lint

* Polish the color picker UX and fix a bug

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2024-06-09 22:55:13 -07:00
committed by GitHub
co-authored by 0hypercube
parent 449729f1e1
commit a9a4b5cd19
48 changed files with 1380 additions and 664 deletions
@@ -4,6 +4,7 @@ use crate::messages::prelude::*;
use graphene_core::raster::color::Color;
use graphene_core::text::Font;
use graphene_std::vector::style::{FillChoice, GradientStops};
use serde_json::Value;
@@ -83,24 +84,63 @@ impl LayoutMessageHandler {
let callback_message = match action {
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
WidgetValueAction::Update => {
let update_value = value.as_object().expect("ColorButton update was not of type: object");
let parsed_color = (|| {
let is_none = update_value.get("none")?.as_bool()?;
let decode_color = |color: &serde_json::map::Map<String, serde_json::value::Value>| -> Option<Color> {
let red = color.get("red").and_then(|x| x.as_f64()).map(|x| x as f32);
let green = color.get("green").and_then(|x| x.as_f64()).map(|x| x as f32);
let blue = color.get("blue").and_then(|x| x.as_f64()).map(|x| x as f32);
let alpha = color.get("alpha").and_then(|x| x.as_f64()).map(|x| x as f32);
if !is_none {
Some(Some(Color::from_rgbaf32(
update_value.get("red")?.as_f64()? as f32,
update_value.get("green")?.as_f64()? as f32,
update_value.get("blue")?.as_f64()? as f32,
update_value.get("alpha")?.as_f64()? as f32,
)?))
} else {
Some(None)
if let (Some(red), Some(green), Some(blue), Some(alpha)) = (red, green, blue, alpha) {
if let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) {
return Some(color);
}
}
None
};
(|| {
let update_value = value.as_object().expect("ColorButton update was not of type: object");
// None
let is_none = update_value.get("none").and_then(|x| x.as_bool());
if is_none == Some(true) {
color_button.value = FillChoice::None;
return (color_button.on_update.callback)(color_button);
}
// Solid
if let Some(color) = decode_color(update_value) {
color_button.value = FillChoice::Solid(color);
return (color_button.on_update.callback)(color_button);
}
// Gradient
let gradient = update_value.get("stops").and_then(|x| x.as_array());
if let Some(stops) = gradient {
let gradient_stops = stops
.iter()
.filter_map(|stop| {
stop.as_object().and_then(|stop| {
let position = stop.get("position").and_then(|x| x.as_f64());
let color = stop.get("color").and_then(|x| x.as_object());
if let (Some(position), Some(color_object)) = (position, color) {
if let Some(color) = decode_color(color_object) {
return Some((position, color));
}
}
None
})
})
.collect::<Vec<_>>();
color_button.value = FillChoice::Gradient(GradientStops(gradient_stops));
return (color_button.on_update.callback)(color_button);
}
panic!("ColorButton update was not able to be parsed with color data: {color_button:?}");
})()
.unwrap_or_else(|| panic!("ColorButton update was not able to be parsed with color data: {color_button:?}"));
color_button.value = parsed_color;
(color_button.on_update.callback)(color_button)
}
};
@@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use graphene_core::raster::color::Color;
use graphene_std::vector::style::FillChoice;
use graphite_proc_macros::WidgetBuilder;
use derivative::*;
@@ -124,7 +124,7 @@ pub struct TextButton {
#[derivative(Debug, PartialEq, Default)]
pub struct ColorButton {
#[widget_builder(constructor)]
pub value: Option<Color>,
pub value: FillChoice,
pub disabled: bool,
@@ -137,7 +137,9 @@ pub struct ColorButton {
#[derivative(Default(value = "true"))]
pub allow_none: bool,
// TODO: Implement
// pub disabled: bool,
//
pub tooltip: String,
#[serde(skip)]
@@ -26,6 +26,7 @@ use graphene_core::raster::BlendMode;
use graphene_core::raster::ImageFrame;
use graphene_core::renderer::ClickTarget;
use graphene_core::vector::style::ViewMode;
use graphene_std::vector::style::{Fill, FillType, Gradient};
use glam::{DAffine2, DVec2, IVec2};
@@ -1222,9 +1223,68 @@ impl DocumentMessageHandler {
}
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
serde_json::from_str(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string()))
// serde_json::from_str(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string()))
// TODO: Use this to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
match serde_json::from_str::<Self>(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string())) {
Ok(mut document) => {
for (_, node) in &mut document.network.nodes {
// Upgrade Fill nodes to the format change in #1778
// TODO: Eventually remove this (probably starting late 2024)
if node.name == "Fill" && node.inputs.len() == 8 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type(&node.name).unwrap();
let default_definition_node = node_definition.default_document_node();
node.implementation = default_definition_node.implementation.clone();
let old_inputs = std::mem::replace(&mut node.inputs, default_definition_node.inputs.clone());
node.inputs[0] = old_inputs[0].clone();
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
let TaggedValue::FillType(fill_type) = fill_type else { continue };
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
let TaggedValue::DVec2(start) = start else { continue };
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
let TaggedValue::DVec2(end) = end else { continue };
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
let TaggedValue::DAffine2(transform) = transform else { continue };
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
let TaggedValue::GradientStops(positions) = positions else { continue };
let fill = match (fill_type, solid_color) {
(FillType::Solid, None) => Fill::None,
(FillType::Solid, Some(color)) => Fill::Solid(color),
(FillType::Gradient, _) => Fill::Gradient(Gradient {
stops: positions,
gradient_type,
start,
end,
transform,
}),
};
node.inputs[1] = NodeInput::value(TaggedValue::Fill(fill.clone()), false);
match fill {
Fill::None => {
node.inputs[2] = NodeInput::value(TaggedValue::OptionalColor(None), false);
}
Fill::Solid(color) => {
node.inputs[2] = NodeInput::value(TaggedValue::OptionalColor(Some(color)), false);
}
Fill::Gradient(gradient) => {
node.inputs[3] = NodeInput::value(TaggedValue::Gradient(gradient), false);
}
}
}
}
Ok(document)
}
Err(e) => Err(e),
}
// TODO: This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
// match serde_json::from_str::<Self>(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string())) {
// Ok(mut document) => {
@@ -12,6 +12,7 @@ use graphene_core::text::Font;
use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
use graphene_core::Color;
use graphene_std::vector::convert_usvg_path;
use graphene_std::vector::style::GradientStops;
use glam::{DAffine2, DVec2, IVec2};
@@ -888,13 +889,15 @@ fn apply_usvg_fill(fill: &Option<usvg::Fill>, modify_inputs: &mut ModifyInputsCo
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
let stops = linear.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear,
positions: linear.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
stops,
})
}
usvg::Paint::RadialGradient(radial) => {
@@ -911,13 +914,15 @@ fn apply_usvg_fill(fill: &Option<usvg::Fill>, modify_inputs: &mut ModifyInputsCo
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
let stops = radial.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial,
positions: radial.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
stops,
})
}
usvg::Paint::Pattern(_) => {
@@ -12,7 +12,7 @@ use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, FillType, Stroke};
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Type;
use graphene_core::{Artboard, Color};
use graphene_std::vector::ManipulatorPointId;
@@ -538,23 +538,24 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn fill_set(&mut self, fill: Fill) {
let fill_index = 1;
let backup_color_index = 2;
let backup_gradient_index = 3;
self.modify_inputs("Fill", false, |inputs, _node_id, _metadata| {
let fill_type = match fill {
Fill::None | Fill::Solid(_) => FillType::Solid,
Fill::Gradient(_) => FillType::Gradient,
};
inputs[1] = NodeInput::value(TaggedValue::FillType(fill_type), false);
if Fill::None == fill {
inputs[2] = NodeInput::value(TaggedValue::OptionalColor(None), false);
} else if let Fill::Solid(color) = fill {
inputs[2] = NodeInput::value(TaggedValue::OptionalColor(Some(color)), false);
} else if let Fill::Gradient(gradient) = fill {
inputs[3] = NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false);
inputs[4] = NodeInput::value(TaggedValue::DVec2(gradient.start), false);
inputs[5] = NodeInput::value(TaggedValue::DVec2(gradient.end), false);
inputs[6] = NodeInput::value(TaggedValue::DAffine2(gradient.transform), false);
inputs[7] = NodeInput::value(TaggedValue::GradientPositions(gradient.positions), false);
match &fill {
Fill::None => {
inputs[backup_color_index] = NodeInput::value(TaggedValue::OptionalColor(None), false);
}
Fill::Solid(color) => {
inputs[backup_color_index] = NodeInput::value(TaggedValue::OptionalColor(Some(*color)), false);
}
Fill::Gradient(gradient) => {
inputs[backup_gradient_index] = NodeInput::value(TaggedValue::Gradient(gradient.clone()), false);
}
}
inputs[fill_index] = NodeInput::value(TaggedValue::Fill(fill), false);
});
}
@@ -2389,16 +2389,26 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeDefinition {
name: "Fill",
category: "Vector",
implementation: DocumentNodeImplementation::proto("graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>"),
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(0), 0)],
nodes: vec![DocumentNode {
name: "Set Fill".to_string(),
inputs: vec![NodeInput::network(concrete!(VectorData), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SetFillNode<_>")),
..Default::default()
}]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Fill Type", TaggedValue::FillType(vector::style::FillType::Solid), false),
DocumentInputType::value("Solid Color", TaggedValue::OptionalColor(None), false),
DocumentInputType::value("Gradient Type", TaggedValue::GradientType(vector::style::GradientType::Linear), false),
DocumentInputType::value("Start", TaggedValue::DVec2(DVec2::new(0., 0.5)), false),
DocumentInputType::value("End", TaggedValue::DVec2(DVec2::new(1., 0.5)), false),
DocumentInputType::value("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false),
DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Color::BLACK), (1., Color::WHITE)]), false),
DocumentInputType::value("Fill", TaggedValue::Fill(vector::style::Fill::Solid(Color::BLACK)), false),
// These backup values aren't exposed to the user, but are used to store the previous fill choices so the user can flip back from Solid to Gradient (or vice versa) without losing their settings
DocumentInputType::value("Backup Color", TaggedValue::OptionalColor(Some(Color::BLACK)), false),
DocumentInputType::value("Backup Gradient", TaggedValue::Gradient(Default::default()), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
properties: node_properties::fill_properties,
@@ -2492,7 +2502,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Instance", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Random Scale Min", TaggedValue::F64(1.), false),
DocumentInputType::value("Random Scale Max", TaggedValue::F64(1.), false),
DocumentInputType::value("Random Scale Bias", TaggedValue::F64(1.), false),
DocumentInputType::value("Random Scale Bias", TaggedValue::F64(0.), false),
DocumentInputType::value("Random Rotation", TaggedValue::F64(0.), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
@@ -15,7 +15,8 @@ use graphene_core::raster::{
};
use graphene_core::text::Font;
use graphene_core::vector::misc::CentroidType;
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
use graphene_std::vector::style::{Fill, FillChoice};
use glam::{DVec2, IVec2, UVec2};
use graphene_std::vector::misc::BooleanOperation;
@@ -692,206 +693,48 @@ fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
LayoutGroup::Row { widgets }
}
fn fill_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, "Fill Type", FrontendGraphDataType::General, true);
if let &NodeInput::Value {
tagged_value: TaggedValue::FillType(fill_type),
exposed: false,
} = &document_node.inputs[index]
{
let entries = vec![
RadioEntryData::new("solid")
.label("Solid")
.on_update(update_value(move |_| TaggedValue::FillType(FillType::Solid), node_id, index))
.on_commit(commit_value),
RadioEntryData::new("gradient")
.label("Gradient")
.on_update(update_value(move |_| TaggedValue::FillType(FillType::Gradient), node_id, index))
.on_commit(commit_value),
];
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries)
.selected_index(match fill_type {
FillType::Solid => Some(0),
FillType::Gradient => Some(1),
})
.widget_holder(),
]);
}
LayoutGroup::Row { widgets }
}
fn gradient_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, "Gradient Type", FrontendGraphDataType::General, true);
if let &NodeInput::Value {
tagged_value: TaggedValue::GradientType(gradient_type),
exposed: false,
} = &document_node.inputs[index]
{
let entries = vec![
RadioEntryData::new("linear")
.label("Linear")
.on_update(update_value(move |_| TaggedValue::GradientType(GradientType::Linear), node_id, index))
.on_commit(commit_value),
RadioEntryData::new("radial")
.label("Radial")
.on_update(update_value(move |_| TaggedValue::GradientType(GradientType::Radial), node_id, index))
.on_commit(commit_value),
];
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_holder(),
]);
}
LayoutGroup::Row { widgets }
}
fn gradient_row(row: &mut Vec<WidgetHolder>, positions: &Vec<(f64, Color)>, index: usize, node_id: NodeId, input_index: usize) {
let label = TextLabel::new(format!("Gradient: {:.0}%", positions[index].0 * 100.)).tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active");
row.push(label.widget_holder());
let on_update = {
let positions = positions.clone();
move |color_button: &ColorButton| {
let mut new_positions = positions.clone();
new_positions[index].1 = color_button.value.unwrap();
TaggedValue::GradientPositions(new_positions)
}
};
let color = ColorButton::new(Some(positions[index].1))
.on_update(update_value(on_update, node_id, input_index))
.on_commit(commit_value)
.allow_none(false);
add_blank_assist(row);
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
row.push(color.widget_holder());
let mut skip_separator = false;
// Remove button
if positions.len() != index + 1 && index != 0 {
let on_update = {
let in_positions = positions.clone();
move |_: &IconButton| {
let mut new_positions = in_positions.clone();
new_positions.remove(index);
TaggedValue::GradientPositions(new_positions)
}
};
skip_separator = true;
row.push(Separator::new(SeparatorType::Related).widget_holder());
row.push(
IconButton::new("Remove", 16)
.tooltip("Remove this gradient stop")
.on_update(update_value(on_update, node_id, input_index))
.on_commit(commit_value)
.widget_holder(),
);
}
// Add button
if positions.len() != index + 1 {
let on_update = {
let positions = positions.clone();
move |_: &IconButton| {
let mut new_positions = positions.clone();
// Blend linearly between the two colors.
let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).map(|x| x.1)) {
(a, Some(b)) => Color::from_rgbaf32_unchecked((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
(_, None) => Color::WHITE,
};
let get_pos = |index: usize| (new_positions[index].0 + new_positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.;
new_positions.push((get_pos(index), get_color(index)));
new_positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
TaggedValue::GradientPositions(new_positions)
}
};
if !skip_separator {
row.push(Separator::new(SeparatorType::Related).widget_holder());
}
row.push(
IconButton::new("Add", 16)
.tooltip("Add a gradient stop after this")
.on_update(update_value(on_update, node_id, input_index))
.on_commit(commit_value)
.widget_holder(),
);
}
}
fn gradient_positions(rows: &mut Vec<LayoutGroup>, document_node: &DocumentNode, name: &str, node_id: NodeId, input_index: usize) {
let mut widgets = vec![expose_widget(node_id, input_index, FrontendGraphDataType::General, document_node.inputs[input_index].is_exposed())];
if let NodeInput::Value {
tagged_value: TaggedValue::GradientPositions(gradient_positions),
exposed: false,
} = &document_node.inputs[input_index]
{
for index in 0..gradient_positions.len() {
gradient_row(&mut widgets, gradient_positions, index, node_id, input_index);
let widgets = std::mem::take(&mut widgets);
rows.push(LayoutGroup::Row { widgets });
}
let on_update = {
let gradient_positions = gradient_positions.clone();
move |_: &TextButton| {
let mut new_positions = gradient_positions.clone();
new_positions = new_positions.iter().map(|(distance, color)| (1. - distance, *color)).collect();
new_positions.reverse();
TaggedValue::GradientPositions(new_positions)
}
};
let invert = TextButton::new("Invert")
.icon(Some("Swap".into()))
.tooltip("Reverse the order of each color stop")
.on_update(update_value(on_update, node_id, input_index))
.on_commit(commit_value)
.widget_holder();
if widgets.is_empty() {
widgets.push(TextLabel::new("").widget_holder());
add_blank_assist(&mut widgets);
}
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(invert);
rows.push(LayoutGroup::Row { widgets });
} else {
widgets.push(TextLabel::new(name).widget_holder());
rows.push(LayoutGroup::Row { widgets })
}
}
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] {
if let &TaggedValue::Color(x) = tagged_value {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
color_props
.value(Some(x as Color))
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.unwrap()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
])
} else if let &TaggedValue::OptionalColor(x) = tagged_value {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
color_props
.value(x)
.on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value), node_id, index))
.on_commit(commit_value)
.widget_holder(),
])
}
// Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel
let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] else {
return LayoutGroup::Row { widgets };
};
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
match tagged_value {
TaggedValue::Color(color) => widgets.push(
color_props
.value(FillChoice::Solid(*color))
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColor(color) => widgets.push(
color_props
.value(match color {
Some(color) => FillChoice::Solid(*color),
None => FillChoice::None,
})
.on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(ref x) => widgets.push(
color_props
.value(FillChoice::Gradient(x.clone()))
.on_update(update_value(
|x: &ColorButton| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
_ => {}
}
LayoutGroup::Row { widgets }
}
@@ -2460,37 +2303,141 @@ pub fn morph_properties(document_node: &DocumentNode, node_id: NodeId, _context:
/// Fill Node Widgets LayoutGroup
pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let fill_type_index = 1;
let solid_color_index = 2;
let gradient_type_index = 3;
let positions_index = 7;
let fill_index = 1;
let backup_color_index = 2;
let backup_gradient_index = 3;
let fill_type = if let &NodeInput::Value {
tagged_value: TaggedValue::FillType(fill_type),
..
} = &document_node.inputs[fill_type_index]
{
Some(fill_type)
let mut widgets_first_row = start_widgets(document_node, node_id, fill_index, "Fill", FrontendGraphDataType::General, true);
let (fill, backup_color, backup_gradient) = if let (
NodeInput::Value {
tagged_value: TaggedValue::Fill(fill),
..
},
NodeInput::Value {
tagged_value: TaggedValue::OptionalColor(backup_color),
..
},
NodeInput::Value {
tagged_value: TaggedValue::Gradient(backup_gradient),
..
},
) = (
&document_node.inputs[fill_index],
&document_node.inputs[backup_color_index],
&document_node.inputs[backup_gradient_index],
) {
(fill, backup_color, backup_gradient)
} else {
None
return vec![LayoutGroup::Row { widgets: widgets_first_row }];
};
let fill2 = fill.clone();
let backup_color_fill: Fill = backup_color.clone().into();
let backup_gradient_fill: Fill = backup_gradient.clone().into();
let mut widgets = Vec::new();
let gradient = fill_type == Some(graphene_core::vector::style::FillType::Gradient);
let solid = fill_type == Some(graphene_core::vector::style::FillType::Solid);
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets_first_row.push(
ColorButton::default()
.value(fill.clone().into())
.on_update(move |x: &ColorButton| {
Message::Batched(Box::new([
match &fill2 {
Fill::None => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_color_index,
value: TaggedValue::OptionalColor(None),
}
.into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_color_index,
value: TaggedValue::OptionalColor(Some(color.clone())),
}
.into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
node_id,
input_index: backup_gradient_index,
value: TaggedValue::Gradient(gradient.clone()),
}
.into(),
},
NodeGraphMessage::SetInputValue {
node_id,
input_index: fill_index,
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
}
.into(),
]))
})
.on_commit(commit_value)
.widget_holder(),
);
let mut widgets = vec![LayoutGroup::Row { widgets: widgets_first_row }];
let fill_type_switch = fill_type_widget(document_node, node_id, fill_type_index);
let fill_type_switch = {
let mut row = vec![TextLabel::new("").widget_holder()];
add_blank_assist(&mut row);
let entries = vec![
RadioEntryData::new("solid")
.label("Solid")
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, fill_index))
.on_commit(commit_value),
RadioEntryData::new("gradient")
.label("Gradient")
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, fill_index))
.on_commit(commit_value),
];
row.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_holder(),
]);
LayoutGroup::Row { widgets: row }
};
widgets.push(fill_type_switch);
if fill_type.is_none() || solid {
let solid_color = color_widget(document_node, node_id, solid_color_index, "Color", ColorButton::default(), true);
widgets.push(solid_color);
}
if let Fill::Gradient(gradient) = fill {
let mut row = vec![TextLabel::new("").widget_holder()];
add_blank_assist(&mut row);
if fill_type.is_none() || gradient {
let gradient_type_switch = gradient_type_widget(document_node, node_id, gradient_type_index);
widgets.push(gradient_type_switch);
gradient_positions(&mut widgets, document_node, "Gradient Positions", node_id, positions_index);
let new_gradient1 = gradient.clone();
let new_gradient2 = gradient.clone();
let entries = vec![
RadioEntryData::new("linear")
.label("Linear")
.on_update(update_value(
move |_| {
let mut new_gradient = new_gradient1.clone();
new_gradient.gradient_type = GradientType::Linear;
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
fill_index,
))
.on_commit(commit_value),
RadioEntryData::new("radial")
.label("Radial")
.on_update(update_value(
move |_| {
let mut new_gradient = new_gradient2.clone();
new_gradient.gradient_type = GradientType::Radial;
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
fill_index,
))
.on_commit(commit_value),
];
row.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_holder(),
]);
widgets.push(LayoutGroup::Row { widgets: row });
}
widgets
@@ -7,6 +7,7 @@ use graphene_core::raster::color::Color;
use graphene_core::renderer::Quad;
use glam::DVec2;
use graphene_std::vector::style::FillChoice;
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
let origin = document.snapping_state.grid.origin;
@@ -226,7 +227,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
};
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
update_val::<ColorButton>(grid, move |grid, color| {
if let Some(color) = color.value {
if let FillChoice::Solid(color) = color.value {
if let Some(update_color) = update(grid) {
*update_color = color;
}
@@ -276,7 +277,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
Separator::new(SeparatorType::Related).widget_holder(),
]);
color_widgets.push(
ColorButton::new(Some(grid.grid_color))
ColorButton::new(FillChoice::Solid(grid.grid_color))
.tooltip("Grid display color")
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))
.widget_holder(),
@@ -375,7 +375,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
self.load_document(document, document_id, responses);
}
Err(e) => {
println!("Failed to open document: {e}");
if !document_is_auto_saved {
responses.add(DialogMessage::DisplayDialogError {
title: "Failed to open document".to_string(),
@@ -612,7 +611,7 @@ impl PortfolioMessageHandler {
}
}
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Load*
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Open*
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
self.document_ids.push(document_id);
@@ -2,6 +2,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::Message;
use graphene_core::Color;
use graphene_std::vector::style::FillChoice;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ToolColorType {
@@ -96,7 +97,9 @@ impl ToolColorOptions {
widgets.push(radio);
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
let color_button = ColorButton::new(self.active_color()).allow_none(color_allow_none).on_update(color_callback);
let color_button = ColorButton::new(FillChoice::from_optional_color(self.active_color()))
.allow_none(color_allow_none)
.on_update(color_callback);
widgets.push(color_button.widget_holder());
widgets
@@ -7,7 +7,7 @@ use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput,
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::{FillType, Gradient};
use graphene_core::vector::style::Gradient;
use graphene_core::Color;
use glam::DVec2;
@@ -95,38 +95,21 @@ pub fn get_colinear_manipulators(layer: LayerNodeIdentifier, document_network: &
/// Get the current gradient of a layer from the closest Fill node
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else {
let TaggedValue::Fill(graphene_std::vector::style::Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
let TaggedValue::GradientType(gradient_type) = inputs.get(3)?.as_value()? else {
return None;
};
let TaggedValue::DVec2(start) = inputs.get(4)?.as_value()? else {
return None;
};
let TaggedValue::DVec2(end) = inputs.get(5)?.as_value()? else {
return None;
};
let TaggedValue::DAffine2(transform) = inputs.get(6)?.as_value()? else {
return None;
};
let TaggedValue::GradientPositions(positions) = inputs.get(7)?.as_value()? else {
return None;
};
Some(Gradient {
start: *start,
end: *end,
transform: *transform,
positions: positions.clone(),
gradient_type: *gradient_type,
})
Some(gradient.clone())
}
/// Get the current fill of a layer from the closest Fill node
pub fn get_fill_color(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Color> {
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Fill")?;
let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else {
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(*color)
@@ -152,7 +152,7 @@ impl LayoutHolder for BrushTool {
false,
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into(),
|color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
@@ -90,7 +90,7 @@ impl LayoutHolder for EllipseTool {
true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -100,7 +100,7 @@ impl LayoutHolder for EllipseTool {
true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -99,7 +99,7 @@ impl LayoutHolder for FreehandTool {
true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -109,7 +109,7 @@ impl LayoutHolder for FreehandTool {
true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -185,11 +185,11 @@ impl SelectedGradient {
// Should not go off end but can swap
let clamped = new_pos.clamp(0., 1.);
self.gradient.positions[s].0 = clamped;
let new_pos = self.gradient.positions[s];
self.gradient.stops.0[s].0 = clamped;
let new_pos = self.gradient.stops.0[s];
self.gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self.dragging = GradientDragTarget::Step(self.gradient.positions.iter().position(|x| *x == new_pos).unwrap());
self.gradient.stops.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self.dragging = GradientDragTarget::Step(self.gradient.stops.0.iter().position(|x| *x == new_pos).unwrap());
}
}
self.render_gradient(responses);
@@ -256,14 +256,14 @@ impl Fsm for GradientToolFsmState {
.filter(|selected| selected.layer.map_or(false, |selected_layer| selected_layer == layer))
.map(|selected| selected.dragging);
let Gradient { start, end, positions, .. } = gradient;
let Gradient { start, end, stops, .. } = gradient;
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end, None, None);
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
for (index, (position, _)) in positions.into_iter().enumerate() {
for (index, (position, _)) in stops.0.into_iter().enumerate() {
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
continue;
}
@@ -280,32 +280,31 @@ impl Fsm for GradientToolFsmState {
};
// Skip if invalid gradient
if selected_gradient.gradient.positions.len() < 2 {
if selected_gradient.gradient.stops.0.len() < 2 {
return self;
}
// Remove the selected point
match selected_gradient.dragging {
GradientDragTarget::Start => selected_gradient.gradient.positions.remove(0),
GradientDragTarget::End => selected_gradient.gradient.positions.pop().unwrap(),
GradientDragTarget::Step(index) => selected_gradient.gradient.positions.remove(index),
GradientDragTarget::Start => selected_gradient.gradient.stops.0.remove(0),
GradientDragTarget::End => selected_gradient.gradient.stops.0.pop().unwrap(),
GradientDragTarget::Step(index) => selected_gradient.gradient.stops.0.remove(index),
};
// The gradient has only one point and so should become a fill
if selected_gradient.gradient.positions.len() == 1 {
if selected_gradient.gradient.stops.0.len() == 1 {
if let Some(layer) = selected_gradient.layer {
responses.add(GraphOperationMessage::FillSet {
layer,
fill: Fill::Solid(selected_gradient.gradient.positions[0].1),
fill: Fill::Solid(selected_gradient.gradient.stops.0[0].1),
});
}
return self;
}
// Find the minimum and maximum positions
let min_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
let max_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
let min_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
let max_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
// Recompute the start and end position of the gradient (in viewport transform)
let transform = selected_gradient.transform;
@@ -315,7 +314,7 @@ impl Fsm for GradientToolFsmState {
selected_gradient.gradient.end = transform.inverse().transform_point2(new_end);
// Remap the positions
for (position, _) in selected_gradient.gradient.positions.iter_mut() {
for (position, _) in selected_gradient.gradient.stops.0.iter_mut() {
*position = (*position - min_position) / (max_position - min_position);
}
@@ -369,7 +368,7 @@ impl Fsm for GradientToolFsmState {
let transform = gradient_space_transform(layer, document);
// Check for dragging step
for (index, (pos, _)) in gradient.positions.iter().enumerate() {
for (index, (pos, _)) in gradient.stops.0.iter().enumerate() {
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
if pos.distance_squared(mouse) < tolerance {
dragging = true;
@@ -87,7 +87,7 @@ impl LayoutHolder for LineTool {
true,
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -107,7 +107,7 @@ impl LayoutHolder for PenTool {
true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -117,7 +117,7 @@ impl LayoutHolder for PenTool {
true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -133,7 +133,7 @@ impl LayoutHolder for PolygonTool {
true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -143,7 +143,7 @@ impl LayoutHolder for PolygonTool {
true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -77,7 +77,7 @@ impl LayoutHolder for RectangleTool {
true,
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -87,7 +87,7 @@ impl LayoutHolder for RectangleTool {
true,
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -99,7 +99,7 @@ impl LayoutHolder for SplineTool {
true,
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(),
);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -109,7 +109,7 @@ impl LayoutHolder for SplineTool {
true,
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value)).into(),
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
@@ -127,7 +127,7 @@ impl LayoutHolder for TextTool {
true,
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value)).into(),
|color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(),
));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
+1 -1
View File
@@ -141,7 +141,7 @@ impl DocumentToolData {
},
LayoutGroup::Row {
widgets: vec![
IconButton::new("Swap", 16)
IconButton::new("SwapVertical", 16)
.tooltip("Swap")
.tooltip_shortcut(action_keys!(ToolMessageDiscriminant::SwapColors))
.on_update(|_| ToolMessage::SwapColors.into())