Skip to content

Commit

Permalink
Add basic support for radial gradients (GraphiteEditor#639)
Browse files Browse the repository at this point in the history
* Add SVG string generator for radial gradients

* Add the UI for the linear vs radial radio inputs

* Initial radial gradient support for gradient tool

* Enabled click and drag support for radial gradients

* Refactor code for gradient in properties panel

* Added gradient type to gradient struct

* Finish refactor to use gradient_type instead of fill

* Fix lint issue

* Combine LinearGradient and RadialGradient in Fill enum

* Add label to properties panel and fix bug

Co-authored-by: Robert Nadal <[email protected]>
Co-authored-by: Oliver Davies <[email protected]>
  • Loading branch information
3 people authored and Keavon committed May 24, 2022
1 parent fc2d983 commit 860c4ad
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 107 deletions.
177 changes: 93 additions & 84 deletions editor/src/document/properties_panel_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::message_prelude::*;
use graphene::color::Color;
use graphene::document::{Document as GrapheneDocument, FontCache};
use graphene::layers::layer_info::{Layer, LayerDataType};
use graphene::layers::style::{Fill, LineCap, LineJoin, Stroke};
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
use graphene::layers::text_layer::TextLayer;
use graphene::{LayerId, Operation};

Expand Down Expand Up @@ -785,6 +785,94 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow {
}
}

fn node_gradient_type(gradient: &Gradient) -> LayoutRow {
let selected_index = match gradient.gradient_type {
GradientType::Linear => 0,
GradientType::Radial => 1,
};
let mut cloned_gradient_linear = gradient.clone();
cloned_gradient_linear.gradient_type = GradientType::Linear;
let mut cloned_gradient_radial = gradient.clone();
cloned_gradient_radial.gradient_type = GradientType::Radial;
LayoutRow::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Gradient Type".into(),
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::RadioInput(RadioInput {
selected_index,
entries: vec![
RadioEntryData {
value: "linear".into(),
label: "Linear".into(),
tooltip: "Linear Gradient".into(),
on_update: WidgetCallback::new(move |_| {
PropertiesPanelMessage::ModifyFill {
fill: Fill::Gradient(cloned_gradient_linear.clone()),
}
.into()
}),
..RadioEntryData::default()
},
RadioEntryData {
value: "radial".into(),
label: "Radial".into(),
tooltip: "Radial Gradient".into(),
on_update: WidgetCallback::new(move |_| {
PropertiesPanelMessage::ModifyFill {
fill: Fill::Gradient(cloned_gradient_radial.clone()),
}
.into()
}),
..RadioEntryData::default()
},
],
})),
],
}
}

fn node_gradient_color(gradient: &Gradient, percent_label: &'static str, position: usize) -> LayoutRow {
let gradient_clone = Rc::new(gradient.clone());
let send_fill_message = move |new_gradient: Gradient| PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into();
LayoutRow::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("Gradient: {}", percent_label),
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::ColorInput(ColorInput {
value: gradient_clone.positions[position].1.map(|color| color.rgba_hex()),
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
if let Some(value) = &text_input.value {
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
let mut new_gradient = (*gradient_clone).clone();
new_gradient.positions[position].1 = Some(color);
send_fill_message(new_gradient)
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
} else {
let mut new_gradient = (*gradient_clone).clone();
new_gradient.positions[position].1 = None;
send_fill_message(new_gradient)
}
}),
..ColorInput::default()
})),
],
}
}

fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
match fill {
Fill::Solid(_) | Fill::None => Some(LayoutRow::Section {
Expand Down Expand Up @@ -818,89 +906,10 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
],
}],
}),
Fill::LinearGradient(gradient) => {
let gradient_1 = Rc::new(gradient.clone());
let gradient_2 = gradient_1.clone();
Some(LayoutRow::Section {
name: "Fill".into(),
layout: vec![
LayoutRow::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Gradient: 0%".into(),
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::ColorInput(ColorInput {
value: gradient_1.positions[0].1.map(|color| color.rgba_hex()),
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
if let Some(value) = &text_input.value {
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
let mut new_gradient = (*gradient_1).clone();
new_gradient.positions[0].1 = Some(color);
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
} else {
let mut new_gradient = (*gradient_1).clone();
new_gradient.positions[0].1 = None;
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
}
}),
..ColorInput::default()
})),
],
},
LayoutRow::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Gradient: 100%".into(),
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::ColorInput(ColorInput {
value: gradient_2.positions[1].1.map(|color| color.rgba_hex()),
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
if let Some(value) = &text_input.value {
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
let mut new_gradient = (*gradient_2).clone();
new_gradient.positions[1].1 = Some(color);
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
} else {
let mut new_gradient = (*gradient_2).clone();
new_gradient.positions[1].1 = None;
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
}
}),
..ColorInput::default()
})),
],
},
],
})
}
Fill::Gradient(gradient) => Some(LayoutRow::Section {
name: "Fill".into(),
layout: vec![node_gradient_type(gradient), node_gradient_color(gradient, "0%", 0), node_gradient_color(gradient, "100%", 1)],
}),
}
}

Expand Down
82 changes: 70 additions & 12 deletions editor/src/viewport_tools/tools/gradient_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::layout::widgets::{LayoutRow, PropertyHolder, RadioEntryData, RadioInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
Expand All @@ -12,7 +12,7 @@ use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::color::Color;
use graphene::intersection::Quad;
use graphene::layers::layer_info::Layer;
use graphene::layers::style::{Fill, Gradient, PathStyle, Stroke};
use graphene::layers::style::{Fill, Gradient, GradientType, PathStyle, Stroke};
use graphene::Operation;

use glam::{DAffine2, DVec2};
Expand All @@ -22,6 +22,17 @@ use serde::{Deserialize, Serialize};
pub struct GradientTool {
fsm_state: GradientToolFsmState,
data: GradientToolData,
options: GradientOptions,
}

pub struct GradientOptions {
gradient_type: GradientType,
}

impl Default for GradientOptions {
fn default() -> Self {
Self { gradient_type: GradientType::Linear }
}
}

#[remain::sorted]
Expand All @@ -40,6 +51,13 @@ pub enum GradientToolMessage {
constrain_axis: Key,
},
PointerUp,
UpdateOptions(GradientOptionsUpdate),
}

#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum GradientOptionsUpdate {
Type(GradientType),
}

impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool {
Expand All @@ -53,8 +71,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = action {
match action {
GradientOptionsUpdate::Type(gradient_type) => self.options.gradient_type = gradient_type,
}
return;
}

let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses);

if self.fsm_state != new_state {
self.fsm_state = new_state;
Expand All @@ -65,7 +89,31 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool
advertise_actions!(GradientToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, Abort);
}

impl PropertyHolder for GradientTool {}
impl PropertyHolder for GradientTool {
fn properties(&self) -> WidgetLayout {
WidgetLayout::new(vec![LayoutRow::Row {
widgets: vec![WidgetHolder::new(Widget::RadioInput(RadioInput {
selected_index: if self.options.gradient_type == GradientType::Radial { 1 } else { 0 },
entries: vec![
RadioEntryData {
value: "linear".into(),
label: "Linear".into(),
tooltip: "Linear Gradient".into(),
on_update: WidgetCallback::new(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()),
..RadioEntryData::default()
},
RadioEntryData {
value: "radial".into(),
label: "Radial".into(),
tooltip: "Radial Gradient".into(),
on_update: WidgetCallback::new(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()),
..RadioEntryData::default()
},
],
}))],
}])
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum GradientToolFsmState {
Expand Down Expand Up @@ -199,7 +247,9 @@ impl SelectedGradient {
self
}

pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool) {
pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool, gradient_type: GradientType) {
self.gradient.gradient_type = gradient_type;

if snap_rotate {
let point = if self.dragging_start {
self.transform.transform_point2(self.gradient.end)
Expand Down Expand Up @@ -228,7 +278,7 @@ impl SelectedGradient {
}

self.gradient.transform = self.transform;
let fill = Fill::LinearGradient(self.gradient.clone());
let fill = Fill::Gradient(self.gradient.clone());
let path = self.path.clone();
responses.push_back(Operation::SetLayerFill { path, fill }.into());
}
Expand All @@ -248,15 +298,15 @@ pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHand

impl Fsm for GradientToolFsmState {
type ToolData = GradientToolData;
type ToolOptions = ();
type ToolOptions = GradientOptions;

fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
Expand All @@ -270,7 +320,7 @@ impl Fsm for GradientToolFsmState {
for path in document.selected_visible_layers() {
let layer = document.graphene_document.layer(path).unwrap();

if let Ok(Fill::LinearGradient(gradient)) = layer.style().map(|style| style.fill()) {
if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) {
let dragging_start = data
.selected_gradient
.as_ref()
Expand Down Expand Up @@ -326,9 +376,17 @@ impl Fsm for GradientToolFsmState {

let layer = document.graphene_document.layer(&intersection).unwrap();

let gradient = Gradient::new(DVec2::ZERO, tool_data.secondary_color, DVec2::ONE, tool_data.primary_color, DAffine2::IDENTITY, generate_uuid());
let gradient = Gradient::new(
DVec2::ZERO,
tool_data.secondary_color,
DVec2::ONE,
tool_data.primary_color,
DAffine2::IDENTITY,
generate_uuid(),
tool_options.gradient_type,
);
let mut selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document).with_gradient_start(input.mouse.position);
selected_gradient.update_gradient(input.mouse.position, responses, false);
selected_gradient.update_gradient(input.mouse.position, responses, false, tool_options.gradient_type);

data.selected_gradient = Some(selected_gradient);

Expand All @@ -343,7 +401,7 @@ impl Fsm for GradientToolFsmState {
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
if let Some(selected_gradient) = &mut data.selected_gradient {
let mouse = data.snap_handler.snap_position(responses, document, input.mouse.position);
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize));
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type);
}
GradientToolFsmState::Drawing
}
Expand Down
Loading

0 comments on commit 860c4ad

Please sign in to comment.