Restructure the entire editor codebase to consistently match the message hierarchy

Closes #744
This commit is contained in:
Keavon Chambers
2022-08-05 17:13:43 -07:00
parent 9e1f4eb46d
commit ddfd7db0a0
154 changed files with 4199 additions and 4064 deletions
@@ -0,0 +1,465 @@
use crate::application::generate_uuid;
use crate::consts::SELECTION_TOLERANCE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::LayerId;
use glam::{DVec2, Vec2Swizzles};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct ArtboardTool {
fsm_state: ArtboardToolFsmState,
data: ArtboardToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Artboard)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ArtboardToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
// Tool-specific messages
DeleteSelected,
PointerDown,
PointerMove {
constrain_axis_or_aspect: Key,
center: Key,
},
PointerUp,
}
impl ToolMetadata for ArtboardTool {
fn icon_name(&self) -> String {
"GeneralArtboardTool".into()
}
fn tooltip(&self) -> String {
"Artboard Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Artboard
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ArtboardTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
}
}
advertise_actions!(ArtboardToolMessageDiscriminant;
PointerDown,
PointerUp,
PointerMove,
DeleteSelected,
Abort,
);
}
impl PropertyHolder for ArtboardTool {}
impl ToolTransition for ArtboardTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(ArtboardToolMessage::DocumentIsDirty.into()),
tool_abort: Some(ArtboardToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ArtboardToolFsmState {
Ready,
Drawing,
ResizingBounds,
Dragging,
}
impl Default for ArtboardToolFsmState {
fn default() -> Self {
ArtboardToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct ArtboardToolData {
bounding_box_overlays: Option<BoundingBoxOverlays>,
selected_board: Option<LayerId>,
snap_manager: SnapManager,
cursor: MouseCursorIcon,
drag_start: DVec2,
drag_current: DVec2,
}
impl Fsm for ArtboardToolFsmState {
type ToolData = ArtboardToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Artboard(event) = event {
match (self, event) {
(ArtboardToolFsmState::Ready | ArtboardToolFsmState::ResizingBounds | ArtboardToolFsmState::Dragging, ArtboardToolMessage::DocumentIsDirty) => {
match (
tool_data.selected_board.map(|path| document.artboard_bounding_box_and_transform(&[path], font_cache)).unwrap_or(None),
tool_data.bounding_box_overlays.take(),
) {
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
(Some((bounds, transform)), paths) => {
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
bounding_box_overlays.bounds = bounds;
bounding_box_overlays.transform = transform;
bounding_box_overlays.transform(responses);
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
responses.push_back(OverlaysMessage::Rerender.into());
responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: vec![vec![tool_data.selected_board.unwrap()]],
document: TargetDocument::Artboard,
}
.into(),
);
}
_ => {}
};
self
}
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerDown) => {
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
let dragging_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
bounding_box.pivot = edges.calculate_pivot();
edges
});
edges
} else {
None
};
if let Some(selected_edges) = dragging_bounds {
let snap_x = selected_edges.2 || selected_edges.3;
let snap_y = selected_edges.0 || selected_edges.1;
tool_data
.snap_manager
.start_snap(document, document.bounding_boxes(None, Some(tool_data.selected_board.unwrap()), font_cache), snap_x, snap_y);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
ArtboardToolFsmState::ResizingBounds
} else {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
let intersection = document.artboard_message_handler.artboards_graphene_document.intersects_quad_root(quad, font_cache);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
if let Some(intersection) = intersection.last() {
tool_data.selected_board = Some(intersection[0]);
tool_data
.snap_manager
.start_snap(document, document.bounding_boxes(None, Some(intersection[0]), font_cache), true, true);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: vec![intersection.clone()],
document: TargetDocument::Artboard,
}
.into(),
);
ArtboardToolFsmState::Dragging
} else {
let id = generate_uuid();
tool_data.selected_board = Some(id);
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, Some(id), font_cache), true, true);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
responses.push_back(
ArtboardMessage::AddArtboard {
id: Some(id),
position: (0., 0.),
size: (0., 0.),
}
.into(),
);
responses.push_back(PropertiesPanelMessage::ClearSelection.into());
ArtboardToolFsmState::Drawing
}
}
}
(ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
if let Some(bounds) = &tool_data.bounding_box_overlays {
if let Some(movement) = &bounds.selected_edges {
let from_center = input.keyboard.get(center as usize);
let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize);
let mouse_position = input.mouse.position;
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
let (mut position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square);
if from_center {
position = movement.center_position(position, size);
}
responses.push_back(
ArtboardMessage::ResizeArtboard {
artboard: tool_data.selected_board.unwrap(),
position: position.round().into(),
size: size.round().into(),
}
.into(),
);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
}
ArtboardToolFsmState::ResizingBounds
}
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, .. }) => {
if let Some(bounds) = &tool_data.bounding_box_overlays {
let axis_align = input.keyboard.get(constrain_axis_or_aspect as usize);
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
let mouse_delta = mouse_position - tool_data.drag_current;
let snap = bounds.evaluate_transform_handle_positions().into_iter().collect();
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
let size = bounds.bounds[1] - bounds.bounds[0];
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_position - tool_data.drag_current + closest_move);
responses.push_back(
ArtboardMessage::ResizeArtboard {
artboard: tool_data.selected_board.unwrap(),
position: position.round().into(),
size: size.round().into(),
}
.into(),
);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
tool_data.drag_current = mouse_position + closest_move;
}
ArtboardToolFsmState::Dragging
}
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
let mouse_position = input.mouse.position;
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
let root_transform = document.graphene_document.root.transform.inverse();
let mut start = tool_data.drag_start;
let mut size = snapped_mouse_position - start;
// Constrain axis
if input.keyboard.get(constrain_axis_or_aspect as usize) {
size = size.abs().max(size.abs().yx()) * size.signum();
}
// From center
if input.keyboard.get(center as usize) {
start -= size;
size *= 2.;
}
let start = root_transform.transform_point2(start);
let size = root_transform.transform_vector2(size);
responses.push_back(
ArtboardMessage::ResizeArtboard {
artboard: tool_data.selected_board.unwrap(),
position: start.round().into(),
size: size.round().into(),
}
.into(),
);
// Have to put message here instead of when Artboard is created
// This might result in a few more calls but it is not reliant on the order of messages
responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: vec![vec![tool_data.selected_board.unwrap()]],
document: TargetDocument::Artboard,
}
.into(),
);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
ArtboardToolFsmState::Drawing
}
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerMove { .. }) => {
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
if tool_data.cursor != cursor {
tool_data.cursor = cursor;
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
}
ArtboardToolFsmState::Ready
}
(ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
bounds.original_transforms.clear();
}
ArtboardToolFsmState::Ready
}
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
bounds.original_transforms.clear();
}
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
ArtboardToolFsmState::Ready
}
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
bounds.original_transforms.clear();
}
ArtboardToolFsmState::Ready
}
(_, ArtboardToolMessage::DeleteSelected) => {
if let Some(artboard) = tool_data.selected_board.take() {
responses.push_back(ArtboardMessage::DeleteArtboard { artboard }.into());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
ArtboardToolFsmState::Ready
}
(_, ArtboardToolMessage::Abort) => {
if let Some(bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
bounding_box_overlays.delete(responses);
}
// Register properties when switching back to other tools
responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: document.selected_layers().map(|path| path.to_vec()).collect(),
document: TargetDocument::Artwork,
}
.into(),
);
tool_data.snap_manager.cleanup(responses);
ArtboardToolFsmState::Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
ArtboardToolFsmState::Ready => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Artboard"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Move Artboard"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyBackspace])],
key_groups_mac: None,
mouse: None,
label: String::from("Delete Artboard"),
plus: false,
}]),
]),
ArtboardToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain to Axis"),
plus: false,
}])]),
ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,232 @@
use crate::consts::DRAG_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct EllipseTool {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EllipseToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize {
center: Key,
lock_ratio: Key,
},
}
impl ToolMetadata for EllipseTool {
fn icon_name(&self) -> String {
"VectorEllipseTool".into()
}
fn tooltip(&self) -> String {
"Ellipse Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Ellipse
}
}
impl PropertyHolder for EllipseTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EllipseTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use EllipseToolFsmState::*;
match self.fsm_state {
Ready => actions!(EllipseToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(EllipseToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolTransition for EllipseTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(EllipseToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
Drawing,
}
impl Default for EllipseToolFsmState {
fn default() -> Self {
EllipseToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
data: Resize,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use EllipseToolFsmState::*;
use EllipseToolMessage::*;
let mut shape_data = &mut tool_data.data;
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(responses, document, input.mouse.position, font_cache);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(document.get_path_for_new_layer());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddEllipse {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup(responses);
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup(responses);
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Ellipse"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Circular"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Circular"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
@@ -0,0 +1,171 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct EyedropperTool {
fsm_state: EyedropperToolFsmState,
data: EyedropperToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EyedropperToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
LeftMouseDown,
RightMouseDown,
}
impl ToolMetadata for EyedropperTool {
fn icon_name(&self) -> String {
"GeneralEyedropperTool".into()
}
fn tooltip(&self) -> String {
"Eyedropper Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Eyedropper
}
}
impl PropertyHolder for EyedropperTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EyedropperTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
advertise_actions!(EyedropperToolMessageDiscriminant;
LeftMouseDown,
RightMouseDown,
);
}
impl ToolTransition for EyedropperTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(EyedropperToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EyedropperToolFsmState {
Ready,
}
impl Default for EyedropperToolFsmState {
fn default() -> Self {
EyedropperToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EyedropperToolData {}
impl Fsm for EyedropperToolFsmState {
type ToolData = EyedropperToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
_tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use EyedropperToolFsmState::*;
use EyedropperToolMessage::*;
if let ToolMessage::Eyedropper(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
// TODO: Destroy this pyramid
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
if let Ok(layer) = document.graphene_document.layer(path) {
if let LayerDataType::Shape(shape) = &layer.data {
if shape.style.fill().is_some() {
match lmb_or_rmb {
EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: shape.style.fill().color() }.into()),
EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: shape.style.fill().color() }.into()),
_ => {}
}
}
}
}
}
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Sample to Primary"),
plus: false,
},
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Rmb),
label: String::from("Sample to Secondary"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,170 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::Operation;
use glam::DVec2;
use graphene::layers::style::Fill;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct FillTool {
fsm_state: FillToolFsmState,
data: FillToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum FillToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
LeftMouseDown,
RightMouseDown,
}
impl ToolMetadata for FillTool {
fn icon_name(&self) -> String {
"GeneralFillTool".into()
}
fn tooltip(&self) -> String {
"Fill Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Fill
}
}
impl PropertyHolder for FillTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FillTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
advertise_actions!(FillToolMessageDiscriminant;
LeftMouseDown,
RightMouseDown,
);
}
impl ToolTransition for FillTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(FillToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FillToolFsmState {
Ready,
}
impl Default for FillToolFsmState {
fn default() -> Self {
FillToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct FillToolData {}
impl Fsm for FillToolFsmState {
type ToolData = FillToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
_tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use FillToolFsmState::*;
use FillToolMessage::*;
if let ToolMessage::Fill(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
let color = match lmb_or_rmb {
LeftMouseDown => global_tool_data.primary_color,
RightMouseDown => global_tool_data.secondary_color,
Abort => unreachable!(),
};
let fill = Fill::Solid(color);
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(Operation::SetLayerFill { path: path.to_vec(), fill }.into());
responses.push_back(DocumentMessage::CommitTransaction.into());
}
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
FillToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Fill with Primary"),
plus: false,
},
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Rmb),
label: String::from("Fill with Secondary"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,258 @@
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct FreehandTool {
fsm_state: FreehandToolFsmState,
data: FreehandToolData,
options: FreehandOptions,
}
pub struct FreehandOptions {
line_weight: f64,
}
impl Default for FreehandOptions {
fn default() -> Self {
Self { line_weight: 5. }
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Freehand)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FreehandToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
PointerMove,
UpdateOptions(FreehandToolMessageOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FreehandToolMessageOptionsUpdate {
LineWeight(f64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FreehandToolFsmState {
Ready,
Drawing,
}
impl ToolMetadata for FreehandTool {
fn icon_name(&self) -> String {
"VectorFreehandTool".into()
}
fn tooltip(&self) -> String {
"Freehand Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Freehand
}
}
impl PropertyHolder for FreehandTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: Some(self.options.line_weight as f64),
is_integer: false,
min: Some(1.),
on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
..NumberInput::default()
}))],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FreehandTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(action)) = message {
match action {
FreehandToolMessageOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use FreehandToolFsmState::*;
match self.fsm_state {
Ready => actions!(FreehandToolMessageDiscriminant;
DragStart,
DragStop,
Abort,
),
Drawing => actions!(FreehandToolMessageDiscriminant;
DragStop,
PointerMove,
Abort,
),
}
}
}
impl ToolTransition for FreehandTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(FreehandToolMessage::Abort.into()),
selection_changed: None,
}
}
}
impl Default for FreehandToolFsmState {
fn default() -> Self {
FreehandToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct FreehandToolData {
points: Vec<DVec2>,
weight: f64,
path: Option<Vec<LayerId>>,
}
impl Fsm for FreehandToolFsmState {
type ToolData = FreehandToolData;
type ToolOptions = FreehandOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, _font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use FreehandToolFsmState::*;
use FreehandToolMessage::*;
let transform = document.graphene_document.root.transform;
if let ToolMessage::Freehand(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.path = Some(document.get_path_for_new_layer());
let pos = transform.inverse().transform_point2(input.mouse.position);
tool_data.points.push(pos);
tool_data.weight = tool_options.line_weight;
responses.push_back(add_polyline(tool_data, global_tool_data));
Drawing
}
(Drawing, PointerMove) => {
let pos = transform.inverse().transform_point2(input.mouse.position);
if tool_data.points.last() != Some(&pos) {
tool_data.points.push(pos);
}
responses.push_back(remove_preview(tool_data));
responses.push_back(add_polyline(tool_data, global_tool_data));
Drawing
}
(Drawing, DragStop) | (Drawing, Abort) => {
if tool_data.points.len() >= 2 {
responses.push_back(remove_preview(tool_data));
responses.push_back(add_polyline(tool_data, global_tool_data));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
tool_data.path = None;
tool_data.points.clear();
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Polyline"),
plus: false,
}])]),
FreehandToolFsmState::Drawing => HintData(vec![]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
fn remove_preview(data: &FreehandToolData) -> Message {
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
}
fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData) -> Message {
let points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect();
Operation::AddPolyline {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight)), style::Fill::None),
}
.into()
}
@@ -0,0 +1,494 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_TOLERANCE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::{RadioEntryData, RadioInput};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::color::Color;
use graphene::intersection::Quad;
use graphene::layers::layer_info::Layer;
use graphene::layers::style::{Fill, Gradient, GradientType, PathStyle, Stroke};
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use graphene::layers::text_layer::FontCache;
use serde::{Deserialize, Serialize};
#[derive(Default)]
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]
#[impl_message(Message, ToolMessage, Gradient)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum GradientToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
// Tool-specific messages
PointerDown,
PointerMove {
constrain_axis: Key,
},
PointerUp,
UpdateOptions(GradientOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum GradientOptionsUpdate {
Type(GradientType),
}
impl ToolMetadata for GradientTool {
fn icon_name(&self) -> String {
"GeneralGradientTool".into()
}
fn tooltip(&self) -> String {
"Gradient Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Gradient
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool {
fn process_message(&mut self, message: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = message {
match action {
GradientOptionsUpdate::Type(gradient_type) => self.options.gradient_type = gradient_type,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.data, data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
}
}
advertise_actions!(GradientToolMessageDiscriminant;
PointerDown,
PointerUp,
PointerMove,
Abort,
);
}
impl PropertyHolder for GradientTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::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 {
Ready,
Drawing,
}
impl Default for GradientToolFsmState {
fn default() -> Self {
GradientToolFsmState::Ready
}
}
/// Computes the transform from gradient space to layer space (where gradient space is 0..1 in layer space)
fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, font_cache: &FontCache) -> DAffine2 {
let bounds = layer.aabb_for_transform(DAffine2::IDENTITY, font_cache).unwrap();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let multiplied = document.graphene_document.multiply_transforms(path).unwrap();
multiplied * bound_transform
}
/// Contains info on the overlays for a single gradient
#[derive(Clone, Debug, Default)]
pub struct GradientOverlay {
pub handles: [Vec<LayerId>; 2],
pub line: Vec<LayerId>,
path: Vec<LayerId>,
transform: DAffine2,
gradient: Gradient,
}
impl GradientOverlay {
fn generate_overlay_handle(translation: DVec2, responses: &mut VecDeque<Message>, selected: bool) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let size = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) };
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),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
path
}
fn generate_overlay_line(start: DVec2, end: DVec2, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let line_vector = end - start;
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = start;
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let operation = Operation::AddLine {
path: path.clone(),
transform,
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
path
}
pub fn new(fill: &Gradient, dragging_start: Option<bool>, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) -> Self {
let transform = gradient_space_transform(path, layer, document, font_cache);
let Gradient { start, end, .. } = fill;
let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)];
let line = Self::generate_overlay_line(start, end, responses);
let handles = [
Self::generate_overlay_handle(start, responses, dragging_start == Some(true)),
Self::generate_overlay_handle(end, responses, dragging_start == Some(false)),
];
let path = path.to_vec();
let gradient = fill.clone();
Self {
handles,
line,
path,
transform,
gradient,
}
}
pub fn delete_overlays(self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.line }.into()).into());
let [start, end] = self.handles;
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()).into());
}
pub fn evaluate_gradient_start(&self) -> DVec2 {
self.transform.transform_point2(self.gradient.start)
}
pub fn evaluate_gradient_end(&self) -> DVec2 {
self.transform.transform_point2(self.gradient.end)
}
}
/// Contains information about the selected gradient handle
#[derive(Clone, Debug, Default)]
struct SelectedGradient {
path: Vec<LayerId>,
transform: DAffine2,
gradient: Gradient,
dragging_start: bool,
}
impl SelectedGradient {
pub fn new(gradient: Gradient, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, font_cache: &FontCache) -> Self {
let transform = gradient_space_transform(path, layer, document, font_cache);
Self {
path: path.to_vec(),
transform,
gradient,
dragging_start: false,
}
}
pub fn with_gradient_start(mut self, start: DVec2) -> Self {
self.gradient.start = self.transform.inverse().transform_point2(start);
self
}
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)
} else {
self.transform.transform_point2(self.gradient.start)
};
let delta = point - mouse;
let length = delta.length();
let mut angle = -delta.angle_between(DVec2::X);
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
mouse = point - rotated;
}
mouse = self.transform.inverse().transform_point2(mouse);
if self.dragging_start {
self.gradient.start = mouse;
} else {
self.gradient.end = mouse;
}
self.gradient.transform = self.transform;
let fill = Fill::Gradient(self.gradient.clone());
let path = self.path.clone();
responses.push_back(Operation::SetLayerFill { path, fill }.into());
}
}
impl ToolTransition for GradientTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(GradientToolMessage::DocumentIsDirty.into()),
tool_abort: Some(GradientToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Debug, Default)]
struct GradientToolData {
gradient_overlays: Vec<GradientOverlay>,
selected_gradient: Option<SelectedGradient>,
snap_manager: SnapManager,
}
pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, font_cache: &FontCache) {
snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
snap_manager.add_all_document_handles(document, &[], &[], &[]);
}
impl Fsm for GradientToolFsmState {
type ToolData = GradientToolData;
type ToolOptions = GradientOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Gradient(event) = event {
match (self, event) {
(_, GradientToolMessage::DocumentIsDirty) => {
while let Some(overlay) = tool_data.gradient_overlays.pop() {
overlay.delete_overlays(responses);
}
for path in document.selected_visible_layers() {
let layer = document.graphene_document.layer(path).unwrap();
if let Ok(Fill::Gradient(gradient)) = layer.style().map(|style| style.fill()) {
let dragging_start = tool_data
.selected_gradient
.as_ref()
.and_then(|selected| if selected.path == path { Some(selected.dragging_start) } else { None });
tool_data
.gradient_overlays
.push(GradientOverlay::new(gradient, dragging_start, path, layer, document, responses, font_cache))
}
}
self
}
(GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => {
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
let mouse = input.mouse.position;
let tolerance = MANIPULATOR_GROUP_MARKER_SIZE.powi(2);
let mut dragging = false;
for overlay in &tool_data.gradient_overlays {
if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance {
dragging = true;
start_snap(&mut tool_data.snap_manager, document, font_cache);
tool_data.selected_gradient = Some(SelectedGradient {
path: overlay.path.clone(),
transform: overlay.transform,
gradient: overlay.gradient.clone(),
dragging_start: true,
})
}
if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance {
dragging = true;
start_snap(&mut tool_data.snap_manager, document, font_cache);
tool_data.selected_gradient = Some(SelectedGradient {
path: overlay.path.clone(),
transform: overlay.transform,
gradient: overlay.gradient.clone(),
dragging_start: false,
})
}
}
if dragging {
GradientToolFsmState::Drawing
} else {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
let intersection = document.graphene_document.intersects_quad_root(quad, font_cache).pop();
if let Some(intersection) = intersection {
if !document.selected_layers_contains(&intersection) {
let replacement_selected_layers = vec![intersection.clone()];
responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into());
}
let layer = document.graphene_document.layer(&intersection).unwrap();
let gradient = Gradient::new(
DVec2::ZERO,
global_tool_data.secondary_color,
DVec2::ONE,
global_tool_data.primary_color,
DAffine2::IDENTITY,
generate_uuid(),
tool_options.gradient_type,
);
let mut selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document, font_cache).with_gradient_start(input.mouse.position);
selected_gradient.update_gradient(input.mouse.position, responses, false, tool_options.gradient_type);
tool_data.selected_gradient = Some(selected_gradient);
start_snap(&mut tool_data.snap_manager, document, font_cache);
GradientToolFsmState::Drawing
} else {
GradientToolFsmState::Ready
}
}
}
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
if let Some(selected_gradient) = &mut tool_data.selected_gradient {
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type);
}
GradientToolFsmState::Drawing
}
(GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
GradientToolFsmState::Ready
}
(_, GradientToolMessage::Abort) => {
tool_data.snap_manager.cleanup(responses);
while let Some(overlay) = tool_data.gradient_overlays.pop() {
overlay.delete_overlays(responses);
}
GradientToolFsmState::Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Gradient"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: true,
},
])]),
GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,340 @@
use crate::consts::{DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct LineTool {
fsm_state: LineToolFsmState,
tool_data: LineToolData,
options: LineOptions,
}
pub struct LineOptions {
line_weight: f64,
}
impl Default for LineOptions {
fn default() -> Self {
Self { line_weight: 5. }
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum LineToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Redraw {
center: Key,
lock_angle: Key,
snap_angle: Key,
},
UpdateOptions(LineOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum LineOptionsUpdate {
LineWeight(f64),
}
impl ToolMetadata for LineTool {
fn icon_name(&self) -> String {
"VectorLineTool".into()
}
fn tooltip(&self) -> String {
"Line Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Line
}
}
impl PropertyHolder for LineTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: Some(self.options.line_weight as f64),
is_integer: false,
min: Some(0.),
on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
..NumberInput::default()
}))],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for LineTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message {
match action {
LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use LineToolFsmState::*;
match self.fsm_state {
Ready => actions!(LineToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(LineToolMessageDiscriminant;
DragStop,
Redraw,
Abort,
),
}
}
}
impl ToolTransition for LineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(LineToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineToolFsmState {
Ready,
Drawing,
}
impl Default for LineToolFsmState {
fn default() -> Self {
LineToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct LineToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
angle: f64,
weight: f64,
path: Option<Vec<LayerId>>,
snap_manager: SnapManager,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
type ToolOptions = LineOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use LineToolFsmState::*;
use LineToolMessage::*;
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
tool_data.drag_start = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.path = Some(document.get_path_for_new_layer());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.weight = tool_options.line_weight;
responses.push_back(
Operation::AddLine {
path: tool_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
}
.into(),
);
Drawing
}
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
tool_data.drag_current = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
responses.push_back(generate_transform(tool_data, values[0], values[1], values[2]));
Drawing
}
(Drawing, DragStop) => {
tool_data.drag_current = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
tool_data.snap_manager.cleanup(responses);
match tool_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
tool_data.path = None;
Ready
}
(Drawing, Abort) => {
tool_data.snap_manager.cleanup(responses);
responses.push_back(DocumentMessage::AbortTransaction.into());
tool_data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Line"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Lock Angle"),
plus: true,
},
])]),
LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Lock Angle"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
fn generate_transform(tool_data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
let mut start = tool_data.drag_start;
let stop = tool_data.drag_current;
let dir = stop - start;
let mut angle = -dir.angle_between(DVec2::X);
if lock {
angle = tool_data.angle
};
if snap {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
tool_data.angle = angle;
let mut scale = dir.length();
if lock {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
scale = dir.dot(angle_vec);
}
if center {
start -= scale * DVec2::new(angle.cos(), angle.sin());
scale *= 2.;
}
Operation::SetLayerTransformInViewport {
path: tool_data.path.clone().unwrap(),
transform: glam::DAffine2::from_scale_angle_translation(DVec2::new(scale, 1.), angle, start).to_cols_array(),
}
.into()
}
@@ -0,0 +1,15 @@
pub mod artboard_tool;
pub mod ellipse_tool;
pub mod eyedropper_tool;
pub mod fill_tool;
pub mod freehand_tool;
pub mod gradient_tool;
pub mod line_tool;
pub mod navigate_tool;
pub mod path_tool;
pub mod pen_tool;
pub mod rectangle_tool;
pub mod select_tool;
pub mod shape_tool;
pub mod spline_tool;
pub mod text_tool;
@@ -0,0 +1,281 @@
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct NavigateTool {
fsm_state: NavigateToolFsmState,
tool_data: NavigateToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum NavigateToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
ClickZoom {
zoom_in: bool,
},
PointerMove {
snap_angle: Key,
snap_zoom: Key,
},
RotateCanvasBegin,
TransformCanvasEnd,
TranslateCanvasBegin,
ZoomCanvasBegin,
}
impl ToolMetadata for NavigateTool {
fn icon_name(&self) -> String {
"GeneralNavigateTool".into()
}
fn tooltip(&self) -> String {
"Navigate Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Navigate
}
}
impl PropertyHolder for NavigateTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for NavigateTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use NavigateToolFsmState::*;
match self.fsm_state {
Ready => actions!(NavigateToolMessageDiscriminant;
TranslateCanvasBegin,
RotateCanvasBegin,
ZoomCanvasBegin,
),
_ => actions!(NavigateToolMessageDiscriminant;
ClickZoom,
PointerMove,
TransformCanvasEnd,
),
}
}
}
impl ToolTransition for NavigateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(NavigateToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NavigateToolFsmState {
Ready,
Panning,
Tilting,
Zooming,
}
impl Default for NavigateToolFsmState {
fn default() -> Self {
NavigateToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct NavigateToolData {
drag_start: DVec2,
}
impl Fsm for NavigateToolFsmState {
type ToolData = NavigateToolData;
type ToolOptions = ();
fn transition(
self,
message: ToolMessage,
tool_data: &mut Self::ToolData,
(_document, _global_tool_data, input, _font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
messages: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Navigate(navigate) = message {
use NavigateToolMessage::*;
match navigate {
ClickZoom { zoom_in } => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
// Mouse has not moved from pointerdown to pointerup
if tool_data.drag_start == input.mouse.position {
messages.push_front(if zoom_in {
MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into()
} else {
MovementMessage::DecreaseCanvasZoom { center_on_mouse: true }.into()
});
}
NavigateToolFsmState::Ready
}
PointerMove { snap_angle, snap_zoom } => {
messages.push_front(
MovementMessage::PointerMove {
snap_angle,
wait_for_snap_angle_release: false,
snap_zoom,
zoom_from_viewport: Some(tool_data.drag_start),
}
.into(),
);
self
}
TranslateCanvasBegin => {
tool_data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::TranslateCanvasBegin.into());
NavigateToolFsmState::Panning
}
RotateCanvasBegin => {
tool_data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::RotateCanvasBegin.into());
NavigateToolFsmState::Tilting
}
ZoomCanvasBegin => {
tool_data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::ZoomCanvasBegin.into());
NavigateToolFsmState::Zooming
}
TransformCanvasEnd => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
NavigateToolFsmState::Ready
}
Abort => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
NavigateToolFsmState::Ready
}
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
NavigateToolFsmState::Ready => HintData(vec![
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Zoom In"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Zoom Out"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Zoom"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap Increments"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::MmbDrag),
label: String::from("Pan"),
plus: false,
}]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::RmbDrag),
label: String::from("Tilt"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: true,
},
]),
]),
NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}])]),
NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap Increments"),
plus: false,
}])]),
_ => HintData(Vec::new()),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
let cursor = match *self {
NavigateToolFsmState::Ready => MouseCursorIcon::ZoomIn,
NavigateToolFsmState::Panning => MouseCursorIcon::Grabbing,
NavigateToolFsmState::Tilting => MouseCursorIcon::Default,
NavigateToolFsmState::Zooming => MouseCursorIcon::ZoomIn,
};
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
}
}
@@ -0,0 +1,398 @@
use crate::consts::SELECTION_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer;
use crate::messages::tool::common_functionality::shape_editor::ShapeEditor;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::layers::vector::consts::ManipulatorType;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct PathTool {
fsm_state: PathToolFsmState,
tool_data: PathToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PathToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
Delete,
DragStart {
add_to_selection: Key,
},
DragStop,
PointerMove {
alt_mirror_angle: Key,
shift_mirror_distance: Key,
},
}
impl ToolMetadata for PathTool {
fn icon_name(&self) -> String {
"VectorPathTool".into()
}
fn tooltip(&self) -> String {
"Path Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Path
}
}
impl PropertyHolder for PathTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PathTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
// Different actions depending on state may be wanted:
fn actions(&self) -> ActionList {
use PathToolFsmState::*;
match self.fsm_state {
Ready => actions!(PathToolMessageDiscriminant;
DragStart,
Delete,
),
Dragging => actions!(PathToolMessageDiscriminant;
DragStop,
PointerMove,
Delete,
),
}
}
}
impl ToolTransition for PathTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(PathToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PathToolMessage::Abort.into()),
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PathToolFsmState {
Ready,
Dragging,
}
impl Default for PathToolFsmState {
fn default() -> Self {
PathToolFsmState::Ready
}
}
#[derive(Default)]
struct PathToolData {
shape_editor: ShapeEditor,
overlay_renderer: OverlayRenderer,
snap_manager: SnapManager,
drag_start_pos: DVec2,
alt_debounce: bool,
shift_debounce: bool,
}
impl Fsm for PathToolFsmState {
type ToolData = PathToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Path(event) = event {
use PathToolFsmState::*;
use PathToolMessage::*;
match (self, event) {
(_, SelectionChanged) => {
// Set the previously selected layers to invisible
for layer_path in document.all_layers() {
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
}
// Set the newly targeted layers to visible
let layer_paths = document.selected_visible_layers().map(|layer_path| layer_path.to_vec()).collect();
tool_data.shape_editor.set_selected_layers(layer_paths);
// Render the new overlays
for layer_path in tool_data.shape_editor.selected_layers() {
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
// This can happen in any state (which is why we return self)
self
}
(_, DocumentIsDirty) => {
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer_path in document.selected_visible_layers() {
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
// Mouse down
(_, DragStart { add_to_selection }) => {
let toggle_add_to_selection = input.keyboard.get(add_to_selection as usize);
// Select the first point within the threshold (in pixels)
if let Some(mut new_selected) = tool_data
.shape_editor
.select_point(&document.graphene_document, input.mouse.position, SELECTION_THRESHOLD, toggle_add_to_selection, responses)
{
responses.push_back(DocumentMessage::StartTransaction.into());
let ignore_document = tool_data.shape_editor.selected_layers().clone();
tool_data
.snap_manager
.start_snap(document, document.bounding_boxes(Some(&ignore_document), None, font_cache), true, true);
// Do not snap against handles when anchor is selected
let mut extension = Vec::new();
for &(path, id, point_type) in new_selected.iter() {
if point_type == ManipulatorType::Anchor {
extension.push((path, id, ManipulatorType::InHandle));
extension.push((path, id, ManipulatorType::OutHandle));
}
}
new_selected.extend(extension);
let include_handles = tool_data.shape_editor.selected_layers_ref();
tool_data.snap_manager.add_all_document_handles(document, &include_handles, &[], &new_selected);
tool_data.drag_start_pos = input.mouse.position;
Dragging
}
// We didn't find a point nearby, so consider selecting the nearest shape instead
else {
let selection_size = DVec2::new(2.0, 2.0);
// Select shapes directly under our mouse
let intersection = document
.graphene_document
.intersects_quad_root(Quad::from_box([input.mouse.position - selection_size, input.mouse.position + selection_size]), font_cache);
if !intersection.is_empty() {
if toggle_add_to_selection {
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: intersection }.into());
} else {
responses.push_back(
DocumentMessage::SetSelectedLayers {
replacement_selected_layers: intersection,
}
.into(),
);
}
} else {
// Clear the previous selection if we didn't find anything
if !input.keyboard.get(toggle_add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
}
}
Ready
}
}
// Dragging
(
Dragging,
PointerMove {
alt_mirror_angle,
shift_mirror_distance,
},
) => {
// Determine when alt state changes
let alt_pressed = input.keyboard.get(alt_mirror_angle as usize);
if alt_pressed != tool_data.alt_debounce {
tool_data.alt_debounce = alt_pressed;
// Only on alt down
if alt_pressed {
tool_data.shape_editor.toggle_handle_mirroring_on_selected(true, false, responses);
}
}
// Determine when shift state changes
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
if shift_pressed != tool_data.shift_debounce {
tool_data.shift_debounce = shift_pressed;
tool_data.shape_editor.toggle_handle_mirroring_on_selected(false, true, responses);
}
// Move the selected points by the mouse position
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
tool_data.shape_editor.move_selected_points(snapped_position - tool_data.drag_start_pos, snapped_position, responses);
tool_data.drag_start_pos = snapped_position;
Dragging
}
// Mouse up
(_, DragStop) => {
tool_data.snap_manager.cleanup(responses);
Ready
}
// Delete key
(_, Delete) => {
// Delete the selected points and clean up overlays
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.shape_editor.delete_selected_points(responses);
responses.push_back(SelectionChanged.into());
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
Ready
}
(_, Abort) => {
// TODO Tell overlay manager to remove the overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
Ready
}
(
_,
PointerMove {
alt_mirror_angle: _,
shift_mirror_distance: _,
},
) => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PathToolFsmState::Ready => HintData(vec![
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Select Point"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Grow/Shrink Selection"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected"),
plus: false,
}]),
HintGroup(vec![
HintInfo {
key_groups: vec![
KeysGroup(vec![Key::KeyArrowUp]),
KeysGroup(vec![Key::KeyArrowRight]),
KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]),
],
key_groups_mac: None,
mouse: None,
label: String::from("Nudge Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Big Increment Nudge"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyG])],
key_groups_mac: None,
mouse: None,
label: String::from("Grab Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])],
key_groups_mac: None,
mouse: None,
label: String::from("Rotate Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])],
key_groups_mac: None,
mouse: None,
label: String::from("Scale Selected (coming soon)"),
plus: false,
},
]),
]),
PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("Split/Align Handles (Toggle)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Share Lengths of Aligned Handles"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,478 @@
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::layers::vector::subpath::Subpath;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct PenTool {
fsm_state: PenToolFsmState,
tool_data: PenToolData,
options: PenOptions,
}
pub struct PenOptions {
line_weight: f64,
}
impl Default for PenOptions {
fn default() -> Self {
Self { line_weight: 5. }
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PenToolMessage {
// Standard messages
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
Abort,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
Confirm,
DragStart,
DragStop,
PointerMove {
snap_angle: Key,
break_handle: Key,
},
Undo,
UpdateOptions(PenOptionsUpdate),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
DraggingHandle,
PlacingAnchor,
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PenOptionsUpdate {
LineWeight(f64),
}
impl ToolMetadata for PenTool {
fn icon_name(&self) -> String {
"VectorPenTool".into()
}
fn tooltip(&self) -> String {
"Pen Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Pen
}
}
impl PropertyHolder for PenTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: Some(self.options.line_weight),
is_integer: false,
min: Some(0.),
on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
..NumberInput::default()
}))],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PenTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message {
match action {
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
match self.fsm_state {
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant;
Undo,
DragStart,
DragStop,
Confirm,
Abort,
),
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
DragStart,
DragStop,
PointerMove,
Confirm,
Abort,
),
}
}
}
impl ToolTransition for PenTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
}
}
}
impl Default for PenToolFsmState {
fn default() -> Self {
PenToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
weight: f64,
path: Option<Vec<LayerId>>,
overlay_renderer: OverlayRenderer,
snap_manager: SnapManager,
}
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
type ToolOptions = PenOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let transform = tool_data.path.as_ref().and_then(|path| document.graphene_document.multiply_transforms(path).ok()).unwrap_or_default();
if let ToolMessage::Pen(event) = event {
match (self, event) {
(_, PenToolMessage::DocumentIsDirty) => {
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer_path in document.selected_visible_layers() {
tool_data.overlay_renderer.render_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
(_, PenToolMessage::SelectionChanged) => {
// Set the previously selected layers to invisible
for layer_path in document.all_layers() {
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
}
self
}
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
// Create a new layer and prep snap system
tool_data.path = Some(document.get_path_for_new_layer());
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
// Get the position and set properties
let transform = tool_data
.path
.as_ref()
.and_then(|path| document.graphene_document.multiply_transforms(&path[..path.len() - 1]).ok())
.unwrap_or_default();
let start_position = transform.inverse().transform_point2(snapped_position);
tool_data.weight = tool_options.line_weight;
// Create the initial shape with a `bez_path` (only contains a moveto initially)
if let Some(layer_path) = &tool_data.path {
responses.push_back(
Operation::AddShape {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
insert_index: -1,
subpath: Default::default(),
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
}
.into(),
);
responses.push_back(add_manipulator_group(
&tool_data.path,
ManipulatorGroup::new_with_handles(start_position, Some(start_position), Some(start_position)),
));
}
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => PenToolFsmState::DraggingHandle,
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
// Add new point onto path
if let Some(layer_path) = &tool_data.path {
if let Some(manipulator_group) = get_subpath(layer_path, document).and_then(|subpath| subpath.manipulator_groups().last()) {
if let Some(out_handle) = &manipulator_group.points[ManipulatorType::OutHandle] {
responses.push_back(add_manipulator_group(&tool_data.path, ManipulatorGroup::new_with_anchor(out_handle.position)));
}
}
}
PenToolFsmState::PlacingAnchor
}
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle }) => {
if let Some(layer_path) = &tool_data.path {
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let mut pos = transform.inverse().transform_point2(mouse);
if let Some(((&id, manipulator_group), _previous)) = get_subpath(layer_path, document).and_then(last_2_manipulator_groups) {
if let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() {
pos = compute_snapped_angle(input, snap_angle, pos, anchor.position);
}
// Update points on current segment (to show preview of new handle)
let msg = Operation::MoveManipulatorPoint {
layer_path: layer_path.clone(),
id,
manipulator_type: ManipulatorType::OutHandle,
position: pos.into(),
};
responses.push_back(msg.into());
// Mirror handle of last segment
if !input.keyboard.get(break_handle as usize) && get_subpath(layer_path, document).map(|shape| shape.manipulator_groups().len() > 1).unwrap_or_default() {
if let Some(anchor) = manipulator_group.points[ManipulatorType::Anchor].as_ref() {
pos = anchor.position - (pos - anchor.position);
}
let msg = Operation::MoveManipulatorPoint {
layer_path: layer_path.clone(),
id,
manipulator_type: ManipulatorType::InHandle,
position: pos.into(),
};
responses.push_back(msg.into());
}
}
}
self
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, .. }) => {
if let Some(layer_path) = &tool_data.path {
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let mut pos = transform.inverse().transform_point2(mouse);
if let Some(((&id, _), previous)) = get_subpath(layer_path, document).and_then(last_2_manipulator_groups) {
if let Some(relative) = previous.as_ref().and_then(|(_, manipulator_group)| manipulator_group.points[ManipulatorType::Anchor].as_ref()) {
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
}
for manipulator_type in [ManipulatorType::Anchor, ManipulatorType::InHandle, ManipulatorType::OutHandle] {
let msg = Operation::MoveManipulatorPoint {
layer_path: layer_path.clone(),
id,
manipulator_type,
position: pos.into(),
};
responses.push_back(msg.into());
}
}
}
self
}
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
// Abort or commit the transaction to the undo history
if let Some(layer_path) = tool_data.path.as_ref() {
if let Some(subpath) = (get_subpath(layer_path, document)).filter(|subpath| subpath.manipulator_groups().len() > 1) {
if let Some(((&(mut id), mut manipulator_group), previous)) = last_2_manipulator_groups(subpath) {
// Remove the unplaced anchor if in anchor placing mode
if self == PenToolFsmState::PlacingAnchor {
let layer_path = layer_path.clone();
let op = Operation::RemoveManipulatorGroup { layer_path, id };
responses.push_back(op.into());
if let Some((&new_id, new_manipulator_group)) = previous {
id = new_id;
manipulator_group = new_manipulator_group;
}
}
// Remove the out handle if in dragging handle mode
let op = Operation::MoveManipulatorPoint {
layer_path: layer_path.clone(),
id,
manipulator_type: ManipulatorType::OutHandle,
position: manipulator_group.points[ManipulatorType::Anchor].as_ref().unwrap().position.into(),
};
responses.push_back(op.into());
}
}
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
// Clean up overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
tool_data.path = None;
tool_data.snap_manager.cleanup(responses);
PenToolFsmState::Ready
}
(_, PenToolMessage::Abort) => {
// Clean up overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_subpath_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Draw Path"),
plus: false,
}])]),
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Add Handle"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Add Anchor"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Break Handle"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
key_groups_mac: None,
mouse: None,
label: String::from("End Path"),
plus: false,
}]),
]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
// TODO: Expand `pos` name below to the full word (position?)
/// Snap the angle of the line from relative to pos if the key is pressed.
fn compute_snapped_angle(input: &InputPreprocessorMessageHandler, key: Key, pos: DVec2, relative: DVec2) -> DVec2 {
if input.keyboard.get(key as usize) {
let delta = relative - pos;
let length = delta.length();
let mut angle = -delta.angle_between(DVec2::X);
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
relative - rotated
} else {
pos
}
}
/// Pushes a [ManipulatorGroup] to the current layer via an [Operation].
fn add_manipulator_group(layer_path: &Option<Vec<LayerId>>, manipulator_group: ManipulatorGroup) -> Message {
if let Some(layer_path) = layer_path {
Operation::PushManipulatorGroup {
layer_path: layer_path.clone(),
manipulator_group,
}
.into()
} else {
Message::NoOp
}
}
/// Gets the currently editing [Subpath].
fn get_subpath<'a>(layer_path: &'a [LayerId], document: &'a DocumentMessageHandler) -> Option<&'a Subpath> {
document.graphene_document.layer(layer_path).ok().and_then(|layer| layer.as_subpath())
}
type ManipulatorGroupRef<'a> = (&'a u64, &'a ManipulatorGroup);
/// Gets the last 2 [ManipulatorGroup]s on the currently editing layer along with its ID.
fn last_2_manipulator_groups(subpath: &Subpath) -> Option<(ManipulatorGroupRef, Option<ManipulatorGroupRef>)> {
subpath.manipulator_groups().enumerate().last().map(|last| {
(
last,
(subpath.manipulator_groups().len() > 1)
.then(|| subpath.manipulator_groups().enumerate().nth(subpath.manipulator_groups().len() - 2))
.flatten(),
)
})
}
@@ -0,0 +1,233 @@
use crate::consts::DRAG_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct RectangleTool {
fsm_state: RectangleToolFsmState,
tool_data: RectangleToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum RectangleToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize {
center: Key,
lock_ratio: Key,
},
}
impl PropertyHolder for RectangleTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for RectangleTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use RectangleToolFsmState::*;
match self.fsm_state {
Ready => actions!(RectangleToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(RectangleToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolMetadata for RectangleTool {
fn icon_name(&self) -> String {
"VectorRectangleTool".into()
}
fn tooltip(&self) -> String {
"Rectangle Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Rectangle
}
}
impl ToolTransition for RectangleTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(RectangleToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RectangleToolFsmState {
Ready,
Drawing,
}
impl Default for RectangleToolFsmState {
fn default() -> Self {
RectangleToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct RectangleToolData {
data: Resize,
}
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use RectangleToolFsmState::*;
use RectangleToolMessage::*;
let mut shape_data = &mut tool_data.data;
if let ToolMessage::Rectangle(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(responses, document, input.mouse.position, font_cache);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(document.get_path_for_new_layer());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddRect {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup(responses);
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup(responses);
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Rectangle"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
@@ -0,0 +1,846 @@
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::path_outline::*;
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::boolean_ops::BooleanOperation;
use graphene::document::Document;
use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct SelectTool {
fsm_state: SelectToolFsmState,
tool_data: SelectToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum SelectToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
Align {
axis: AlignAxis,
aggregate: AlignAggregate,
},
DragStart {
add_to_selection: Key,
},
DragStop,
EditLayer,
FlipHorizontal,
FlipVertical,
PointerMove {
axis_align: Key,
snap_angle: Key,
center: Key,
},
}
impl ToolMetadata for SelectTool {
fn icon_name(&self) -> String {
"GeneralSelectTool".into()
}
fn tooltip(&self) -> String {
"Select Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Select
}
}
impl PropertyHolder for SelectTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignLeft".into(),
tooltip: "Align Left".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Min,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignHorizontalCenter".into(),
tooltip: "Align Horizontal Center".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Center,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignRight".into(),
tooltip: "Align Right".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::X,
aggregate: AlignAggregate::Max,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Unrelated,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignTop".into(),
tooltip: "Align Top".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Min,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignVerticalCenter".into(),
tooltip: "Align Vertical Center".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Center,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "AlignBottom".into(),
tooltip: "Align Bottom".into(),
size: 24,
on_update: WidgetCallback::new(|_| {
DocumentMessage::AlignSelectedLayers {
axis: AlignAxis::Y,
aggregate: AlignAggregate::Max,
}
.into()
}),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Align".into(),
text: "The contents of this popover menu are coming soon".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Section,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "FlipHorizontal".into(),
tooltip: "Flip Horizontal".into(),
size: 24,
on_update: WidgetCallback::new(|_| SelectToolMessage::FlipHorizontal.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "FlipVertical".into(),
tooltip: "Flip Vertical".into(),
size: 24,
on_update: WidgetCallback::new(|_| SelectToolMessage::FlipVertical.into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Flip".into(),
text: "The contents of this popover menu are coming soon".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Section,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanUnion".into(),
tooltip: "Boolean Union".into(),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Union).into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractFront".into(),
tooltip: "Boolean Subtract Front".into(),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractFront).into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanSubtractBack".into(),
tooltip: "Boolean Subtract Back".into(),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::SubtractBack).into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanIntersect".into(),
tooltip: "Boolean Intersect".into(),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Intersection).into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "BooleanDifference".into(),
tooltip: "Boolean Difference".into(),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::BooleanOperation(BooleanOperation::Difference).into()),
..IconButton::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Boolean".into(),
text: "The contents of this popover menu are coming soon".into(),
..Default::default()
})),
],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
}
}
fn actions(&self) -> ActionList {
use SelectToolFsmState::*;
match self.fsm_state {
Ready => actions!(SelectToolMessageDiscriminant;
DragStart,
PointerMove,
Abort,
EditLayer,
),
_ => actions!(SelectToolMessageDiscriminant;
DragStop,
PointerMove,
Abort,
EditLayer,
),
}
}
}
impl ToolTransition for SelectTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(SelectToolMessage::DocumentIsDirty.into()),
tool_abort: Some(SelectToolMessage::Abort.into()),
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum SelectToolFsmState {
Ready,
Dragging,
DrawingBox,
ResizingBounds,
RotatingBounds,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
SelectToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct SelectToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
drag_box_overlay_layer: Option<Vec<LayerId>>,
path_outlines: PathOutline,
bounding_box_overlays: Option<BoundingBoxOverlays>,
snap_manager: SnapManager,
cursor: MouseCursorIcon,
}
impl SelectToolData {
fn selection_quad(&self) -> Quad {
let bbox = self.selection_box();
Quad::from_box(bbox)
}
fn selection_box(&self) -> [DVec2; 2] {
if self.drag_current == self.drag_start {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
[self.drag_start - tolerance, self.drag_start + tolerance]
} else {
[self.drag_start, self.drag_current]
}
}
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use SelectToolFsmState::*;
use SelectToolMessage::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
(_, DocumentIsDirty | SelectionChanged) => {
match (document.selected_visible_layers_bounding_box(font_cache), tool_data.bounding_box_overlays.take()) {
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
(Some(bounds), paths) => {
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
bounding_box_overlays.bounds = bounds;
bounding_box_overlays.transform = DAffine2::IDENTITY;
bounding_box_overlays.transform(responses);
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
}
(_, _) => {}
};
tool_data.path_outlines.update_selected(document.selected_visible_layers(), document, responses, font_cache);
tool_data.path_outlines.intersect_test_hovered(input, document, responses, font_cache);
self
}
(_, EditLayer) => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(Ok(intersect)) = document
.graphene_document
.intersects_quad_root(quad, font_cache)
.last()
.map(|path| document.graphene_document.layer(path))
{
match intersect.data {
LayerDataType::Text(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextToolMessage::Interact.into());
}
LayerDataType::Shape(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into());
}
_ => {}
}
}
self
}
(Ready, DragStart { add_to_selection }) => {
tool_data.path_outlines.clear_hovered(responses);
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
let dragging_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
bounding_box.pivot = edges.calculate_pivot();
edges
});
edges
} else {
None
};
let rotating_bounds = if let Some(bounding_box) = &mut tool_data.bounding_box_overlays {
bounding_box.check_rotate(input.mouse.position)
} else {
false
};
let mut selected: Vec<_> = document.selected_visible_layers().map(|path| path.to_vec()).collect();
let quad = tool_data.selection_quad();
let mut intersection = document.graphene_document.intersects_quad_root(quad, font_cache);
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
// If the user clicks on new shape, make that layer their new selection.
// Otherwise enter the box select mode
let state = if let Some(selected_edges) = dragging_bounds {
let snap_x = selected_edges.2 || selected_edges.3;
let snap_y = selected_edges.0 || selected_edges.1;
tool_data.snap_manager.start_snap(document, document.bounding_boxes(Some(&selected), None, font_cache), snap_x, snap_y);
tool_data
.snap_manager
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>(), &[]);
tool_data.layers_dragging = selected;
ResizingBounds
} else if rotating_bounds {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
let selected = selected.iter().collect::<Vec<_>>();
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
*selected.pivot = selected.calculate_pivot(font_cache);
}
tool_data.layers_dragging = selected;
RotatingBounds
} else if intersection.last().map(|last| selected.contains(last)).unwrap_or(false) {
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.layers_dragging = selected;
tool_data
.snap_manager
.start_snap(document, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
Dragging
} else {
if !input.keyboard.get(add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.layers_dragging.clear();
}
if let Some(intersection) = intersection.pop() {
selected = vec![intersection];
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into());
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.layers_dragging.append(&mut selected);
tool_data
.snap_manager
.start_snap(document, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
Dragging
} else {
tool_data.drag_box_overlay_layer = Some(add_bounding_box(responses));
DrawingBox
}
};
state
}
(Dragging, PointerMove { axis_align, .. }) => {
// TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here.
responses.push_front(SelectToolMessage::DocumentIsDirty.into());
let mouse_position = axis_align_drag(input.keyboard.get(axis_align as usize), input.mouse.position, tool_data.drag_start);
let mouse_delta = mouse_position - tool_data.drag_current;
let snap = tool_data
.layers_dragging
.iter()
.filter_map(|path| document.graphene_document.viewport_bounding_box(path, font_cache).ok()?)
.flat_map(snapping::expand_bounds)
.collect();
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for path in Document::shallowest_unique_layers(tool_data.layers_dragging.iter()) {
responses.push_front(
Operation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(mouse_delta + closest_move).to_cols_array(),
}
.into(),
);
}
tool_data.drag_current = mouse_position + closest_move;
Dragging
}
(ResizingBounds, PointerMove { axis_align, center, .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(movement) = &mut bounds.selected_edges {
let (center, axis_align) = (input.keyboard.get(center as usize), input.keyboard.get(axis_align as usize));
let mouse_position = input.mouse.position;
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align);
let delta = movement.bounds_to_scale_transform(center, size);
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
selected.update_transforms(delta);
}
}
ResizingBounds
}
(RotatingBounds, PointerMove { snap_angle, .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
let angle = {
let start_offset = tool_data.drag_start - bounds.pivot;
let end_offset = input.mouse.position - bounds.pivot;
start_offset.angle_between(end_offset)
};
let snapped_angle = if input.keyboard.get(snap_angle as usize) {
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
(angle / snap_resolution).round() * snap_resolution
} else {
angle
};
let delta = DAffine2::from_angle(snapped_angle);
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
selected.update_transforms(delta);
}
RotatingBounds
}
(DrawingBox, PointerMove { .. }) => {
tool_data.drag_current = input.mouse.position;
responses.push_front(
DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: tool_data.drag_box_overlay_layer.clone().unwrap(),
transform: transform_from_box(tool_data.drag_start, tool_data.drag_current, DAffine2::IDENTITY).to_cols_array(),
}
.into(),
)
.into(),
);
DrawingBox
}
(Ready, PointerMove { .. }) => {
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
// Generate the select outline (but not if the user is going to use the bound overlays)
if cursor == MouseCursorIcon::Default {
tool_data.path_outlines.intersect_test_hovered(input, document, responses, font_cache);
} else {
tool_data.path_outlines.clear_hovered(responses);
}
if tool_data.cursor != cursor {
tool_data.cursor = cursor;
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
}
Ready
}
(Dragging, DragStop) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::Undo,
false => DocumentMessage::CommitTransaction,
};
tool_data.snap_manager.cleanup(responses);
responses.push_front(response.into());
Ready
}
(ResizingBounds, DragStop) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
bounds.original_transforms.clear();
}
Ready
}
(RotatingBounds, DragStop) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
bounds.original_transforms.clear();
}
Ready
}
(DrawingBox, DragStop) => {
let quad = tool_data.selection_quad();
responses.push_front(
DocumentMessage::AddSelectedLayers {
additional_layers: document.graphene_document.intersects_quad_root(quad, font_cache),
}
.into(),
);
responses.push_front(
DocumentMessage::Overlays(
Operation::DeleteLayer {
path: tool_data.drag_box_overlay_layer.take().unwrap(),
}
.into(),
)
.into(),
);
Ready
}
(Dragging, Abort) => {
tool_data.snap_manager.cleanup(responses);
responses.push_back(DocumentMessage::Undo.into());
tool_data.path_outlines.clear_selected(responses);
Ready
}
(_, Abort) => {
if let Some(path) = tool_data.drag_box_overlay_layer.take() {
responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into())
};
if let Some(mut bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
let mut selected = Selected::new(
&mut bounding_box_overlays.original_transforms,
&mut bounding_box_overlays.pivot,
&selected,
responses,
&document.graphene_document,
);
selected.revert_operation();
bounding_box_overlays.delete(responses);
}
tool_data.path_outlines.clear_hovered(responses);
tool_data.path_outlines.clear_selected(responses);
tool_data.snap_manager.cleanup(responses);
Ready
}
(_, Align { axis, aggregate }) => {
responses.push_back(DocumentMessage::AlignSelectedLayers { axis, aggregate }.into());
self
}
(_, FlipHorizontal) => {
responses.push_back(DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::X }.into());
self
}
(_, FlipVertical) => {
responses.push_back(DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::Y }.into());
self
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
SelectToolFsmState::Ready => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected"),
plus: false,
}]),
HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyG])],
key_groups_mac: None,
mouse: None,
label: String::from("Grab Selected"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])],
key_groups_mac: None,
mouse: None,
label: String::from("Rotate Selected"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])],
key_groups_mac: None,
mouse: None,
label: String::from("Scale Selected"),
plus: false,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Select Object"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand])]),
mouse: None,
label: String::from("Innermost"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Grow/Shrink Selection"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Select Area"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Grow/Shrink Selection"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![
KeysGroup(vec![Key::KeyArrowUp]),
KeysGroup(vec![Key::KeyArrowRight]),
KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]),
],
key_groups_mac: None,
mouse: None,
label: String::from("Nudge Selected"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Big Increment Nudge"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Move Duplicate"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyD])]),
mouse: None,
label: String::from("Duplicate"),
plus: false,
},
]),
]),
SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain to Axis"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap to Points (coming soon)"),
plus: false,
},
])]),
SelectToolFsmState::DrawingBox => HintData(vec![]),
SelectToolFsmState::ResizingBounds => HintData(vec![]),
SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
@@ -0,0 +1,276 @@
use crate::consts::DRAG_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct ShapeTool {
fsm_state: ShapeToolFsmState,
tool_data: ShapeToolData,
options: ShapeOptions,
}
pub struct ShapeOptions {
vertices: u32,
}
impl Default for ShapeOptions {
fn default() -> Self {
Self { vertices: 6 }
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ShapeToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize {
center: Key,
lock_ratio: Key,
},
UpdateOptions(ShapeOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ShapeOptionsUpdate {
Vertices(u32),
}
impl ToolMetadata for ShapeTool {
fn icon_name(&self) -> String {
"VectorShapeTool".into()
}
fn tooltip(&self) -> String {
"Shape Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Shape
}
}
impl PropertyHolder for ShapeTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
label: "Sides".into(),
value: Some(self.options.vertices as f64),
is_integer: true,
min: Some(3.),
max: Some(1000.),
on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()),
..NumberInput::default()
}))],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ShapeTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message {
match action {
ShapeOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use ShapeToolFsmState::*;
match self.fsm_state {
Ready => actions!(ShapeToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(ShapeToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolTransition for ShapeTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(ShapeToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShapeToolFsmState {
Ready,
Drawing,
}
impl Default for ShapeToolFsmState {
fn default() -> Self {
ShapeToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct ShapeToolData {
sides: u32,
data: Resize,
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
type ToolOptions = ShapeOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use ShapeToolFsmState::*;
use ShapeToolMessage::*;
let mut shape_data = &mut tool_data.data;
if let ToolMessage::Shape(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(responses, document, input.mouse.position, font_cache);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(document.get_path_for_new_layer());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.sides = tool_options.vertices;
responses.push_back(
Operation::AddNgon {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
sides: tool_data.sides,
style: style::PathStyle::new(None, style::Fill::solid(global_tool_data.primary_color)),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
match shape_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup(responses);
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup(responses);
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Shape"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain 1:1 Aspect"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain 1:1 Aspect"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
@@ -0,0 +1,308 @@
use crate::consts::DRAG_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct SplineTool {
fsm_state: SplineToolFsmState,
tool_data: SplineToolData,
options: SplineOptions,
}
pub struct SplineOptions {
line_weight: f64,
}
impl Default for SplineOptions {
fn default() -> Self {
Self { line_weight: 5. }
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Spline)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum SplineToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
Confirm,
DragStart,
DragStop,
PointerMove,
Undo,
UpdateOptions(SplineOptionsUpdate),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SplineToolFsmState {
Ready,
Drawing,
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum SplineOptionsUpdate {
LineWeight(f64),
}
impl ToolMetadata for SplineTool {
fn icon_name(&self) -> String {
"VectorSplineTool".into()
}
fn tooltip(&self) -> String {
"Spline Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Spline
}
}
impl PropertyHolder for SplineTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Weight".into(),
value: Some(self.options.line_weight),
is_integer: false,
min: Some(0.),
on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()),
..NumberInput::default()
}))],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SplineTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Spline(SplineToolMessage::UpdateOptions(action)) = message {
match action {
SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use SplineToolFsmState::*;
match self.fsm_state {
Ready => actions!(SplineToolMessageDiscriminant;
Undo,
DragStart,
DragStop,
Confirm,
Abort,
),
Drawing => actions!(SplineToolMessageDiscriminant;
DragStop,
PointerMove,
Confirm,
Abort,
),
}
}
}
impl ToolTransition for SplineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(SplineToolMessage::Abort.into()),
selection_changed: None,
}
}
}
impl Default for SplineToolFsmState {
fn default() -> Self {
SplineToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct SplineToolData {
points: Vec<DVec2>,
next_point: DVec2,
weight: f64,
path: Option<Vec<LayerId>>,
snap_manager: SnapManager,
}
impl Fsm for SplineToolFsmState {
type ToolData = SplineToolData;
type ToolOptions = SplineOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use SplineToolFsmState::*;
use SplineToolMessage::*;
let transform = document.graphene_document.root.transform;
if let ToolMessage::Spline(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.path = Some(document.get_path_for_new_layer());
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let pos = transform.inverse().transform_point2(snapped_position);
tool_data.points.push(pos);
tool_data.next_point = pos;
tool_data.weight = tool_options.line_weight;
responses.push_back(add_spline(tool_data, global_tool_data, true));
Drawing
}
(Drawing, DragStop) => {
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let pos = transform.inverse().transform_point2(snapped_position);
if let Some(last_pos) = tool_data.points.last() {
if last_pos.distance(pos) > DRAG_THRESHOLD {
tool_data.points.push(pos);
tool_data.next_point = pos;
}
}
responses.push_back(remove_preview(tool_data));
responses.push_back(add_spline(tool_data, global_tool_data, true));
Drawing
}
(Drawing, PointerMove) => {
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
let pos = transform.inverse().transform_point2(snapped_position);
tool_data.next_point = pos;
responses.push_back(remove_preview(tool_data));
responses.push_back(add_spline(tool_data, global_tool_data, true));
Drawing
}
(Drawing, Confirm) | (Drawing, Abort) => {
if tool_data.points.len() >= 2 {
responses.push_back(remove_preview(tool_data));
responses.push_back(add_spline(tool_data, global_tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
tool_data.path = None;
tool_data.points.clear();
tool_data.snap_manager.cleanup(responses);
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Draw Spline"),
plus: false,
}])]),
SplineToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Extend Spline"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
key_groups_mac: None,
mouse: None,
label: String::from("End Spline"),
plus: false,
}]),
]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
fn remove_preview(tool_data: &SplineToolData) -> Message {
Operation::DeleteLayer {
path: tool_data.path.clone().unwrap(),
}
.into()
}
fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, show_preview: bool) -> Message {
let mut points: Vec<(f64, f64)> = tool_data.points.iter().map(|p| (p.x, p.y)).collect();
if show_preview {
points.push((tool_data.next_point.x, tool_data.next_point.y))
}
Operation::AddSpline {
path: tool_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
}
.into()
}
@@ -0,0 +1,499 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, SELECTION_TOLERANCE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::input_widgets::{FontInput, NumberInput};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct TextTool {
fsm_state: TextToolFsmState,
tool_data: TextToolData,
options: TextOptions,
}
pub struct TextOptions {
font_size: u32,
font_name: String,
font_style: String,
}
impl Default for TextOptions {
fn default() -> Self {
Self {
font_size: 24,
font_name: "Merriweather".into(),
font_style: "Normal (400)".into(),
}
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Text)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum TextToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
// Tool-specific messages
CommitText,
Interact,
TextChange {
new_text: String,
},
UpdateBounds {
new_text: String,
},
UpdateOptions(TextOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum TextOptionsUpdate {
Font { family: String, style: String },
FontSize(u32),
}
impl ToolMetadata for TextTool {
fn icon_name(&self) -> String {
"VectorTextTool".into()
}
fn tooltip(&self) -> String {
"Text Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Text
}
}
impl PropertyHolder for TextTool {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::FontInput(FontInput {
is_style_picker: false,
font_family: self.options.font_name.clone(),
font_style: self.options.font_style.clone(),
on_update: WidgetCallback::new(|font_input: &FontInput| {
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
})
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::FontInput(FontInput {
is_style_picker: true,
font_family: self.options.font_name.clone(),
font_style: self.options.font_style.clone(),
on_update: WidgetCallback::new(|font_input: &FontInput| {
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
})
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Horizontal,
separator_type: SeparatorType::Related,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " px".into(),
label: "Size".into(),
value: Some(self.options.font_size as f64),
is_integer: true,
min: Some(1.),
on_update: WidgetCallback::new(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()),
..NumberInput::default()
})),
],
}]))
}
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
if let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = message {
match action {
TextOptionsUpdate::Font { family, style } => {
self.options.font_name = family;
self.options.font_style = style;
self.register_properties(responses, LayoutTarget::ToolOptions);
}
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
}
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &self.options, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use TextToolFsmState::*;
match self.fsm_state {
Ready => actions!(TextToolMessageDiscriminant;
Interact,
),
Editing => actions!(TextToolMessageDiscriminant;
Interact,
Abort,
CommitText,
),
}
}
}
impl ToolTransition for TextTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(TextToolMessage::DocumentIsDirty.into()),
tool_abort: Some(TextToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TextToolFsmState {
Ready,
Editing,
}
impl Default for TextToolFsmState {
fn default() -> Self {
TextToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct TextToolData {
path: Vec<LayerId>,
overlays: Vec<Vec<LayerId>>,
}
fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array()
}
fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, newlen: usize) {
while overlays.len() > newlen {
let operation = Operation::DeleteLayer { path: overlays.pop().unwrap() };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
while overlays.len() < newlen {
let path = vec![generate_uuid()];
overlays.push(path.clone());
let operation = Operation::AddRect {
path,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
}
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
resize_overlays(&mut tool_data.overlays, responses, visible_text_layers.len());
let bounds = visible_text_layers
.into_iter()
.zip(&tool_data.overlays)
.filter_map(|(layer_path, overlay_path)| {
document
.graphene_document
.layer(layer_path)
.unwrap()
.aabb_for_transform(document.graphene_document.multiply_transforms(layer_path).unwrap(), font_cache)
.map(|bounds| (bounds, overlay_path))
})
.collect::<Vec<_>>();
let new_len = bounds.len();
for (bounds, overlay_path) in bounds {
let operation = Operation::SetLayerTransformInViewport {
path: overlay_path.to_vec(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
resize_overlays(&mut tool_data.overlays, responses, new_len);
}
impl Fsm for TextToolFsmState {
type ToolData = TextToolData;
type ToolOptions = TextOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use TextToolFsmState::*;
use TextToolMessage::*;
if let ToolMessage::Text(event) = event {
match (self, event) {
(state, DocumentIsDirty) => {
update_overlays(document, tool_data, responses, font_cache);
state
}
(state, Interact) => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
let new_state = if let Some(l) = document
.graphene_document
.intersects_quad_root(quad, font_cache)
.last()
.filter(|l| document.graphene_document.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false))
// Editing existing text
{
if state == TextToolFsmState::Editing {
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: false,
}
.into(),
);
}
tool_data.path = l.clone();
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: true,
}
.into(),
);
responses.push_back(
DocumentMessage::SetSelectedLayers {
replacement_selected_layers: vec![tool_data.path.clone()],
}
.into(),
);
Editing
}
// Creating new text
else if state == TextToolFsmState::Ready {
let transform = DAffine2::from_translation(input.mouse.position).to_cols_array();
let font_size = tool_options.font_size;
let font_name = tool_options.font_name.clone();
let font_style = tool_options.font_style.clone();
tool_data.path = document.get_path_for_new_layer();
responses.push_back(
Operation::AddText {
path: tool_data.path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
insert_index: -1,
text: r#""#.to_string(),
style: style::PathStyle::new(None, Fill::solid(global_tool_data.primary_color)),
size: font_size as f64,
font_name,
font_style,
}
.into(),
);
responses.push_back(
Operation::SetLayerTransformInViewport {
path: tool_data.path.clone(),
transform,
}
.into(),
);
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: true,
}
.into(),
);
responses.push_back(
DocumentMessage::SetSelectedLayers {
replacement_selected_layers: vec![tool_data.path.clone()],
}
.into(),
);
Editing
} else {
// Removing old text as editable
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: false,
}
.into(),
);
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
};
new_state
}
(state, Abort) => {
if state == TextToolFsmState::Editing {
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: false,
}
.into(),
);
}
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
}
(Editing, CommitText) => {
responses.push_back(FrontendMessage::TriggerTextCommit.into());
Editing
}
(Editing, TextChange { new_text }) => {
responses.push_back(
Operation::SetTextContent {
path: tool_data.path.clone(),
new_text,
}
.into(),
);
responses.push_back(
DocumentMessage::SetTexboxEditability {
path: tool_data.path.clone(),
editable: false,
}
.into(),
);
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
}
(Editing, UpdateBounds { new_text }) => {
resize_overlays(&mut tool_data.overlays, responses, 1);
let text = document.graphene_document.layer(&tool_data.path).unwrap().as_text().unwrap();
let quad = text.bounding_box(&new_text, text.load_face(font_cache));
let transformed_quad = document.graphene_document.multiply_transforms(&tool_data.path).unwrap() * quad;
let bounds = transformed_quad.bounding_box();
let operation = Operation::SetLayerTransformInViewport {
path: tool_data.overlays[0].clone(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
Editing
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
TextToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Add Text"),
plus: false,
},
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb),
label: String::from("Edit Text"),
plus: false,
},
])]),
TextToolFsmState::Editing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyEnter])]),
mouse: None,
label: String::from("Commit Edit"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEscape])],
key_groups_mac: None,
mouse: None,
label: String::from("Discard Edit"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into());
}
}