diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index 46858c95..06d5efff 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -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}; @@ -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 { match fill { Fill::Solid(_) | Fill::None => Some(LayoutRow::Section { @@ -818,89 +906,10 @@ fn node_section_fill(fill: &Fill) -> Option { ], }], }), - 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)], + }), } } diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs index c0a0a25d..bb1ae97e 100644 --- a/editor/src/viewport_tools/tools/gradient_tool.rs +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -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; @@ -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}; @@ -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] @@ -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> for GradientTool { @@ -53,8 +71,14 @@ impl<'a> MessageHandler> 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; @@ -65,7 +89,31 @@ impl<'a> MessageHandler> 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 { @@ -199,7 +247,9 @@ impl SelectedGradient { self } - pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool) { + pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, 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) @@ -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()); } @@ -248,7 +298,7 @@ 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, @@ -256,7 +306,7 @@ impl Fsm for GradientToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, - _tool_options: &Self::ToolOptions, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -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() @@ -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); @@ -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 } diff --git a/graphene/src/layers/style/mod.rs b/graphene/src/layers/style/mod.rs index d306b617..1690d55b 100644 --- a/graphene/src/layers/style/mod.rs +++ b/graphene/src/layers/style/mod.rs @@ -36,6 +36,18 @@ impl Default for ViewMode { } } +#[derive(PartialEq, Clone, Copy, Debug, Hash, Serialize, Deserialize)] +pub enum GradientType { + Linear, + Radial, +} + +impl Default for GradientType { + fn default() -> Self { + GradientType::Linear + } +} + /// A gradient fill. /// /// Contains the start and end points, along with the colors at varying points along the length. @@ -47,16 +59,19 @@ pub struct Gradient { pub transform: DAffine2, pub positions: Vec<(f64, Option)>, uuid: u64, + pub gradient_type: GradientType, } + impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. - pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64) -> Self { + pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64, gradient_type: GradientType) -> Self { Gradient { start, end, positions: vec![(0., Some(start_color)), (1., Some(end_color))], transform, uuid, + gradient_type, } } @@ -86,23 +101,35 @@ impl Gradient { .map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," }) .collect::(); - let _ = write!( - svg_defs, - r#"{}"#, - self.uuid, start.x, end.x, start.y, end.y, transform, positions - ); + match self.gradient_type { + GradientType::Linear => { + let _ = write!( + svg_defs, + r#"{}"#, + self.uuid, start.x, end.x, start.y, end.y, transform, positions + ); + } + GradientType::Radial => { + let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt(); + let _ = write!( + svg_defs, + r#"{}"#, + self.uuid, start.x, start.y, radius, transform, positions + ); + } + } } } /// Describes the fill of a layer. /// -/// Can be None, a solid [Color], a linear [Gradient], or potentially some sort of image or pattern in the future +/// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future #[repr(C)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Fill { None, Solid(Color), - LinearGradient(Gradient), + Gradient(Gradient), } impl Default for Fill { @@ -117,13 +144,13 @@ impl Fill { Self::Solid(color) } - /// Evaluate the color at some point on the fill. Doesn't currently work for LinearGradient. + /// Evaluate the color at some point on the fill. Doesn't currently work for Gradient. pub fn color(&self) -> Color { match self { Self::None => Color::BLACK, Self::Solid(color) => *color, // TODO: Should correctly sample the gradient - Self::LinearGradient(Gradient { positions, .. }) => positions[0].1.unwrap_or(Color::BLACK), + Self::Gradient(Gradient { positions, .. }) => positions[0].1.unwrap_or(Color::BLACK), } } @@ -132,7 +159,7 @@ impl Fill { match self { Self::None => r#" fill="none""#.to_string(), Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())), - Self::LinearGradient(gradient) => { + Self::Gradient(gradient) => { gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds); format!(r##" fill="url('#{}')""##, gradient.uuid) }