Add fill and stroke color choices to the Pen tool options bar (#1188)

* Add basic layout - WIP

* Add color input min-width -> 80px

* First pass implementation - WIP

* Allow fill to be None

* Fix null Fill color

* refactor fill and stroke options into struct

* toolbar progress - WIP

* Switch is_working_color bool to PenColorType enum

* Add todo

* Add WorkingColorChanged event

* remove unused import

* Add WorkingColor[Primary/Secondary] icons

* Allow new strokes to have no color

* Set to base color when X is pressed (as per req)

* Improve icons for new UI layout design

* Add radio buttons

* Fix menu bar Edit12px -> Edit

* Add tooltips to radio buttons

* Make the color selector only on custom

* Fix edit icon correctly this time (whoops)

* Fix working colors icons

* Changes to improve the UX

* Remove lines obviated by Default::default()

* Make Eyedropper tool use working_color_changed event

* Fix tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Chase 2023-05-03 02:08:35 +08:00 committed by Keavon Chambers
parent cef1cf7587
commit 1aaf2a521b
35 changed files with 275 additions and 71 deletions

View file

@ -8,4 +8,5 @@ pub enum BroadcastEvent {
DocumentIsDirty,
ToolAbort,
SelectionChanged,
WorkingColorChanged,
}

View file

@ -1193,7 +1193,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
.widget_holder(),
WidgetHolder::unrelated_separator(),
CheckboxInput::new(!dimensions_is_auto || transform_not_connected)
.icon("Edit")
.icon("Edit12px")
.tooltip({
let message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64)";
let manual_message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64).\n\

View file

@ -159,7 +159,7 @@ impl OverlayRenderer {
let operation = Operation::AddShape {
path: layer_path.clone(),
subpath,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
@ -174,7 +174,7 @@ impl OverlayRenderer {
let operation = Operation::AddRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
@ -187,7 +187,7 @@ impl OverlayRenderer {
let operation = Operation::AddEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
@ -212,7 +212,7 @@ impl OverlayRenderer {
let operation = Operation::AddLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add_front(DocumentMessage::Overlays(operation.into()));
@ -327,8 +327,8 @@ impl OverlayRenderer {
/// Sets the overlay style for this point.
fn style_overlays(state: &SelectedShapeState, layer_path: &[LayerId], manipulator_group: &GraphiteManipulatorGroup, overlays: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
// TODO Move the style definitions out of the Subpath, should be looked up from a stylesheet or similar
let selected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
let selected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
let selected_shape_state = state.get(layer_path);
// Update if the manipulator points are shown as selected
// Here the index is important, even though overlays[..] has five elements we only care about the first three

View file

@ -46,7 +46,7 @@ impl PathOutline {
let operation = Operation::AddShape {
path: overlay_path.clone(),
subpath: Default::default(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};

View file

@ -112,7 +112,7 @@ impl Pivot {
path: layer_paths[0].clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(
Some(style::Stroke::new(COLOR_ACCENT, PIVOT_OUTER_OUTLINE_THICKNESS)),
Some(style::Stroke::new(Some(COLOR_ACCENT), PIVOT_OUTER_OUTLINE_THICKNESS)),
style::Fill::Solid(graphene_core::raster::color::Color::WHITE),
),
insert_index: -1,

View file

@ -34,7 +34,7 @@ impl SnapOverlays {
Operation::AddLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), style::Fill::None),
insert_index: -1,
}
} else {

View file

@ -156,7 +156,7 @@ pub fn add_bounding_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let operation = Operation::AddRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
@ -175,7 +175,7 @@ fn add_transform_handles(responses: &mut VecDeque<Message>) -> [Vec<LayerId>; 8]
let operation = Operation::AddRect {
path: current_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));

View file

@ -80,7 +80,7 @@ impl ToolTransition for ArtboardTool {
EventToMessageMap {
document_dirty: Some(ArtboardToolMessage::DocumentIsDirty.into()),
tool_abort: Some(ArtboardToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -164,9 +164,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
impl ToolTransition for BrushTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(BrushToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -73,9 +73,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Ellipse
impl ToolTransition for EllipseTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(EllipseToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -61,9 +61,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Eyedrop
impl ToolTransition for EyedropperTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(EyedropperToolMessage::Abort.into()),
selection_changed: None,
working_color_changed: Some(EyedropperToolMessage::PointerMove.into()),
..Default::default()
}
}
}

View file

@ -59,9 +59,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for FillToo
impl ToolTransition for FillTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(FillToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -73,9 +73,8 @@ impl ToolMetadata for FrameTool {
impl ToolTransition for FrameTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(FrameToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -116,9 +116,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Freehan
impl ToolTransition for FreehandTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(FreehandToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}
@ -224,6 +223,6 @@ fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path,
stroke: Stroke::new(tool_data.primary_color, data.weight),
stroke: Stroke::new(Some(tool_data.primary_color), data.weight),
});
}

View file

@ -160,7 +160,7 @@ impl GradientOverlay {
let operation = Operation::AddEllipse {
path: path.clone(),
transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(),
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), fill),
style: PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), fill),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
@ -179,7 +179,7 @@ impl GradientOverlay {
let operation = Operation::AddLine {
path: path.clone(),
transform,
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
style: PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
@ -371,6 +371,7 @@ impl ToolTransition for GradientTool {
document_dirty: Some(GradientToolMessage::DocumentIsDirty.into()),
tool_abort: Some(GradientToolMessage::Abort.into()),
selection_changed: Some(GradientToolMessage::DocumentIsDirty.into()),
..Default::default()
}
}
}

View file

@ -73,9 +73,8 @@ impl ToolMetadata for ImaginateTool {
impl ToolTransition for ImaginateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(ImaginateToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -105,9 +105,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for LineToo
impl ToolTransition for LineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(LineToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}
@ -165,7 +164,7 @@ impl Fsm for LineToolFsmState {
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path,
stroke: Stroke::new(global_tool_data.primary_color, tool_options.line_weight),
stroke: Stroke::new(Some(global_tool_data.primary_color), tool_options.line_weight),
});
tool_data.weight = tool_options.line_weight;

View file

@ -76,9 +76,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Navigat
impl ToolTransition for NavigateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(NavigateToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -99,6 +99,7 @@ impl ToolTransition for PathTool {
document_dirty: Some(PathToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PathToolMessage::Abort.into()),
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
..Default::default()
}
}
}

View file

@ -2,6 +2,8 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widget_prelude::{ColorInput, IconButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextLabel, WidgetHolder};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::portfolio::document::node_graph::VectorDataModification;
use crate::messages::prelude::*;
@ -14,7 +16,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use document_legacy::LayerId;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::Stroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::Color;
@ -28,13 +30,53 @@ pub struct PenTool {
options: PenOptions,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum PenColorType {
Primary,
Secondary,
Custom,
}
pub struct PenColorOptions {
color: Option<Color>,
primary_working_color: Option<Color>,
secondary_working_color: Option<Color>,
color_type: PenColorType,
}
impl PenColorOptions {
pub fn active_color(&self) -> Option<Color> {
match self.color_type {
PenColorType::Custom => self.color,
PenColorType::Primary => self.primary_working_color,
PenColorType::Secondary => self.secondary_working_color,
}
}
}
pub struct PenOptions {
line_weight: f64,
fill: PenColorOptions,
stroke: PenColorOptions,
}
impl Default for PenOptions {
fn default() -> Self {
Self { line_weight: 5. }
Self {
line_weight: 5.,
fill: PenColorOptions {
color: Some(Color::BLACK),
primary_working_color: Some(Color::BLACK),
secondary_working_color: Some(Color::WHITE),
color_type: PenColorType::Primary,
},
stroke: PenColorOptions {
color: None,
primary_working_color: Some(Color::BLACK),
secondary_working_color: Some(Color::WHITE),
color_type: PenColorType::Custom,
},
}
}
}
@ -49,6 +91,8 @@ pub enum PenToolMessage {
Abort,
#[remain::unsorted]
SelectionChanged,
#[remain::unsorted]
WorkingColorChanged,
// Tool-specific messages
Confirm,
@ -74,7 +118,13 @@ enum PenToolFsmState {
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum PenOptionsUpdate {
FillColor(Option<Color>),
FillColorType(PenColorType),
LineWeight(f64),
PrimaryColor(Option<Color>),
SecondaryColor(Option<Color>),
StrokeColor(Option<Color>),
StrokeColorType(PenColorType),
}
impl ToolMetadata for PenTool {
@ -89,15 +139,102 @@ impl ToolMetadata for PenTool {
}
}
// TODO: Generalize create_fill_widget and create_stroke_widget into one function.
fn create_fill_widget(fill: &PenColorOptions) -> Vec<WidgetHolder> {
let label = TextLabel::new("Fill").widget_holder();
let reset = IconButton::new("CloseX", 12)
.disabled(fill.color.is_none() && fill.color_type == PenColorType::Custom)
.on_update(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into())
.tooltip("Clear color")
.widget_holder();
let entries = vec![
("WorkingColorsPrimary", "Primary Working Color", PenColorType::Primary),
("WorkingColorsSecondary", "Secondary Working Color", PenColorType::Secondary),
("Edit", "Custom Color", PenColorType::Custom),
]
.into_iter()
.map(|(icon, tooltip, color_type)| {
RadioEntryData::new("")
.tooltip(tooltip)
.icon(icon)
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into())
})
.collect();
let radio = RadioInput::new(entries).selected_index(fill.color_type.clone() as u32).widget_holder();
let color_input = ColorInput::new(fill.active_color())
.on_update(|fill_color| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(fill_color.value)).into())
.widget_holder();
vec![
label,
WidgetHolder::related_separator(),
reset,
WidgetHolder::related_separator(),
radio,
WidgetHolder::related_separator(),
color_input,
]
}
fn create_stroke_widget(stroke: &PenColorOptions) -> Vec<WidgetHolder> {
let label = TextLabel::new("Stroke").widget_holder();
let reset = IconButton::new("CloseX", 12)
.disabled(stroke.color.is_none() && stroke.color_type == PenColorType::Custom)
.on_update(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into())
.tooltip("Clear color")
.widget_holder();
let entries = vec![
("WorkingColorsPrimary", "Primary Working Color", PenColorType::Primary),
("WorkingColorsSecondary", "Secondary Working Color", PenColorType::Secondary),
("Edit", "Custom Color", PenColorType::Custom),
]
.into_iter()
.map(|(icon, tooltip, color_type)| {
RadioEntryData::new("")
.tooltip(tooltip)
.icon(icon)
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into())
})
.collect();
let radio = RadioInput::new(entries).selected_index(stroke.color_type.clone() as u32).widget_holder();
let color_input = ColorInput::new(stroke.active_color())
.on_update(|stroke_color| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(stroke_color.value)).into())
.widget_holder();
vec![
label,
WidgetHolder::related_separator(),
reset,
WidgetHolder::related_separator(),
radio,
WidgetHolder::related_separator(),
color_input,
]
}
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
NumberInput::new(Some(line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.widget_holder()
}
impl PropertyHolder for PenTool {
fn properties(&self) -> Layout {
let weight = NumberInput::new(Some(self.options.line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
.widget_holder();
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![weight] }]))
let mut widgets = create_fill_widget(&self.options.fill);
widgets.push(Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder());
widgets.append(&mut create_stroke_widget(&self.options.stroke));
widgets.push(WidgetHolder::unrelated_separator());
widgets.push(create_weight_widget(self.options.line_weight));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
@ -106,7 +243,31 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
if let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message {
match action {
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
PenOptionsUpdate::FillColor(color) => {
self.options.fill.color = color;
self.options.fill.color_type = PenColorType::Custom;
}
PenOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
PenOptionsUpdate::StrokeColor(color) => {
self.options.stroke.color = color;
self.options.stroke.color_type = PenColorType::Custom;
}
PenOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
PenOptionsUpdate::PrimaryColor(color) => {
self.options.stroke.primary_working_color = color;
self.options.fill.primary_working_color = color;
}
PenOptionsUpdate::SecondaryColor(color) => {
self.options.stroke.secondary_working_color = color;
self.options.fill.secondary_working_color = color;
}
}
responses.add(LayoutMessage::SendLayout {
layout: self.properties(),
layout_target: LayoutTarget::ToolOptions,
});
return;
}
@ -139,6 +300,7 @@ impl ToolTransition for PenTool {
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
}
}
}
@ -177,7 +339,16 @@ impl PenToolData {
},
});
}
fn create_new_path(&mut self, document: &DocumentMessageHandler, line_weight: f64, color: Color, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
fn create_new_path(
&mut self,
document: &DocumentMessageHandler,
line_weight: f64,
stroke_color: Option<Color>,
fill_color: Option<Color>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
// Deselect layers because we are now creating a new layer
responses.add(DocumentMessage::DeselectAllLayers);
@ -192,18 +363,24 @@ impl PenToolData {
// Create the initial shape with a `bez_path` (only contains a moveto initially)
let subpath = bezier_rs::Subpath::new(vec![bezier_rs::ManipulatorGroup::new(start_position, Some(start_position), Some(start_position))], false);
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
responses.add(GraphOperationMessage::FillSet {
layer: layer_path.clone(),
fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path.clone(),
stroke: Stroke::new(color, line_weight),
stroke: Stroke::new(stroke_color, line_weight),
});
self.path = Some(layer_path);
self.from_start = false;
self.subpath_index = 0;
}
// TODO: tooltip / user documentation?
/// If you place the anchor on top of the previous anchor then you break the mirror
///
/// TODO: tooltip / user documentation?
fn check_break(&mut self, document: &DocumentMessageHandler, transform: DAffine2, shape_overlay: &mut OverlayRenderer, responses: &mut VecDeque<Message>) -> Option<()> {
// Get subpath
let layer_path = self.path.as_ref()?;
@ -523,6 +700,11 @@ impl Fsm for PenToolFsmState {
}
self
}
(_, PenToolMessage::WorkingColorChanged) => {
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::PrimaryColor(Some(global_tool_data.primary_color))));
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::SecondaryColor(Some(global_tool_data.secondary_color))));
self
}
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction);
@ -537,7 +719,14 @@ impl Fsm for PenToolFsmState {
if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.extend_subpath(layer, subpath_index, from_start, document, responses);
} else {
tool_data.create_new_path(document, tool_options.line_weight, global_tool_data.primary_color, input, responses);
tool_data.create_new_path(
document,
tool_options.line_weight,
tool_options.stroke.active_color(),
tool_options.fill.active_color(),
input,
responses,
);
}
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle

View file

@ -72,9 +72,8 @@ impl ToolMetadata for RectangleTool {
impl ToolTransition for RectangleTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(RectangleToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -287,6 +287,7 @@ impl ToolTransition for SelectTool {
document_dirty: Some(SelectToolMessage::DocumentIsDirty.into()),
tool_abort: Some(SelectToolMessage::Abort.into()),
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
..Default::default()
}
}
}

View file

@ -110,9 +110,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ShapeTo
impl ToolTransition for ShapeTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(ShapeToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}

View file

@ -122,9 +122,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SplineT
impl ToolTransition for SplineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(SplineToolMessage::Abort.into()),
selection_changed: None,
..Default::default()
}
}
}
@ -270,6 +269,6 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path.clone(),
stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight),
stroke: Stroke::new(Some(global_tool_data.primary_color), tool_data.weight),
});
}

View file

@ -172,6 +172,7 @@ impl ToolTransition for TextTool {
document_dirty: Some(TextToolMessage::DocumentIsDirty.into()),
tool_abort: Some(TextToolMessage::Abort.into()),
selection_changed: Some(TextToolMessage::DocumentIsDirty.into()),
..Default::default()
}
}
}
@ -370,7 +371,7 @@ fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Me
let operation = Operation::AddRect {
path,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));

View file

@ -1,6 +1,8 @@
use super::common_functionality::overlay_renderer::OverlayRenderer;
use super::common_functionality::shape_editor::ShapeState;
use super::tool_messages::*;
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
use crate::messages::broadcast::BroadcastMessage;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, LayoutKeysGroup, MouseMotion};
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
@ -154,7 +156,7 @@ impl DocumentToolData {
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 16,
icon: "ResetColors".into(),
icon: "WorkingColors".into(),
tooltip: "Reset".into(),
tooltip_shortcut: action_keys!(ToolMessageDiscriminant::ResetColors),
on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()),
@ -169,15 +171,16 @@ impl DocumentToolData {
layout_target: LayoutTarget::WorkingColors,
});
responses.add(EyedropperToolMessage::PointerMove);
responses.add(BroadcastMessage::TriggerEvent(BroadcastEvent::WorkingColorChanged));
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct EventToMessageMap {
pub document_dirty: Option<ToolMessage>,
pub selection_changed: Option<ToolMessage>,
pub tool_abort: Option<ToolMessage>,
pub working_color_changed: Option<ToolMessage>,
}
pub trait ToolTransition {
@ -197,6 +200,7 @@ pub trait ToolTransition {
subscribe_message(event_to_tool_map.document_dirty, BroadcastEvent::DocumentIsDirty);
subscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
subscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
subscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
}
fn deactivate(&self, responses: &mut VecDeque<Message>) {
@ -213,6 +217,7 @@ pub trait ToolTransition {
unsubscribe_message(event_to_tool_map.document_dirty, BroadcastEvent::DocumentIsDirty);
unsubscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
unsubscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
unsubscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
}
}

View file

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M15.2,2.3l-1.6-1.6C13.1,0.3,12.5,0,11.8,0c-0.7,0-1.4,0.3-1.9,0.8L2.2,8.5c-0.6,0.6-0.9,1.3-1.1,2L0,16l5.5-1.1c0.8-0.2,1.5-0.5,2-1.1l7.7-7.7C16.3,5.1,16.3,3.4,15.2,2.3z M6.6,12.9c-0.4,0.4-0.8,0.6-1.4,0.7l-0.1,0l-2.7-2.7l0-0.1c0.1-0.5,0.4-1,0.7-1.4l5.3-5.3l3.4,3.4L6.6,12.9z M14.3,5.2l-1.4,1.4L9.4,3.1l1.4-1.4c0.3-0.3,0.6-0.4,0.9-0.4s0.7,0.1,0.9,0.4l1.6,1.6C14.8,3.8,14.8,4.7,14.3,5.2z" />
</svg>

After

Width:  |  Height:  |  Size: 465 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M13,5c0-2.8-2.2-5-5-5S3,2.2,3,5c0,1.1,0.4,2.2,1,3c-0.6,0.8-1,1.9-1,3c0,2.8,2.2,5,5,5s5-2.2,5-5c0-1.1-0.4-2.2-1-3C12.6,7.2,13,6.1,13,5z M8,1.4c2,0,3.6,1.6,3.6,3.6S10,8.6,8,8.6S4.4,7,4.4,5S6,1.4,8,1.4z M2,5L0,7V3L2,5z M16,3v4l-2-2L16,3z" />
</svg>

After

Width:  |  Height:  |  Size: 317 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M13,5c0-2.8-2.2-5-5-5S3,2.2,3,5c0,1.1,0.4,2.2,1,3c-0.6,0.8-1,1.9-1,3c0,2.8,2.2,5,5,5s5-2.2,5-5c0-1.1-0.4-2.2-1-3C12.6,7.2,13,6.1,13,5z M8,1.4c2,0,3.6,1.6,3.6,3.6S10,8.6,8,8.6S4.4,7,4.4,5S6,1.4,8,1.4z M2,11l-2,2V9L2,11zM16,9v4l-2-2L16,9z" />
</svg>

After

Width:  |  Height:  |  Size: 319 B

View file

@ -56,6 +56,7 @@
border: 1px solid var(--color-5-dullgray);
border-radius: 2px;
padding: 1px;
min-width: 80px;
> button {
position: relative;

View file

@ -12,7 +12,7 @@ import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit from "@graphite-frontend/assets/icon-12px-solid/edit.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
@ -33,7 +33,6 @@ import KeyboardTab from "@graphite-frontend/assets/icon-12px-solid/keyboard-tab.
import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
import ResetColors from "@graphite-frontend/assets/icon-12px-solid/reset-colors.svg";
import Snapping from "@graphite-frontend/assets/icon-12px-solid/snapping.svg";
import Swap from "@graphite-frontend/assets/icon-12px-solid/swap.svg";
import VerticalEllipsis from "@graphite-frontend/assets/icon-12px-solid/vertical-ellipsis.svg";
@ -42,13 +41,14 @@ import WindowButtonWinClose from "@graphite-frontend/assets/icon-12px-solid/wind
import WindowButtonWinMaximize from "@graphite-frontend/assets/icon-12px-solid/window-button-win-maximize.svg";
import WindowButtonWinMinimize from "@graphite-frontend/assets/icon-12px-solid/window-button-win-minimize.svg";
import WindowButtonWinRestoreDown from "@graphite-frontend/assets/icon-12px-solid/window-button-win-restore-down.svg";
import WorkingColors from "@graphite-frontend/assets/icon-12px-solid/working-colors.svg";
const SOLID_12PX = {
Add: { svg: Add, size: 12 },
Checkmark: { svg: Checkmark, size: 12 },
CloseX: { svg: CloseX, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit: { svg: Edit, size: 12 },
Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 },
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
FullscreenExit: { svg: FullscreenExit, size: 12 },
@ -69,7 +69,6 @@ const SOLID_12PX = {
Link: { svg: Link, size: 12 },
Overlays: { svg: Overlays, size: 12 },
Remove: { svg: Remove, size: 12 },
ResetColors: { svg: ResetColors, size: 12 },
Snapping: { svg: Snapping, size: 12 },
Swap: { svg: Swap, size: 12 },
VerticalEllipsis: { svg: VerticalEllipsis, size: 12 },
@ -78,6 +77,7 @@ const SOLID_12PX = {
WindowButtonWinMaximize: { svg: WindowButtonWinMaximize, size: 12 },
WindowButtonWinMinimize: { svg: WindowButtonWinMinimize, size: 12 },
WindowButtonWinRestoreDown: { svg: WindowButtonWinRestoreDown, size: 12 },
WorkingColors: { svg: WorkingColors, size: 12 },
} as const;
// 16px Solid
@ -95,6 +95,7 @@ import BooleanUnion from "@graphite-frontend/assets/icon-16px-solid/boolean-unio
import CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg";
import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.svg";
import Copy from "@graphite-frontend/assets/icon-16px-solid/copy.svg";
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.svg";
import EyeHidden from "@graphite-frontend/assets/icon-16px-solid/eye-hidden.svg";
import EyeVisible from "@graphite-frontend/assets/icon-16px-solid/eye-visible.svg";
@ -133,6 +134,8 @@ import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-
import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg";
import WorkingColorsPrimary from "@graphite-frontend/assets/icon-16px-solid/working-colors-primary.svg";
import WorkingColorsSecondary from "@graphite-frontend/assets/icon-16px-solid/working-colors-secondary.svg";
import ZoomIn from "@graphite-frontend/assets/icon-16px-solid/zoom-in.svg";
import ZoomOut from "@graphite-frontend/assets/icon-16px-solid/zoom-out.svg";
import ZoomReset from "@graphite-frontend/assets/icon-16px-solid/zoom-reset.svg";
@ -152,6 +155,7 @@ const SOLID_16PX = {
CheckboxChecked: { svg: CheckboxChecked, size: 16 },
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
Copy: { svg: Copy, size: 16 },
Edit: { svg: Edit, size: 16 },
Eyedropper: { svg: Eyedropper, size: 16 },
EyeHidden: { svg: EyeHidden, size: 16 },
EyeVisible: { svg: EyeVisible, size: 16 },
@ -190,6 +194,8 @@ const SOLID_16PX = {
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
ZoomIn: { svg: ZoomIn, size: 16 },
ZoomOut: { svg: ZoomOut, size: 16 },
ZoomReset: { svg: ZoomReset, size: 16 },

View file

@ -281,9 +281,9 @@ impl core::hash::Hash for Stroke {
}
impl Stroke {
pub const fn new(color: Color, weight: f64) -> Self {
pub const fn new(color: Option<Color>, weight: f64) -> Self {
Self {
color: Some(color),
color,
weight,
dash_lengths: Vec::new(),
dash_offset: 0.,
@ -439,7 +439,7 @@ impl PathStyle {
/// ```
/// # use graphene_core::vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::raster::color::Color;
/// let stroke = Stroke::new(Color::GREEN, 42.);
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
///
/// assert_eq!(style.stroke(), Some(stroke));
@ -477,7 +477,7 @@ impl PathStyle {
///
/// assert_eq!(style.stroke(), None);
///
/// let stroke = Stroke::new(Color::GREEN, 42.);
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
/// style.set_stroke(stroke.clone());
///
/// assert_eq!(style.stroke(), Some(stroke));
@ -510,7 +510,7 @@ impl PathStyle {
/// ```
/// # use graphene_core::vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::raster::color::Color;
/// let mut style = PathStyle::new(Some(Stroke::new(Color::GREEN, 42.)), Fill::None);
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
///
/// assert!(style.stroke().is_some());
///
@ -528,7 +528,7 @@ impl PathStyle {
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds),
};
let stroke_attribute = match (view_mode, &self.stroke) {
(ViewMode::Outline, _) => Stroke::new(LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT).render(),
(ViewMode::Outline, _) => Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(),
(_, Some(stroke)) => stroke.render(),
(_, None) => String::new(),
};

View file

@ -23,7 +23,7 @@ impl VectorData {
Self {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Color::BLACK, 0.)), super::style::Fill::None),
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
mirror_angle: Vec::new(),
}
}