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,11 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
#[impl_message(Message, BroadcastMessage, TriggerEvent)]
pub enum BroadcastEvent {
DocumentIsDirty,
ToolAbort,
SelectionChanged,
}
@@ -0,0 +1,23 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Broadcast)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum BroadcastMessage {
// Sub-messages
#[remain::unsorted]
#[child]
TriggerEvent(BroadcastEvent),
// Messages
SubscribeEvent {
on: BroadcastEvent,
send: Box<Message>,
},
UnsubscribeEvent {
on: BroadcastEvent,
message: Box<Message>,
},
}
@@ -0,0 +1,32 @@
use crate::messages::prelude::*;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct BroadcastMessageHandler {
listeners: HashMap<BroadcastEvent, Vec<Message>>,
}
impl MessageHandler<BroadcastMessage, ()> for BroadcastMessageHandler {
#[remain::check]
fn process_message(&mut self, message: BroadcastMessage, _data: (), responses: &mut VecDeque<Message>) {
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
BroadcastMessage::TriggerEvent(event) => {
for message in self.listeners.entry(event).or_default() {
responses.push_front(message.clone())
}
}
// Messages
BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send),
BroadcastMessage::UnsubscribeEvent { on, message } => self.listeners.entry(on).or_default().retain(|msg| *msg != *message),
}
}
fn actions(&self) -> ActionList {
vec![]
}
}
+9
View File
@@ -0,0 +1,9 @@
mod broadcast_message;
mod broadcast_message_handler;
pub mod broadcast_event;
#[doc(inline)]
pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant};
#[doc(inline)]
pub use broadcast_message_handler::BroadcastMessageHandler;
@@ -0,0 +1,12 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[impl_message(Message, Debug)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum DebugMessage {
ToggleTraceLogs,
MessageOff,
MessageNames,
MessageContents,
}
@@ -0,0 +1,52 @@
use super::utility_types::MessageLoggingVerbosity;
use crate::messages::prelude::*;
use std::collections::VecDeque;
#[derive(Debug, Default)]
pub struct DebugMessageHandler {
pub message_logging_verbosity: MessageLoggingVerbosity,
}
impl MessageHandler<DebugMessage, ()> for DebugMessageHandler {
#[remain::check]
fn process_message(&mut self, message: DebugMessage, _data: (), responses: &mut VecDeque<Message>) {
match message {
DebugMessage::ToggleTraceLogs => {
if let log::LevelFilter::Debug = log::max_level() {
log::set_max_level(log::LevelFilter::Trace);
} else {
log::set_max_level(log::LevelFilter::Debug);
}
// Refresh the checkmark beside the menu entry for this
responses.push_back(MenuBarMessage::SendLayout.into());
}
DebugMessage::MessageOff => {
self.message_logging_verbosity = MessageLoggingVerbosity::Off;
// Refresh the checkmark beside the menu entry for this
responses.push_back(MenuBarMessage::SendLayout.into());
}
DebugMessage::MessageNames => {
self.message_logging_verbosity = MessageLoggingVerbosity::Names;
// Refresh the checkmark beside the menu entry for this
responses.push_back(MenuBarMessage::SendLayout.into());
}
DebugMessage::MessageContents => {
self.message_logging_verbosity = MessageLoggingVerbosity::Contents;
// Refresh the checkmark beside the menu entry for this
responses.push_back(MenuBarMessage::SendLayout.into());
}
}
}
advertise_actions!(DebugMessageDiscriminant;
ToggleTraceLogs,
MessageOff,
MessageNames,
MessageContents,
);
}
+9
View File
@@ -0,0 +1,9 @@
mod debug_message;
mod debug_message_handler;
pub mod utility_types;
#[doc(inline)]
pub use debug_message::{DebugMessage, DebugMessageDiscriminant};
#[doc(inline)]
pub use debug_message_handler::DebugMessageHandler;
@@ -0,0 +1,7 @@
#[derive(Debug, Default, Clone, Copy)]
pub enum MessageLoggingVerbosity {
#[default]
Off,
Names,
Contents,
}
@@ -0,0 +1,37 @@
use super::export_dialog::ExportDialogMessage;
use super::new_document_dialog::NewDocumentDialogMessage;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Dialog)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum DialogMessage {
// Sub-messages
#[remain::unsorted]
#[child]
ExportDialog(ExportDialogMessage),
#[remain::unsorted]
#[child]
NewDocumentDialog(NewDocumentDialogMessage),
// Messages
CloseAllDocumentsWithConfirmation,
CloseDialogAndThen {
followups: Vec<Message>,
},
DisplayDialogError {
title: String,
description: String,
},
RequestAboutGraphiteDialog,
RequestAboutGraphiteDialogWithLocalizedCommitDate {
localized_commit_date: String,
},
RequestComingSoonDialog {
issue: Option<i32>,
},
RequestExportDialog,
RequestNewDocumentDialog,
}
@@ -0,0 +1,108 @@
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog};
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::prelude::*;
#[derive(Debug, Default, Clone)]
pub struct DialogMessageHandler {
export_dialog: ExportDialogMessageHandler,
new_document_dialog: NewDocumentDialogMessageHandler,
}
impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHandler {
#[remain::check]
fn process_message(&mut self, message: DialogMessage, portfolio: &PortfolioMessageHandler, responses: &mut VecDeque<Message>) {
#[remain::sorted]
match message {
#[remain::unsorted]
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, (), responses),
#[remain::unsorted]
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, (), responses),
DialogMessage::CloseAllDocumentsWithConfirmation => {
let dialog = simple_dialogs::CloseAllDocumentsDialog;
dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }.into());
}
DialogMessage::CloseDialogAndThen { followups } => {
responses.push_back(FrontendMessage::DisplayDialogDismiss.into());
for message in followups.into_iter() {
responses.push_back(message);
}
}
DialogMessage::DisplayDialogError { title, description } => {
let dialog = simple_dialogs::ErrorDialog { title, description };
dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
}
DialogMessage::RequestAboutGraphiteDialog => {
responses.push_back(
FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
}
.into(),
);
}
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date } => {
let about_graphite = AboutGraphiteDialog { localized_commit_date };
about_graphite.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() }.into());
}
DialogMessage::RequestComingSoonDialog { issue } => {
let coming_soon = ComingSoonDialog { issue };
coming_soon.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
}
DialogMessage::RequestExportDialog => {
if let Some(document) = portfolio.active_document() {
let artboard_handler = &document.artboard_message_handler;
let mut index = 0;
let artboards = artboard_handler
.artboard_ids
.iter()
.rev()
.filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer)))
.map(|(artboard, layer)| {
(
artboard,
format!(
"Artboard: {}",
layer.name.clone().unwrap_or_else(|| {
index += 1;
format!("Untitled {index}")
})
),
)
})
.collect();
self.export_dialog = ExportDialogMessageHandler {
file_name: document.name.clone(),
scale_factor: 1.,
artboards,
has_selection: document.selected_layers().next().is_some(),
..Default::default()
};
self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
}
}
DialogMessage::RequestNewDocumentDialog => {
self.new_document_dialog = NewDocumentDialogMessageHandler {
name: portfolio.generate_new_document_name(),
infinite: true,
dimensions: glam::UVec2::new(1920, 1080),
};
self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
}
}
}
advertise_actions!(DialogMessageDiscriminant;
RequestNewDocumentDialog,
RequestExportDialog,
CloseAllDocumentsWithConfirmation,
);
}
@@ -0,0 +1,15 @@
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[impl_message(Message, DialogMessage, ExportDialog)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ExportDialogMessage {
FileName(String),
FileType(FileType),
ScaleFactor(f64),
ExportBounds(ExportBounds),
Submit,
}
@@ -0,0 +1,185 @@
use super::ExportDialogMessage;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
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::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::input_widgets::{DropdownEntryData, DropdownInput, NumberInput, RadioEntryData, RadioInput, TextInput};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::*;
use graphene::LayerId;
use std::collections::HashMap;
/// A dialog to allow users to customize their file export.
#[derive(Debug, Clone, Default)]
pub struct ExportDialogMessageHandler {
pub file_name: String,
pub file_type: FileType,
pub scale_factor: f64,
pub bounds: ExportBounds,
pub artboards: HashMap<LayerId, String>,
pub has_selection: bool,
}
impl MessageHandler<ExportDialogMessage, ()> for ExportDialogMessageHandler {
fn process_message(&mut self, message: ExportDialogMessage, _data: (), responses: &mut VecDeque<Message>) {
match message {
ExportDialogMessage::FileName(name) => self.file_name = name,
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
ExportDialogMessage::ScaleFactor(x) => self.scale_factor = x,
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
ExportDialogMessage::Submit => responses.push_front(
DocumentMessage::ExportDocument {
file_name: self.file_name.clone(),
file_type: self.file_type,
scale_factor: self.scale_factor,
bounds: self.bounds,
}
.into(),
),
}
self.register_properties(responses, LayoutTarget::DialogDetails);
}
advertise_actions! {ExportDialogUpdate;}
}
impl PropertyHolder for ExportDialogMessageHandler {
fn properties(&self) -> Layout {
let file_name = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "File Name".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextInput(TextInput {
value: self.file_name.clone(),
on_update: WidgetCallback::new(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into()),
..Default::default()
})),
];
let entries = [(FileType::Svg, "SVG"), (FileType::Png, "PNG"), (FileType::Jpg, "JPG")]
.into_iter()
.map(|(val, name)| RadioEntryData {
label: name.into(),
on_update: WidgetCallback::new(move |_| ExportDialogMessage::FileType(val).into()),
..RadioEntryData::default()
})
.collect();
let export_type = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "File Type".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::RadioInput(RadioInput {
selected_index: self.file_type as u32,
entries,
})),
];
let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false));
let mut export_area_options = vec![
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
(ExportBounds::Selection, "Selection".to_string(), !self.has_selection),
];
export_area_options.extend(artboards);
let index = export_area_options.iter().position(|(val, _, _)| val == &self.bounds).unwrap();
let entries = vec![export_area_options
.into_iter()
.map(|(val, name, disabled)| DropdownEntryData {
label: name,
on_update: WidgetCallback::new(move |_| ExportDialogMessage::ExportBounds(val).into()),
disabled,
..Default::default()
})
.collect()];
let export_area = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Bounds".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
selected_index: Some(index as u32),
entries,
..Default::default()
})),
];
let resolution = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Scale Factor".into(),
table_align: true,
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(self.scale_factor),
label: "".into(),
unit: " ".into(),
min: Some(0.),
disabled: self.file_type == FileType::Svg,
on_update: WidgetCallback::new(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()),
..NumberInput::default()
})),
];
let button_widgets = vec![
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Export".to_string(),
min_width: 96,
emphasized: true,
on_update: WidgetCallback::new(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![ExportDialogMessage::Submit.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Cancel".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
})),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Export".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: file_name },
LayoutGroup::Row { widgets: export_type },
LayoutGroup::Row { widgets: resolution },
LayoutGroup::Row { widgets: export_area },
LayoutGroup::Row { widgets: button_widgets },
]))
}
}
@@ -0,0 +1,7 @@
mod export_dialog_message;
mod export_dialog_message_handler;
#[doc(inline)]
pub use export_dialog_message::{ExportDialogMessage, ExportDialogMessageDiscriminant};
#[doc(inline)]
pub use export_dialog_message_handler::ExportDialogMessageHandler;
+18
View File
@@ -0,0 +1,18 @@
//! Handles modal dialogs that appear as floating menus in the center of the editor window.
//!
//! Dialogs are represented as structs that implement the `PropertyHolder` trait.
//!
//! To open a dialog, call the function `register_properties` on the dialog struct with `responses` and the `LayoutTarget::DialogDetails` enum variant.
//! Then dialog can be opened by sending the `FrontendMessage::DisplayDialog` message;
mod dialog_message;
mod dialog_message_handler;
pub mod export_dialog;
pub mod new_document_dialog;
pub mod simple_dialogs;
#[doc(inline)]
pub use dialog_message::{DialogMessage, DialogMessageDiscriminant};
#[doc(inline)]
pub use dialog_message_handler::DialogMessageHandler;
@@ -0,0 +1,7 @@
mod new_document_dialog_message;
mod new_document_dialog_message_handler;
#[doc(inline)]
pub use new_document_dialog_message::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant};
#[doc(inline)]
pub use new_document_dialog_message_handler::NewDocumentDialogMessageHandler;
@@ -0,0 +1,14 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[impl_message(Message, DialogMessage, NewDocumentDialog)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum NewDocumentDialogMessage {
Name(String),
Infinite(bool),
DimensionsX(f64),
DimensionsY(f64),
Submit,
}
@@ -0,0 +1,158 @@
use super::NewDocumentDialogMessage;
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::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, NumberInput, TextInput};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::*;
use glam::UVec2;
/// A dialog to allow users to set some initial options about a new document.
#[derive(Debug, Clone, Default)]
pub struct NewDocumentDialogMessageHandler {
pub name: String,
pub infinite: bool,
pub dimensions: UVec2,
}
impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHandler {
fn process_message(&mut self, message: NewDocumentDialogMessage, _data: (), responses: &mut VecDeque<Message>) {
match message {
NewDocumentDialogMessage::Name(name) => self.name = name,
NewDocumentDialogMessage::Infinite(infinite) => self.infinite = infinite,
NewDocumentDialogMessage::DimensionsX(x) => self.dimensions.x = x as u32,
NewDocumentDialogMessage::DimensionsY(y) => self.dimensions.y = y as u32,
NewDocumentDialogMessage::Submit => {
responses.push_back(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }.into());
if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 {
responses.push_back(
ArtboardMessage::AddArtboard {
id: None,
position: (0., 0.),
size: (self.dimensions.x as f64, self.dimensions.y as f64),
}
.into(),
);
responses.push_back(DocumentMessage::ZoomCanvasToFitAll.into());
}
}
}
self.register_properties(responses, LayoutTarget::DialogDetails);
}
advertise_actions! {NewDocumentDialogUpdate;}
}
impl PropertyHolder for NewDocumentDialogMessageHandler {
fn properties(&self) -> Layout {
let title = vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "New document".into(),
bold: true,
..Default::default()
}))];
let name = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Name".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextInput(TextInput {
value: self.name.clone(),
on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into()),
..Default::default()
})),
];
let infinite = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Infinite Canvas".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: self.infinite,
icon: "Checkmark".to_string(),
on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()),
..Default::default()
})),
];
let scale = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Dimensions".into(),
table_align: true,
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(self.dimensions.x as f64),
label: "W".into(),
unit: " px".into(),
disabled: self.infinite,
is_integer: true,
min: Some(0.),
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into()),
..NumberInput::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(self.dimensions.y as f64),
label: "H".into(),
unit: " px".into(),
disabled: self.infinite,
is_integer: true,
min: Some(0.),
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into()),
..NumberInput::default()
})),
];
let button_widgets = vec![
WidgetHolder::new(Widget::TextButton(TextButton {
label: "OK".to_string(),
min_width: 96,
emphasized: true,
on_update: WidgetCallback::new(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![NewDocumentDialogMessage::Submit.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Cancel".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
})),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: title },
LayoutGroup::Row { widgets: name },
LayoutGroup::Row { widgets: infinite },
LayoutGroup::Row { widgets: scale },
LayoutGroup::Row { widgets: button_widgets },
]))
}
}
@@ -0,0 +1,54 @@
use crate::application::{commit_info_localized, release_series};
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use crate::messages::prelude::*;
/// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar.
pub struct AboutGraphiteDialog {
pub localized_commit_date: String,
}
impl PropertyHolder for AboutGraphiteDialog {
fn properties(&self) -> Layout {
let links = [
("Website", "https://graphite.rs"),
("Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"),
("License", "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt"),
("Third-Party Licenses", "/third-party-licenses.txt"),
];
let link_widgets = links
.into_iter()
.map(|(label, url)| {
WidgetHolder::new(Widget::TextButton(TextButton {
label: label.to_string(),
on_update: WidgetCallback::new(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()),
..Default::default()
}))
})
.collect();
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Graphite".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: release_series(),
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: commit_info_localized(self.localized_commit_date.as_str()),
multiline: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: link_widgets },
]))
}
}
@@ -0,0 +1,49 @@
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use crate::messages::prelude::*;
/// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar.
pub struct CloseAllDocumentsDialog;
impl PropertyHolder for CloseAllDocumentsDialog {
fn properties(&self) -> Layout {
let button_widgets = vec![
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Discard All".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Cancel".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
})),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Close all documents?".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Unsaved work will be lost!".to_string(),
multiline: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: button_widgets },
]))
}
}
@@ -0,0 +1,68 @@
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use crate::messages::portfolio::document::DocumentMessage;
use crate::messages::prelude::*;
/// A dialog for confirming the closing a document with unsaved changes.
pub struct CloseDocumentDialog {
pub document_name: String,
pub document_id: u64,
}
impl PropertyHolder for CloseDocumentDialog {
fn properties(&self) -> Layout {
let document_id = self.document_id;
let button_widgets = vec![
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Save".to_string(),
min_width: 96,
emphasized: true,
on_update: WidgetCallback::new(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![DocumentMessage::SaveDocument.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Discard".to_string(),
min_width: 96,
on_update: WidgetCallback::new(move |_| {
DialogMessage::CloseDialogAndThen {
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Cancel".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
})),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Save changes before closing?".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: self.document_name.clone(),
multiline: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: button_widgets },
]))
}
}
@@ -0,0 +1,55 @@
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use crate::messages::prelude::*;
use std::fmt::Write;
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
pub struct ComingSoonDialog {
pub issue: Option<i32>,
}
impl PropertyHolder for ComingSoonDialog {
fn properties(&self) -> Layout {
let mut details = "This feature is not implemented yet".to_string();
let mut buttons = vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "OK".to_string(),
emphasized: true,
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
}))];
if let Some(issue) = self.issue {
let _ = write!(details, "— but you can help add it!\nSee issue #{issue} on GitHub.");
buttons.push(WidgetHolder::new(Widget::TextButton(TextButton {
label: format!("Issue #{issue}"),
min_width: 96,
on_update: WidgetCallback::new(move |_| {
FrontendMessage::TriggerVisitLink {
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
}
.into()
}),
..Default::default()
})));
}
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Coming soon".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: details,
multiline: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: buttons },
]))
}
}
@@ -0,0 +1,40 @@
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::widgets::button_widgets::TextButton;
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use crate::messages::prelude::*;
/// A dialog to notify users of a non-fatal error.
pub struct ErrorDialog {
pub title: String,
pub description: String,
}
impl PropertyHolder for ErrorDialog {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: self.title.clone(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: self.description.clone(),
multiline: true,
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "OK".to_string(),
emphasized: true,
min_width: 96,
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
..Default::default()
}))],
},
]))
}
}
@@ -0,0 +1,11 @@
mod about_graphite_dialog;
mod close_all_documents_dialog;
mod close_document_dialog;
mod coming_soon_dialog;
mod error_dialog;
pub use about_graphite_dialog::AboutGraphiteDialog;
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
pub use close_document_dialog::CloseDocumentDialog;
pub use coming_soon_dialog::ComingSoonDialog;
pub use error_dialog::ErrorDialog;
@@ -0,0 +1,65 @@
use super::utility_types::{FrontendDocumentDetails, FrontendImageData, MouseCursorIcon};
use crate::messages::layout::utility_types::layout_widget::SubLayout;
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::menu_widgets::MenuColumn;
use crate::messages::portfolio::document::utility_types::layer_panel::{LayerPanelEntry, RawBuffer};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use graphene::color::Color;
use graphene::layers::text_layer::Font;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog
DisplayDialog { icon: String },
DisplayDialogDismiss,
DisplayDialogPanic { panic_info: String, header: String, description: String },
DisplayEditableTextbox { text: String, line_width: Option<f64>, font_size: f64, color: Color },
DisplayRemoveEditableTextbox,
// Trigger prefix: cause a browser API to do something
TriggerAboutGraphiteLocalizedCommitDate { commit_date: String },
TriggerFileDownload { document: String, name: String },
TriggerFontLoad { font: Font, is_default: bool },
TriggerImport,
TriggerIndexedDbRemoveDocument { document_id: u64 },
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
TriggerOpenDocument,
TriggerPaste,
TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) },
TriggerRefreshBoundsOfViewports,
TriggerTextCommit,
TriggerTextCopy { copy_text: String },
TriggerViewportResize,
TriggerVisitLink { url: String },
// Update prefix: give the frontend a new value or state for it to use
UpdateActiveDocument { document_id: u64 },
UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout },
UpdateDocumentArtboards { svg: String },
UpdateDocumentArtwork { svg: String },
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateDocumentLayerDetails { data: LayerPanelEntry },
UpdateDocumentLayerTreeStructure { data_buffer: RawBuffer },
UpdateDocumentModeLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateDocumentOverlays { svg: String },
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
UpdateImageData { image_data: Vec<FrontendImageData> },
UpdateInputHints { hint_data: HintData },
UpdateLayerTreeOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateMenuBarLayout { layout_target: LayoutTarget, layout: Vec<MenuColumn> },
UpdateMouseCursor { cursor: MouseCursorIcon },
UpdateNodeGraphVisibility { visible: bool },
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdatePropertyPanelOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateToolShelfLayout { layout_target: LayoutTarget, layout: SubLayout },
UpdateWorkingColorsLayout { layout_target: LayoutTarget, layout: SubLayout },
}
+6
View File
@@ -0,0 +1,6 @@
mod frontend_message;
pub mod utility_types;
#[doc(inline)]
pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant};
@@ -0,0 +1,57 @@
use graphene::LayerId;
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct FrontendDocumentDetails {
pub is_saved: bool,
pub name: String,
pub id: u64,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct FrontendImageData {
pub path: Vec<LayerId>,
pub mime: String,
pub image_data: Vec<u8>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum MouseCursorIcon {
#[default]
Default,
ZoomIn,
ZoomOut,
Grabbing,
Crosshair,
Text,
NSResize,
EWResize,
NESWResize,
NWSEResize,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum FileType {
#[default]
Svg,
Png,
Jpg,
}
impl FileType {
pub fn to_mime(self) -> &'static str {
match self {
FileType::Svg => "image/svg+xml",
FileType::Png => "image/png",
FileType::Jpg => "image/jpeg",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum ExportBounds {
#[default]
AllArtwork,
Selection,
Artboard(LayerId),
}
@@ -0,0 +1,387 @@
use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT};
use crate::messages::input_mapper::input_mapper_message::InputMapperMessage;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
use crate::messages::input_mapper::utility_types::macros::*;
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use glam::DVec2;
pub fn default_mapping() -> Mapping {
use InputMapperMessage::*;
use Key::*;
// NOTICE:
// If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise
// it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`).
let mappings = mapping![
// HIGHER PRIORITY:
//
// MovementMessage
entry!(
PointerMove;
refresh_keys=[KeyControl],
action_dispatch=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None },
),
// NORMAL PRIORITY:
//
// TransformLayerMessage
entry!(KeyDown(KeyEnter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(Lmb); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(KeyEscape); action_dispatch=TransformLayerMessage::CancelTransformOperation),
entry!(KeyDown(Rmb); action_dispatch=TransformLayerMessage::CancelTransformOperation),
entry!(KeyDown(KeyX); action_dispatch=TransformLayerMessage::ConstrainX),
entry!(KeyDown(KeyY); action_dispatch=TransformLayerMessage::ConstrainY),
entry!(KeyDown(KeyBackspace); action_dispatch=TransformLayerMessage::TypeBackspace),
entry!(KeyDown(KeyMinus); action_dispatch=TransformLayerMessage::TypeNegate),
entry!(KeyDown(KeyComma); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry!(KeyDown(KeyPeriod); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }),
//
// SelectToolMessage
entry!(PointerMove; refresh_keys=[KeyControl, KeyShift, KeyAlt], action_dispatch=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }),
entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: KeyShift }),
entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop),
entry!(KeyDown(KeyEnter); action_dispatch=SelectToolMessage::DragStop),
entry!(DoubleClick; action_dispatch=SelectToolMessage::EditLayer),
entry!(KeyDown(Rmb); action_dispatch=SelectToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=SelectToolMessage::Abort),
//
// ArtboardToolMessage
entry!(KeyDown(Lmb); action_dispatch=ArtboardToolMessage::PointerDown),
entry!(PointerMove; refresh_keys=[KeyShift, KeyAlt], action_dispatch=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }),
entry!(KeyUp(Lmb); action_dispatch=ArtboardToolMessage::PointerUp),
entry!(KeyDown(KeyDelete); action_dispatch=ArtboardToolMessage::DeleteSelected),
entry!(KeyDown(KeyBackspace); action_dispatch=ArtboardToolMessage::DeleteSelected),
//
// NavigateToolMessage
entry!(KeyUp(Lmb); modifiers=[KeyShift], action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: false }),
entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: true }),
entry!(PointerMove; refresh_keys=[KeyControl], action_dispatch=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }),
entry!(KeyDown(Mmb); action_dispatch=NavigateToolMessage::TranslateCanvasBegin),
entry!(KeyDown(Rmb); action_dispatch=NavigateToolMessage::RotateCanvasBegin),
entry!(KeyDown(Lmb); action_dispatch=NavigateToolMessage::ZoomCanvasBegin),
entry!(KeyUp(Rmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
//
// EyedropperToolMessage
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown),
//
// TextToolMessage
entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
entry!(KeyDown(KeyEscape); action_dispatch=TextToolMessage::Abort),
entry_multiplatform!(
standard!(KeyDown(KeyEnter); modifiers=[KeyControl], action_dispatch=TextToolMessage::CommitText),
mac_only!(KeyDown(KeyEnter); modifiers=[KeyCommand], action_dispatch=TextToolMessage::CommitText),
),
//
// GradientToolMessage
entry!(KeyDown(Lmb); action_dispatch=GradientToolMessage::PointerDown),
entry!(PointerMove; refresh_keys=[KeyShift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: KeyShift }),
entry!(KeyUp(Lmb); action_dispatch=GradientToolMessage::PointerUp),
//
// RectangleToolMessage
entry!(KeyDown(Lmb); action_dispatch=RectangleToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=RectangleToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=RectangleToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=RectangleToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
//
// EllipseToolMessage
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=EllipseToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=EllipseToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
//
// ShapeToolMessage
entry!(KeyDown(Lmb); action_dispatch=ShapeToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=ShapeToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=ShapeToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=ShapeToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
//
// LineToolMessage
entry!(KeyDown(Lmb); action_dispatch=LineToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=LineToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift, KeyControl], action_dispatch=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }),
//
// PathToolMessage
entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: KeyShift }),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }),
entry!(KeyDown(KeyDelete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyBackspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop),
//
// PenToolMessage
entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }),
entry!(KeyDown(Lmb); action_dispatch=PenToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=PenToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(KeyEscape); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(KeyEnter); action_dispatch=PenToolMessage::Confirm),
//
// FreehandToolMessage
entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove),
entry!(KeyDown(Lmb); action_dispatch=FreehandToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=FreehandToolMessage::DragStop),
//
// SplineToolMessage
entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove),
entry!(KeyDown(Lmb); action_dispatch=SplineToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=SplineToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=SplineToolMessage::Confirm),
entry!(KeyDown(KeyEscape); action_dispatch=SplineToolMessage::Confirm),
entry!(KeyDown(KeyEnter); action_dispatch=SplineToolMessage::Confirm),
//
// FillToolMessage
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown),
//
// ToolMessage
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate),
entry!(KeyDown(KeyI); action_dispatch=ToolMessage::ActivateToolEyedropper),
entry!(KeyDown(KeyT); action_dispatch=ToolMessage::ActivateToolText),
entry!(KeyDown(KeyF); action_dispatch=ToolMessage::ActivateToolFill),
entry!(KeyDown(KeyH); action_dispatch=ToolMessage::ActivateToolGradient),
entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath),
entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen),
entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand),
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine),
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
entry_multiplatform!(
standard!(KeyDown(KeyX); modifiers=[KeyShift, KeyControl], action_dispatch=ToolMessage::ResetColors),
mac_only!(KeyDown(KeyX); modifiers=[KeyShift, KeyCommand], action_dispatch=ToolMessage::ResetColors),
),
entry!(KeyDown(KeyX); modifiers=[KeyShift], action_dispatch=ToolMessage::SwapColors),
entry!(KeyDown(KeyC); modifiers=[KeyAlt], action_dispatch=ToolMessage::SelectRandomPrimaryColor),
//
// DocumentMessage
entry!(KeyDown(KeyDelete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyBackspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyP); modifiers=[KeyAlt], action_dispatch=DocumentMessage::DebugPrintDocument),
entry_multiplatform!(
standard!(KeyDown(KeyZ); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::Redo),
mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::Redo),
),
entry_multiplatform!(
standard!(KeyDown(KeyZ); modifiers=[KeyControl], action_dispatch=DocumentMessage::Undo),
mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand], action_dispatch=DocumentMessage::Undo),
),
entry_multiplatform!(
standard!(KeyDown(KeyA); modifiers=[KeyControl, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
mac_only!(KeyDown(KeyA); modifiers=[KeyCommand, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyA); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectAllLayers),
mac_only!(KeyDown(KeyA); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectAllLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyS); modifiers=[KeyControl], action_dispatch=DocumentMessage::SaveDocument),
mac_only!(KeyDown(KeyS); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SaveDocument),
),
entry_multiplatform!(
standard!(KeyDown(Key0); modifiers=[KeyControl], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
mac_only!(KeyDown(Key0); modifiers=[KeyCommand], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
),
entry_multiplatform!(
standard!(KeyDown(KeyD); modifiers=[KeyControl], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
mac_only!(KeyDown(KeyD); modifiers=[KeyCommand], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyG); modifiers=[KeyControl], action_dispatch=DocumentMessage::GroupSelectedLayers),
mac_only!(KeyDown(KeyG); modifiers=[KeyCommand], action_dispatch=DocumentMessage::GroupSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyG); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
mac_only!(KeyDown(KeyG); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyN); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
mac_only!(KeyDown(KeyN); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
),
entry_multiplatform!(
standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
),
entry_multiplatform!(
// TODO: Delete this in favor of the KeyLeftBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards)
standard!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
mac_only!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
),
entry_multiplatform!(
standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
),
entry_multiplatform!(
// TODO: Delete this in favor of the KeyRightBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards)
standard!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
mac_only!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
),
entry_multiplatform!(
standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersLower),
mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersLower),
),
entry_multiplatform!(
standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersRaise),
mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersRaise),
),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
//
// TransformLayerMessage
entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGrab),
entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginRotate),
entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginScale),
//
// MovementMessage
entry!(KeyDown(Mmb); modifiers=[KeyControl], action_dispatch=MovementMessage::RotateCanvasBegin),
entry!(KeyDown(Mmb); modifiers=[KeyShift], action_dispatch=MovementMessage::ZoomCanvasBegin),
entry!(KeyDown(Mmb); action_dispatch=MovementMessage::TranslateCanvasBegin),
entry!(KeyUp(Mmb); action_dispatch=MovementMessage::TransformCanvasEnd),
entry!(KeyDown(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TranslateCanvasBegin),
entry!(KeyUp(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TransformCanvasEnd),
entry_multiplatform!(
standard!(KeyDown(KeyPlus); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyPlus); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(KeyEquals); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyEquals); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(KeyMinus); modifiers=[KeyControl], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyMinus); modifiers=[KeyCommand], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(Key1); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }),
mac_only!(KeyDown(Key1); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }),
),
entry_multiplatform!(
standard!(KeyDown(Key2); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }),
mac_only!(KeyDown(Key2); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }),
),
entry!(WheelScroll; modifiers=[KeyControl], action_dispatch=MovementMessage::WheelCanvasZoom),
entry!(WheelScroll; modifiers=[KeyShift], action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }),
entry!(WheelScroll; action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }),
entry!(KeyDown(KeyPageUp); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }),
entry!(KeyDown(KeyPageDown); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(-1., 0.) }),
entry!(KeyDown(KeyPageUp); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }),
entry!(KeyDown(KeyPageDown); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }),
//
// PortfolioMessage
entry_multiplatform!(
standard!(KeyDown(KeyO); modifiers=[KeyControl], action_dispatch=PortfolioMessage::OpenDocument),
mac_only!(KeyDown(KeyO); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::OpenDocument),
),
entry_multiplatform!(
standard!(KeyDown(KeyI); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Import),
mac_only!(KeyDown(KeyI); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Import),
),
entry!(KeyDown(KeyTab); modifiers=[KeyControl], action_dispatch=PortfolioMessage::NextDocument),
entry!(KeyDown(KeyTab); modifiers=[KeyControl, KeyShift], action_dispatch=PortfolioMessage::PrevDocument),
entry_multiplatform!(
standard!(KeyDown(KeyW); modifiers=[KeyControl], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
mac_only!(KeyDown(KeyW); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
),
entry_multiplatform!(
standard!(KeyDown(KeyX); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
mac_only!(KeyDown(KeyX); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
),
entry_multiplatform!(
standard!(KeyDown(KeyC); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
mac_only!(KeyDown(KeyC); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
),
entry_multiplatform!(
// This shortcut is intercepted in the frontend; it exists here only as a shortcut mapping source
standard!(KeyDown(KeyV); modifiers=[KeyControl], action_dispatch=FrontendMessage::TriggerPaste),
mac_only!(KeyDown(KeyV); modifiers=[KeyCommand], action_dispatch=FrontendMessage::TriggerPaste),
),
//
// DialogMessage
entry_multiplatform!(
standard!(KeyDown(KeyN); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestNewDocumentDialog),
mac_only!(KeyDown(KeyN); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestNewDocumentDialog),
),
entry_multiplatform!(
standard!(KeyDown(KeyW); modifiers=[KeyControl, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
mac_only!(KeyDown(KeyW); modifiers=[KeyCommand, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
),
entry_multiplatform!(
standard!(KeyDown(KeyE); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestExportDialog),
mac_only!(KeyDown(KeyE); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestExportDialog),
),
//
// DebugMessage
entry!(KeyDown(KeyT); modifiers=[KeyAlt], action_dispatch=DebugMessage::ToggleTraceLogs),
entry!(KeyDown(Key0); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageOff),
entry!(KeyDown(Key1); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageNames),
entry!(KeyDown(Key2); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageContents),
];
let (mut key_up, mut key_down, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;
// TODO: Hardcode these 10 lines into 10 lines of declarations, or make this use a macro to do all 10 in one line
const NUMBER_KEYS: [Key; 10] = [Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9];
for (i, key) in NUMBER_KEYS.iter().enumerate() {
key_down[*key as usize].0.insert(
0,
MappingEntry {
action: TransformLayerMessage::TypeDigit { digit: i as u8 }.into(),
input: InputMapperMessage::KeyDown(*key),
platform_layout: None,
modifiers: modifiers!(),
},
);
}
let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|u, v| v.modifiers.ones().cmp(&u.modifiers.ones()));
for list in [&mut key_up, &mut key_down] {
for sublist in list {
sort(sublist);
}
}
sort(&mut double_click);
sort(&mut wheel_scroll);
sort(&mut pointer_move);
Mapping {
key_up,
key_down,
double_click,
wheel_scroll,
pointer_move,
}
}
@@ -0,0 +1,22 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, InputMapper)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum InputMapperMessage {
// Sub-messages
#[remain::unsorted]
#[child]
KeyDown(Key),
#[remain::unsorted]
#[child]
KeyUp(Key),
// Messages
DoubleClick,
PointerMove,
WheelScroll,
}
@@ -0,0 +1,99 @@
use super::utility_types::misc::Mapping;
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use std::fmt::Write;
#[derive(Debug, Default)]
pub struct InputMapperMessageHandler {
mapping: Mapping,
}
impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, KeyboardPlatformLayout, ActionList)> for InputMapperMessageHandler {
fn process_message(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, KeyboardPlatformLayout, ActionList), responses: &mut VecDeque<Message>) {
let (input, keyboard_platform, actions) = data;
if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions, keyboard_platform) {
responses.push_back(message);
}
}
advertise_actions!();
}
impl InputMapperMessageHandler {
pub fn hints(&self, actions: ActionList) -> String {
let mut output = String::new();
let mut actions = actions
.into_iter()
.flatten()
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::ActivateTool) | MessageDiscriminant::Debug(_)));
self.mapping
.key_down
.iter()
.enumerate()
.filter_map(|(i, m)| {
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
ma.map(|a| unsafe { (std::mem::transmute_copy::<usize, Key>(&i), a) })
})
.for_each(|(k, a)| {
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').last().unwrap());
});
output.replace("Key", "")
}
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant, keyboard_platform: KeyboardPlatformLayout) -> Vec<Vec<Key>> {
let key_up = self.mapping.key_up.iter();
let key_down = self.mapping.key_down.iter();
let double_click = std::iter::once(&self.mapping.double_click);
let wheel_scroll = std::iter::once(&self.mapping.wheel_scroll);
let pointer_move = std::iter::once(&self.mapping.pointer_move);
let all_key_mapping_entries = key_up.chain(key_down).chain(double_click).chain(wheel_scroll).chain(pointer_move);
let all_mapping_entries = all_key_mapping_entries.flat_map(|entry| entry.0.iter());
// Filter for the desired message
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
// Filter for a compatible keyboard platform layout
let found_actions = found_actions.filter(|entry| if let Some(layout) = entry.platform_layout { layout == keyboard_platform } else { true });
// Find the key combinations for all keymaps matching the desired action
assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<Key>());
found_actions
.map(|entry| {
let mut keys = entry
.modifiers
.iter()
.map(|i| {
// TODO: Use a safe solution eventually
assert!(
i < input_keyboard::NUMBER_OF_KEYS,
"Attempting to convert a Key with enum index {}, which is larger than the number of Key enums",
i
);
unsafe { std::mem::transmute_copy::<usize, Key>(&i) }
})
.collect::<Vec<_>>();
if let InputMapperMessage::KeyDown(key) = entry.input {
keys.push(key);
}
keys.sort_by(|a, b| {
// Order according to platform guidelines mentioned at https://ux.stackexchange.com/questions/58185/normative-ordering-for-modifier-key-combinations
const ORDER: [Key; 4] = [Key::KeyControl, Key::KeyAlt, Key::KeyShift, Key::KeyCommand];
match (ORDER.contains(a), ORDER.contains(b)) {
(true, true) => ORDER.iter().position(|key| key == a).unwrap().cmp(&ORDER.iter().position(|key| key == b).unwrap()),
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
(false, false) => std::cmp::Ordering::Equal,
}
});
keys
})
.collect::<Vec<_>>()
}
}
+10
View File
@@ -0,0 +1,10 @@
mod input_mapper_message;
mod input_mapper_message_handler;
pub mod default_mapping;
pub mod utility_types;
#[doc(inline)]
pub use input_mapper_message::{InputMapperMessage, InputMapperMessageDiscriminant};
#[doc(inline)]
pub use input_mapper_message_handler::InputMapperMessageHandler;
@@ -0,0 +1,285 @@
use crate::messages::prelude::*;
pub use graphene::DocumentResponse;
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign};
// TODO: Increase size of type
/// Edit this to specify the storage type used.
pub type StorageType = u128;
// Base-2 logarithm of the storage type used to represents how many bits you need to fully address every bit in that storage type
const STORAGE_SIZE: u32 = (std::mem::size_of::<StorageType>() * 8).trailing_zeros();
const STORAGE_SIZE_BITS: usize = 1 << STORAGE_SIZE;
const KEY_MASK_STORAGE_LENGTH: usize = (NUMBER_OF_KEYS + STORAGE_SIZE_BITS - 1) >> STORAGE_SIZE;
pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
pub enum KeyPosition {
Pressed,
Released,
}
bitflags! {
#[derive(Default, Serialize, Deserialize)]
#[repr(transparent)]
pub struct ModifierKeys: u8 {
const SHIFT = 0b0000_0001;
const ALT = 0b0000_0010;
const CONTROL = 0b0000_0100;
const META_OR_COMMAND = 0b0000_1000;
}
}
// TODO: Consider renaming to `KeyMessage` for consistency with other messages that implement `#[impl_message(..)]`
#[impl_message(Message, InputMapperMessage, KeyDown)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Key {
UnknownKey,
// Mouse keys
Lmb,
Rmb,
Mmb,
// Keyboard keys
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
Key0,
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
KeyEnter,
KeyEquals,
KeyMinus,
KeyPlus,
KeyShift,
KeySpace,
KeyControl,
KeyCommand,
KeyMeta,
KeyDelete,
KeyBackspace,
KeyAlt,
KeyEscape,
KeyTab,
KeyArrowUp,
KeyArrowDown,
KeyArrowLeft,
KeyArrowRight,
KeyLeftBracket,
KeyRightBracket,
KeyLeftCurlyBracket,
KeyRightCurlyBracket,
KeyPageUp,
KeyPageDown,
KeyComma,
KeyPeriod,
// This has to be the last element in the enum
NumKeys,
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
let key_name = format!("{:?}", self);
let name = if &key_name[0..3] == "Key" { key_name.chars().skip(3).collect::<String>() } else { key_name };
write!(f, "{}", name)
}
}
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
/// Only `Key`s that exist on a physical keyboard should be used.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeysGroup(pub Vec<Key>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MouseMotion {
None,
Lmb,
Rmb,
Mmb,
ScrollUp,
ScrollDown,
Drag,
LmbDrag,
RmbDrag,
MmbDrag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);
impl<const LENGTH: usize> BitVector<LENGTH> {
#[inline]
fn convert_index(bitvector_index: usize) -> (usize, StorageType) {
let bit = 1 << (bitvector_index & (STORAGE_SIZE_BITS as StorageType - 1) as usize);
let offset = bitvector_index >> STORAGE_SIZE;
(offset, bit)
}
pub const fn new() -> Self {
Self([0; LENGTH])
}
pub fn set(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] |= bit;
}
pub fn unset(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] &= !bit;
}
pub fn toggle(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] ^= bit;
}
pub fn get(&self, bitvector_index: usize) -> bool {
let (offset, bit) = Self::convert_index(bitvector_index);
(self.0[offset] & bit) != 0
}
pub fn is_empty(&self) -> bool {
let mut result = 0;
for storage in self.0.iter() {
result |= storage;
}
result == 0
}
pub fn ones(&self) -> u32 {
let mut result = 0;
for storage in self.0.iter() {
result += storage.count_ones();
}
result
}
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
BitVectorIter::<LENGTH> { bitvector: self, iter_index: 0 }
}
}
impl<const LENGTH: usize> Default for BitVector<LENGTH> {
fn default() -> Self {
Self::new()
}
}
struct BitVectorIter<'a, const LENGTH: usize> {
bitvector: &'a BitVector<LENGTH>,
iter_index: usize,
}
impl<'a, const LENGTH: usize> Iterator for BitVectorIter<'a, LENGTH> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
while self.iter_index < (STORAGE_SIZE_BITS as usize) * LENGTH {
let bit_value = self.bitvector.get(self.iter_index);
self.iter_index += 1;
if bit_value {
return Some(self.iter_index - 1);
}
}
None
}
}
impl<const LENGTH: usize> Display for BitVector<LENGTH> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for storage in self.0.iter().rev() {
write!(f, "{:0width$b}", storage, width = STORAGE_SIZE_BITS)?;
}
Ok(())
}
}
macro_rules! bit_ops {
($(($op:ident, $func:ident)),* $(,)?) => {
$(
impl<const LENGTH: usize> $op for BitVector<LENGTH> {
type Output = Self;
fn $func(self, right: Self) -> Self::Output {
let mut result = Self::new();
for ((left, right), new) in self.0.iter().zip(right.0.iter()).zip(result.0.iter_mut()) {
*new = $op::$func(left, right);
}
result
}
}
impl<const LENGTH: usize> $op for &BitVector<LENGTH> {
type Output = BitVector<LENGTH>;
fn $func(self, right: Self) -> Self::Output {
let mut result = BitVector::<LENGTH>::new();
for ((left, right), new) in self.0.iter().zip(right.0.iter()).zip(result.0.iter_mut()) {
*new = $op::$func(left, right);
}
result
}
}
)*
};
}
macro_rules! bit_ops_assign {
($(($op:ident, $func:ident)),* $(,)?) => {
$(impl<const LENGTH: usize> $op for BitVector<LENGTH> {
fn $func(&mut self, right: Self) {
for (left, right) in self.0.iter_mut().zip(right.0.iter()) {
$op::$func(left, right);
}
}
})*
};
}
bit_ops!((BitAnd, bitand), (BitOr, bitor), (BitXor, bitxor));
bit_ops_assign!((BitAndAssign, bitand_assign), (BitOrAssign, bitor_assign), (BitXorAssign, bitxor_assign));
@@ -0,0 +1,134 @@
use bitflags::bitflags;
use glam::DVec2;
use serde::{Deserialize, Serialize};
// Origin is top left
pub type ViewportPosition = DVec2;
pub type EditorPosition = DVec2;
#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize)]
pub struct ViewportBounds {
pub top_left: DVec2,
pub bottom_right: DVec2,
}
impl ViewportBounds {
pub fn from_slice(slice: &[f64]) -> Self {
Self {
top_left: DVec2::from_slice(&slice[0..2]),
bottom_right: DVec2::from_slice(&slice[2..4]),
}
}
pub fn size(&self) -> DVec2 {
self.bottom_right - self.top_left
}
pub fn center(&self) -> DVec2 {
self.bottom_right.lerp(self.top_left, 0.5)
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ScrollDelta {
// TODO: Switch these to `f64` values (not trivial because floats don't provide PartialEq, Eq, and Hash)
pub x: i32,
pub y: i32,
pub z: i32,
}
impl ScrollDelta {
pub fn new(x: i32, y: i32, z: i32) -> Self {
Self { x, y, z }
}
pub fn as_dvec2(&self) -> DVec2 {
DVec2::new(self.x as f64, self.y as f64)
}
pub fn scroll_delta(&self) -> f64 {
let (dx, dy) = (self.x, self.y);
dy.signum() as f64 * ((dy * dy + i32::min(dy.abs(), dx.abs()).pow(2)) as f64).sqrt()
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct MouseState {
pub position: ViewportPosition,
pub mouse_keys: MouseKeys,
pub scroll_delta: ScrollDelta,
}
impl MouseState {
pub fn new() -> Self {
Self::default()
}
pub fn from_position(x: f64, y: f64) -> Self {
Self {
position: (x, y).into(),
mouse_keys: MouseKeys::default(),
scroll_delta: ScrollDelta::default(),
}
}
pub fn from_keys_and_editor_position(keys: u8, position: ViewportPosition) -> Self {
let mouse_keys = MouseKeys::from_bits(keys).expect("Invalid modifier keys");
Self {
position,
mouse_keys,
scroll_delta: ScrollDelta::default(),
}
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EditorMouseState {
pub editor_position: EditorPosition,
pub mouse_keys: MouseKeys,
pub scroll_delta: ScrollDelta,
}
impl EditorMouseState {
pub fn new() -> Self {
Self::default()
}
pub fn from_editor_position(x: f64, y: f64) -> Self {
Self {
editor_position: (x, y).into(),
mouse_keys: MouseKeys::default(),
scroll_delta: ScrollDelta::default(),
}
}
pub fn from_keys_and_editor_position(keys: u8, editor_position: EditorPosition) -> Self {
let mouse_keys = MouseKeys::from_bits(keys).expect("Invalid modifier keys");
Self {
editor_position,
mouse_keys,
scroll_delta: ScrollDelta::default(),
}
}
pub fn to_mouse_state(&self, active_viewport_bounds: &ViewportBounds) -> MouseState {
MouseState {
position: self.editor_position - active_viewport_bounds.top_left,
mouse_keys: self.mouse_keys,
scroll_delta: self.scroll_delta,
}
}
}
bitflags! {
#[derive(Default, Serialize, Deserialize)]
#[repr(transparent)]
pub struct MouseKeys: u8 {
const LEFT = 0b0000_0001;
const RIGHT = 0b0000_0010;
const MIDDLE = 0b0000_0100;
const NONE = 0b0000_0000;
}
}
@@ -0,0 +1,167 @@
/// Constructs a `KeyStates` bit vector and sets the bit flags for all the given modifier `Key`s.
macro_rules! modifiers {
($($m:ident),*) => {{
#[allow(unused_mut)]
let mut state = KeyStates::new();
$(
state.set(Key::$m as usize);
)*
state
}};
}
/// Builds a slice of `MappingEntry` struct(s) that are used to:
/// - ...dispatch the given `action_dispatch` as an output `Message` if its discriminant is a currently available action
/// - ...when the `InputMapperMessage` enum variant, as specified at the start and followed by a semicolon, is received
/// - ...while any further conditions are met, like the optional `modifiers` being pressed or `layout` matching the OS.
///
/// Syntax:
/// ```rs
/// entry_for_layout!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message, layout: Option<KeyboardPlatformLayout>)
/// ```
///
/// To avoid having to specify the final `layout` argument, instead use the wrapper macros: [entry]!, [standard]!, and [mac]!.
/// The former sets the layout to `None` which means the key mapping is layout-agnostic and compatible with all platforms.
///
/// The actions system controls which actions are currently available. Those are provided by the different message handlers based on the current application state and context.
/// Each handler adds or removes actions in the form of message discriminants. Here, we tie an input condition (such as a hotkey) to an action's full message.
/// When an action is currently available, and the user enters that input, the action's message is dispatched on the message bus.
macro_rules! entry_for_layout {
($input:expr; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? action_dispatch=$action_dispatch:expr,$(,)? layout=$layout:expr) => {
&[
// Cause the `action_dispatch` message to be sent when the specified input occurs.
MappingEntry {
action: $action_dispatch.into(),
input: $input,
modifiers: modifiers!($($($modifier),*)?),
platform_layout: $layout,
},
// Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change.
//
// For example, a snapping state bound to the Shift key may change if the user presses or releases that key.
// In that case, we want to dispatch the action's message even though the pointer didn't necessarily move so
// the input handler can update the snapping state without making the user move the mouse to see the change.
$(
$(
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyDown(Key::$refresh),
modifiers: modifiers!(),
platform_layout: $layout,
},
MappingEntry {
action: $action_dispatch.into(),
input: InputMapperMessage::KeyUp(Key::$refresh),
modifiers: modifiers!(),
platform_layout: $layout,
},
)*
)*
]
};
}
/// Wraps [entry_for_layout]! and calls it with an agnostic (`None`) keyboard platform `layout` to avoid having to specify that argument.
///
/// Syntax:
/// ```rs
/// entry!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message)
/// ```
macro_rules! entry {
($($arg:tt)*) => {
&[entry_for_layout!($($arg)*, layout=None)]
};
}
/// Wraps [entry_for_layout]! and calls it with a `Standard` keyboard platform `layout` to avoid having to specify that argument.
///
/// Syntax:
/// ```rs
/// standard!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message)
/// ```
macro_rules! standard {
($($arg:tt)*) => {
entry_for_layout!($($arg)*, layout=Some(KeyboardPlatformLayout::Standard))
};
}
/// Wraps [entry_for_layout]! and calls it with a `Mac` keyboard platform `layout` to avoid having to specify that argument.
///
/// Syntax:
/// ```rs
/// mac_only!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message)
/// ```
macro_rules! mac_only {
($($arg:tt)*) => {
entry_for_layout!($($arg)*, layout=Some(KeyboardPlatformLayout::Mac))
};
}
/// Groups multiple related entries for different platforms.
/// When a keyboard shortcut is not platform-agnostic, this should be used to contain a [mac]! and/or [standard]! entry.
///
/// Syntax:
///
/// ```rs
/// entry_multiplatform!(
/// standard!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message),
/// mac_only!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message),
/// )
/// ```
macro_rules! entry_multiplatform {
{$($arg:expr),*,} => {
&[$($arg ),*]
};
}
/// Constructs a `KeyMappingEntries` list for each input type and inserts every given entry into the list corresponding to its input type.
/// Returns a tuple of `KeyMappingEntries` in the order:
/// ```rs
/// (key_up, key_down, double_click, wheel_scroll, pointer_move)
/// ```
macro_rules! mapping {
[$($entry:expr),* $(,)?] => {{
let mut key_up = KeyMappingEntries::key_array();
let mut key_down = KeyMappingEntries::key_array();
let mut double_click = KeyMappingEntries::new();
let mut wheel_scroll = KeyMappingEntries::new();
let mut pointer_move = KeyMappingEntries::new();
$(
// Each of the many entry slices, one specified per action
for entry_slice in $entry {
// Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries)
for entry in entry_slice.into_iter() {
let corresponding_list = match entry.input {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
InputMapperMessage::DoubleClick => &mut double_click,
InputMapperMessage::WheelScroll => &mut wheel_scroll,
InputMapperMessage::PointerMove => &mut pointer_move,
};
// Push each entry to the corresponding `KeyMappingEntries` list for its input type
corresponding_list.push(entry.clone());
}
}
)*
(key_up, key_down, double_click, wheel_scroll, pointer_move)
}};
}
/// Constructs an `ActionKeys` macro with a certain `Action` variant, conveniently wrapped in `Some()`.
macro_rules! action_keys {
($action:expr) => {
Some(crate::messages::input_mapper::utility_types::misc::ActionKeys::Action($action.into()))
};
}
pub(crate) use action_keys;
pub(crate) use entry;
pub(crate) use entry_for_layout;
pub(crate) use entry_multiplatform;
pub(crate) use mac_only;
pub(crate) use mapping;
pub(crate) use modifiers;
pub(crate) use standard;
@@ -0,0 +1,148 @@
use crate::messages::input_mapper::default_mapping::default_mapping;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, NUMBER_OF_KEYS};
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct Mapping {
pub key_up: [KeyMappingEntries; NUMBER_OF_KEYS],
pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS],
pub double_click: KeyMappingEntries,
pub wheel_scroll: KeyMappingEntries,
pub pointer_move: KeyMappingEntries,
}
impl Mapping {
pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option<Message> {
let list = match message {
InputMapperMessage::KeyDown(key) => &self.key_down[key as usize],
InputMapperMessage::KeyUp(key) => &self.key_up[key as usize],
InputMapperMessage::DoubleClick => &self.double_click,
InputMapperMessage::WheelScroll => &self.wheel_scroll,
InputMapperMessage::PointerMove => &self.pointer_move,
};
list.match_mapping(keyboard_state, actions, keyboard_platform)
}
}
impl Default for Mapping {
fn default() -> Self {
default_mapping()
}
}
#[derive(Debug, Clone)]
pub struct KeyMappingEntries(pub Vec<MappingEntry>);
impl KeyMappingEntries {
pub fn match_mapping(&self, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option<Message> {
for entry in self.0.iter() {
// Skip this entry if it is platform-specific, and for a layout that does not match the user's keyboard platform layout
if let Some(entry_platform_layout) = entry.platform_layout {
if entry_platform_layout != keyboard_platform {
continue;
}
}
// Find which currently pressed keys are also the modifiers in this hotkey entry, then compare those against the required modifiers to see if there are zero missing
let pressed_modifiers = *keyboard_state & entry.modifiers;
let all_modifiers_without_pressed_modifiers = entry.modifiers ^ pressed_modifiers;
let all_required_modifiers_pressed = all_modifiers_without_pressed_modifiers.is_empty();
// Skip this entry if any of the required modifiers are missing
if !all_required_modifiers_pressed {
continue;
}
if actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) {
return Some(entry.action.clone());
}
}
None
}
pub fn push(&mut self, entry: MappingEntry) {
self.0.push(entry)
}
pub const fn new() -> Self {
Self(Vec::new())
}
pub fn key_array() -> [Self; NUMBER_OF_KEYS] {
const DEFAULT: KeyMappingEntries = KeyMappingEntries::new();
[DEFAULT; NUMBER_OF_KEYS]
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct MappingEntry {
/// Serves two purposes:
/// - This is the message that gets dispatched when the hotkey is matched
/// - This message's discriminant is the action; it must be a currently active action to be considered as a shortcut
pub action: Message,
/// The user input event from an input device which this input mapping matches on
pub input: InputMapperMessage,
/// Any additional keys that must be also pressed for this input mapping to match
pub modifiers: KeyStates,
/// The keyboard platform layout which this mapping is exclusive to, or `None` if it's platform-agnostic
pub platform_layout: Option<KeyboardPlatformLayout>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ActionKeys {
Action(MessageDiscriminant),
#[serde(rename = "keys")]
Keys(Vec<Key>),
}
impl ActionKeys {
pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>) {
match self {
ActionKeys::Action(action) => {
if let Some(keys) = action_input_mapping(action).get_mut(0) {
let mut taken_keys = Vec::new();
std::mem::swap(keys, &mut taken_keys);
*self = ActionKeys::Keys(taken_keys);
} else {
*self = ActionKeys::Keys(Vec::new());
}
}
ActionKeys::Keys(keys) => {
log::warn!("Calling `.to_keys()` on a `ActionKeys::Keys` is a mistake/bug. Keys are: {:?}.", keys);
}
}
}
}
pub fn keys_text_shortcut(keys: &[Key], keyboard_platform: KeyboardPlatformLayout) -> String {
const JOINER_MARK: &str = "+";
let mut joined = keys
.iter()
.map(|key| {
let key_string = key.to_string();
if keyboard_platform == KeyboardPlatformLayout::Mac {
match key_string.as_str() {
"Command" => "⌘".to_string(),
"Control" => "⌃".to_string(),
"Alt" => "⌥".to_string(),
"Shift" => "⇧".to_string(),
_ => key_string + JOINER_MARK,
}
} else {
key_string + JOINER_MARK
}
})
.collect::<String>();
// Truncate to cut the joining character off the end if it's present
if joined.ends_with(JOINER_MARK) {
joined.truncate(joined.len() - JOINER_MARK.len());
}
joined
}
@@ -0,0 +1,4 @@
pub mod input_keyboard;
pub mod input_mouse;
pub mod macros;
pub mod misc;
@@ -0,0 +1,22 @@
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ViewportBounds};
use crate::messages::prelude::*;
#[doc(inline)]
pub use graphene::DocumentResponse;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum InputPreprocessorMessage {
BoundsOfViewports { bounds_of_viewports: Vec<ViewportBounds> },
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
KeyDown { key: Key, modifier_keys: ModifierKeys },
KeyUp { key: Key, modifier_keys: ModifierKeys },
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
}
@@ -0,0 +1,263 @@
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{MouseKeys, MouseState, ViewportBounds};
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
#[doc(inline)]
pub use graphene::DocumentResponse;
use glam::DVec2;
#[derive(Debug, Default)]
pub struct InputPreprocessorMessageHandler {
pub keyboard: KeyStates,
pub mouse: MouseState,
pub viewport_bounds: ViewportBounds,
}
impl MessageHandler<InputPreprocessorMessage, KeyboardPlatformLayout> for InputPreprocessorMessageHandler {
#[remain::check]
fn process_message(&mut self, message: InputPreprocessorMessage, data: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
let keyboard_platform = data;
#[remain::sorted]
match message {
InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } => {
assert_eq!(bounds_of_viewports.len(), 1, "Only one viewport is currently supported");
for bounds in bounds_of_viewports {
let new_size = bounds.size();
let existing_size = self.viewport_bounds.size();
let translation = (new_size - existing_size) / 2.;
// TODO: Extend this to multiple viewports instead of setting it to the value of this last loop iteration
self.viewport_bounds = bounds;
responses.push_back(
graphene::Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
);
responses.push_back(
DocumentMessage::Artboard(
graphene::Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
)
.into(),
);
responses.push_back(FrontendMessage::TriggerViewportResize.into());
}
}
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
responses.push_back(InputMapperMessage::DoubleClick.into());
}
InputPreprocessorMessage::KeyDown { key, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
InputPreprocessorMessage::KeyUp { key, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
}
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
self.translate_mouse_event(mouse_state, true, responses);
}
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
responses.push_back(InputMapperMessage::PointerMove.into());
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
self.handle_modifier_keys(modifier_keys, keyboard_platform, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
self.mouse.scroll_delta = mouse_state.scroll_delta;
responses.push_back(InputMapperMessage::WheelScroll.into());
}
};
}
// Clean user input and if possible reconstruct it.
// Store the changes in the keyboard if it is a key event.
// Transform canvas coordinates to document coordinates.
advertise_actions!();
}
impl InputPreprocessorMessageHandler {
fn translate_mouse_event(&mut self, mut new_state: MouseState, allow_first_button_down: bool, responses: &mut VecDeque<Message>) {
for (bit_flag, key) in [(MouseKeys::LEFT, Key::Lmb), (MouseKeys::RIGHT, Key::Rmb), (MouseKeys::MIDDLE, Key::Mmb)] {
// Calculate the intersection between the two key states
let old_down = self.mouse.mouse_keys & bit_flag == bit_flag;
let new_down = new_state.mouse_keys & bit_flag == bit_flag;
if !old_down && new_down {
if allow_first_button_down || self.mouse.mouse_keys != MouseKeys::NONE {
responses.push_back(InputMapperMessage::KeyDown(key).into());
} else {
// Required to stop a keyup being emitted for a keydown outside canvas
new_state.mouse_keys ^= bit_flag;
}
}
if old_down && !new_down {
responses.push_back(InputMapperMessage::KeyUp(key).into());
}
}
self.mouse = new_state;
}
fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, keyboard_platform: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
self.handle_modifier_key(Key::KeyShift, modifier_keys.contains(ModifierKeys::SHIFT), responses);
self.handle_modifier_key(Key::KeyAlt, modifier_keys.contains(ModifierKeys::ALT), responses);
self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses);
let meta_or_command = match keyboard_platform {
KeyboardPlatformLayout::Mac => Key::KeyCommand,
KeyboardPlatformLayout::Standard => Key::KeyMeta,
};
self.handle_modifier_key(meta_or_command, modifier_keys.contains(ModifierKeys::META_OR_COMMAND), responses);
}
fn handle_modifier_key(&mut self, key: Key, key_is_down: bool, responses: &mut VecDeque<Message>) {
let key_was_down = self.keyboard.get(key as usize);
if key_was_down && !key_is_down {
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
} else if !key_was_down && key_is_down {
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
}
pub fn document_bounds(&self) -> [DVec2; 2] {
// IPP bounds are relative to the entire application
[(0., 0.).into(), self.viewport_bounds.bottom_right - self.viewport_bounds.top_left]
}
}
#[cfg(test)]
mod test {
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
use crate::messages::input_mapper::InputMapperMessage;
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use std::collections::VecDeque;
#[test]
fn process_action_mouse_move_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
let editor_mouse_state = EditorMouseState::from_editor_position(4., 809.);
let modifier_keys = ModifierKeys::ALT;
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
let mut responses = VecDeque::new();
input_preprocessor.process_message(message, KeyboardPlatformLayout::Standard, &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyAlt as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into()));
}
#[test]
fn process_action_mouse_down_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
let editor_mouse_state = EditorMouseState::new();
let modifier_keys = ModifierKeys::CONTROL;
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
let mut responses = VecDeque::new();
input_preprocessor.process_message(message, KeyboardPlatformLayout::Standard, &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into()));
}
#[test]
fn process_action_mouse_up_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
let editor_mouse_state = EditorMouseState::new();
let modifier_keys = ModifierKeys::SHIFT;
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
let mut responses = VecDeque::new();
input_preprocessor.process_message(message, KeyboardPlatformLayout::Standard, &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into()));
}
#[test]
fn process_action_key_down_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
input_preprocessor.keyboard.set(Key::KeyControl as usize);
let key = Key::KeyA;
let modifier_keys = ModifierKeys::empty();
let message = InputPreprocessorMessage::KeyDown { key, modifier_keys };
let mut responses = VecDeque::new();
input_preprocessor.process_message(message, KeyboardPlatformLayout::Standard, &mut responses);
assert!(!input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into()));
}
#[test]
fn process_action_key_up_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
let key = Key::KeyS;
let modifier_keys = ModifierKeys::CONTROL | ModifierKeys::SHIFT;
let message = InputPreprocessorMessage::KeyUp { key, modifier_keys };
let mut responses = VecDeque::new();
input_preprocessor.process_message(message, KeyboardPlatformLayout::Standard, &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize));
assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into()));
assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into()));
}
}
@@ -0,0 +1,7 @@
mod input_preprocessor_message;
mod input_preprocessor_message_handler;
#[doc(inline)]
pub use input_preprocessor_message::{InputPreprocessorMessage, InputPreprocessorMessageDiscriminant};
#[doc(inline)]
pub use input_preprocessor_message_handler::InputPreprocessorMessageHandler;
@@ -0,0 +1,14 @@
use super::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::layout_widget::Layout;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Layout)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum LayoutMessage {
RefreshLayout { layout_target: LayoutTarget },
SendLayout { layout: Layout, layout_target: LayoutTarget },
UpdateLayout { layout_target: LayoutTarget, widget_id: u64, value: serde_json::Value },
}
@@ -0,0 +1,212 @@
use super::utility_types::misc::LayoutTarget;
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::layout::utility_types::layout_widget::Layout;
use crate::messages::layout::utility_types::layout_widget::Widget;
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use graphene::layers::text_layer::Font;
use serde_json::Value;
use std::collections::VecDeque;
#[derive(Debug, Clone, Default)]
pub struct LayoutMessageHandler {
layouts: [Layout; LayoutTarget::LayoutTargetLength as usize],
}
impl<F: Fn(&MessageDiscriminant) -> Vec<Vec<Key>>> MessageHandler<LayoutMessage, (F, KeyboardPlatformLayout)> for LayoutMessageHandler {
#[remain::check]
fn process_message(&mut self, message: LayoutMessage, data: (F, KeyboardPlatformLayout), responses: &mut std::collections::VecDeque<Message>) {
let (action_input_mapping, keyboard_platform) = data;
use LayoutMessage::*;
#[remain::sorted]
match message {
RefreshLayout { layout_target } => {
self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform);
}
SendLayout { layout, layout_target } => {
self.layouts[layout_target as usize] = layout;
self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform);
}
UpdateLayout { layout_target, widget_id, value } => {
let layout = &mut self.layouts[layout_target as usize];
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id);
if widget_holder.is_none() {
log::trace!(
"Could not find widget_id:{} on layout_target:{:?}. This could be an indication of a problem or just a user clicking off of an actively edited layer",
widget_id,
layout_target
);
return;
}
#[remain::sorted]
match &mut widget_holder.unwrap().widget {
Widget::CheckboxInput(checkbox_input) => {
let update_value = value.as_bool().expect("CheckboxInput update was not of type: bool");
checkbox_input.checked = update_value;
let callback_message = (checkbox_input.on_update.callback)(checkbox_input);
responses.push_back(callback_message);
}
Widget::ColorInput(color_input) => {
let update_value = value.as_str().map(String::from);
color_input.value = update_value;
let callback_message = (color_input.on_update.callback)(color_input);
responses.push_back(callback_message);
}
Widget::DropdownInput(dropdown_input) => {
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
dropdown_input.selected_index = Some(update_value as u32);
let callback_message = (dropdown_input.entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&());
responses.push_back(callback_message);
}
Widget::FontInput(font_input) => {
let update_value = value.as_object().expect("FontInput update was not of type: object");
let font_family_value = update_value.get("fontFamily").expect("FontInput update does not have a fontFamily");
let font_style_value = update_value.get("fontStyle").expect("FontInput update does not have a fontStyle");
let font_family = font_family_value.as_str().expect("FontInput update fontFamily was not of type: string");
let font_style = font_style_value.as_str().expect("FontInput update fontStyle was not of type: string");
font_input.font_family = font_family.into();
font_input.font_style = font_style.into();
responses.push_back(
PortfolioMessage::LoadFont {
font: Font::new(font_family.into(), font_style.into()),
is_default: false,
}
.into(),
);
let callback_message = (font_input.on_update.callback)(font_input);
responses.push_back(callback_message);
}
Widget::IconButton(icon_button) => {
let callback_message = (icon_button.on_update.callback)(icon_button);
responses.push_back(callback_message);
}
Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = (invisible.on_update.callback)(&());
responses.push_back(callback_message);
}
Widget::NumberInput(number_input) => match value {
Value::Number(num) => {
let update_value = num.as_f64().unwrap();
number_input.value = Some(update_value);
let callback_message = (number_input.on_update.callback)(number_input);
responses.push_back(callback_message);
}
Value::String(str) => match str.as_str() {
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
_ => {
panic!("Invalid string found when updating `NumberInput`")
}
},
_ => panic!("Invalid type found when updating `NumberInput`"),
},
Widget::OptionalInput(optional_input) => {
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
optional_input.checked = update_value;
let callback_message = (optional_input.on_update.callback)(optional_input);
responses.push_back(callback_message);
}
Widget::PopoverButton(_) => {}
Widget::RadioInput(radio_input) => {
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
radio_input.selected_index = update_value as u32;
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
responses.push_back(callback_message);
}
Widget::Separator(_) => {}
Widget::SwatchPairInput(_) => {}
Widget::TextAreaInput(text_area_input) => {
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
text_area_input.value = update_value.into();
let callback_message = (text_area_input.on_update.callback)(text_area_input);
responses.push_back(callback_message);
}
Widget::TextButton(text_button) => {
let callback_message = (text_button.on_update.callback)(text_button);
responses.push_back(callback_message);
}
Widget::TextInput(text_input) => {
let update_value = value.as_str().expect("TextInput update was not of type: string");
text_input.value = update_value.into();
let callback_message = (text_input.on_update.callback)(text_input);
responses.push_back(callback_message);
}
Widget::TextLabel(_) => {}
};
responses.push_back(RefreshLayout { layout_target }.into());
}
}
}
fn actions(&self) -> ActionList {
actions!()
}
}
impl LayoutMessageHandler {
#[remain::check]
fn send_layout(
&self,
layout_target: LayoutTarget,
responses: &mut VecDeque<Message>,
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>,
keyboard_platform: KeyboardPlatformLayout,
) {
let layout = &self.layouts[layout_target as usize];
#[remain::sorted]
let message = match layout_target {
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout {
layout_target,
layout: layout.clone().unwrap_menu_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout {
layout_target,
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
},
#[remain::unsorted]
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
};
responses.push_back(message.into());
}
}
+9
View File
@@ -0,0 +1,9 @@
mod layout_message;
mod layout_message_handler;
pub mod utility_types;
#[doc(inline)]
pub use layout_message::{LayoutMessage, LayoutMessageDiscriminant};
#[doc(inline)]
pub use layout_message_handler::LayoutMessageHandler;
@@ -0,0 +1,292 @@
use super::widgets::button_widgets::*;
use super::widgets::input_widgets::*;
use super::widgets::label_widgets::*;
use super::widgets::menu_widgets::MenuLayout;
use crate::application::generate_uuid;
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::input_mapper::utility_types::misc::{keys_text_shortcut, ActionKeys};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::LayoutMessage;
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::rc::Rc;
pub trait PropertyHolder {
fn properties(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::default())
}
fn register_properties(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
responses.push_back(
LayoutMessage::SendLayout {
layout: self.properties(),
layout_target,
}
.into(),
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Layout {
WidgetLayout(WidgetLayout),
MenuLayout(MenuLayout),
}
impl Layout {
pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, keyboard_platform: KeyboardPlatformLayout) -> WidgetLayout {
if let Layout::WidgetLayout(mut widget_layout) = self {
// Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip
let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| {
tooltip_shortcut.to_keys(action_input_mapping);
if let ActionKeys::Keys(keys) = tooltip_shortcut {
let shortcut_text = keys_text_shortcut(keys, keyboard_platform);
if !shortcut_text.is_empty() {
if !tooltip.is_empty() {
tooltip.push(' ');
}
tooltip.push('(');
tooltip.push_str(&shortcut_text);
tooltip.push(')');
}
}
};
// Go through each widget to convert `ActionKeys::Action` to `ActionKeys::Keys` and append the key combination to the widget tooltip
for widget_holder in &mut widget_layout.iter_mut() {
// Handle all the widgets that have tooltips
let mut tooltip_shortcut = match &mut widget_holder.widget {
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
_ => None,
};
if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut {
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
}
// Handle RadioInput separately because its tooltips are children of the widget
if let Widget::RadioInput(radio_input) = &mut widget_holder.widget {
for radio_entry_data in &mut radio_input.entries {
if let RadioEntryData {
tooltip,
tooltip_shortcut: Some(tooltip_shortcut),
..
} = radio_entry_data
{
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
}
}
}
}
widget_layout
} else {
panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self)
}
}
pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, _keyboard_platform: KeyboardPlatformLayout) -> MenuLayout {
if let Layout::MenuLayout(mut menu_layout) = self {
for menu_column in &mut menu_layout.layout {
menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
}
menu_layout
} else {
panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self)
}
}
pub fn iter(&self) -> Box<dyn Iterator<Item = &WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter()),
}
}
pub fn iter_mut(&mut self) -> Box<dyn Iterator<Item = &mut WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter_mut()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()),
}
}
}
impl Default for Layout {
fn default() -> Self {
Layout::WidgetLayout(WidgetLayout::default())
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct WidgetLayout {
pub layout: SubLayout,
}
impl WidgetLayout {
pub fn new(layout: SubLayout) -> Self {
Self { layout }
}
pub fn iter(&self) -> WidgetIter<'_> {
WidgetIter {
stack: self.layout.iter().collect(),
current_slice: None,
}
}
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: self.layout.iter_mut().collect(),
current_slice: None,
}
}
}
#[derive(Debug, Default)]
pub struct WidgetIter<'a> {
pub stack: Vec<&'a LayoutGroup>,
pub current_slice: Option<&'a [WidgetHolder]>,
}
impl<'a> Iterator for WidgetIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.current_slice.and_then(|slice| slice.first()) {
self.current_slice = Some(&self.current_slice.unwrap()[1..]);
return Some(item);
}
match self.stack.pop() {
Some(LayoutGroup::Column { widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Row { widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Section { name: _, layout }) => {
for layout_row in layout {
self.stack.push(layout_row);
}
self.next()
}
None => None,
}
}
}
#[derive(Debug, Default)]
pub struct WidgetIterMut<'a> {
pub stack: Vec<&'a mut LayoutGroup>,
pub current_slice: Option<&'a mut [WidgetHolder]>,
}
impl<'a> Iterator for WidgetIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some((first, rest)) = self.current_slice.take().and_then(|slice| slice.split_first_mut()) {
self.current_slice = Some(rest);
return Some(first);
};
match self.stack.pop() {
Some(LayoutGroup::Column { widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Row { widgets }) => {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Section { name: _, layout }) => {
for layout_row in layout {
self.stack.push(layout_row);
}
self.next()
}
None => None,
}
}
}
pub type SubLayout = Vec<LayoutGroup>;
#[remain::sorted]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LayoutGroup {
#[serde(rename = "column")]
Column {
#[serde(rename = "columnWidgets")]
widgets: Vec<WidgetHolder>,
},
#[serde(rename = "row")]
Row {
#[serde(rename = "rowWidgets")]
widgets: Vec<WidgetHolder>,
},
#[serde(rename = "section")]
Section { name: String, layout: SubLayout },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WidgetHolder {
#[serde(rename = "widgetId")]
pub widget_id: u64,
pub widget: Widget,
}
impl WidgetHolder {
pub fn new(widget: Widget) -> Self {
Self { widget_id: generate_uuid(), widget }
}
}
#[derive(Clone)]
pub struct WidgetCallback<T> {
pub callback: Rc<dyn Fn(&T) -> Message + 'static>,
}
impl<T> WidgetCallback<T> {
pub fn new(callback: impl Fn(&T) -> Message + 'static) -> Self {
Self { callback: Rc::new(callback) }
}
}
impl<T> Default for WidgetCallback<T> {
fn default() -> Self {
Self::new(|_| Message::NoOp)
}
}
#[remain::sorted]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Widget {
CheckboxInput(CheckboxInput),
ColorInput(ColorInput),
DropdownInput(DropdownInput),
FontInput(FontInput),
IconButton(IconButton),
IconLabel(IconLabel),
InvisibleStandinInput(InvisibleStandinInput),
NumberInput(NumberInput),
OptionalInput(OptionalInput),
PopoverButton(PopoverButton),
RadioInput(RadioInput),
Separator(Separator),
SwatchPairInput(SwatchPairInput),
TextAreaInput(TextAreaInput),
TextButton(TextButton),
TextInput(TextInput),
TextLabel(TextLabel),
}
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize)]
#[repr(u8)]
pub enum LayoutTarget {
DialogDetails,
DocumentBar,
DocumentMode,
LayerTreeOptions,
MenuBar,
PropertiesOptions,
PropertiesSections,
ToolOptions,
ToolShelf,
WorkingColors,
// KEEP THIS ENUM LAST
// This is a marker that is used to define an array that is used to hold widgets
#[remain::unsorted]
LayoutTargetLength,
}
@@ -0,0 +1,3 @@
pub mod layout_widget;
pub mod misc;
pub mod widgets;
@@ -0,0 +1,55 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use derivative::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct IconButton {
pub icon: String,
pub size: u32, // TODO: Convert to an `IconSize` enum
pub active: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<IconButton>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct PopoverButton {
pub icon: Option<String>,
// Body
pub header: String,
pub text: String,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
pub struct TextButton {
pub label: String,
pub emphasized: bool,
#[serde(rename = "minWidth")]
pub min_width: u32,
pub disabled: bool,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextButton>,
}
@@ -0,0 +1,268 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use graphene::color::Color;
use derivative::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct CheckboxInput {
pub checked: bool,
pub icon: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<CheckboxInput>,
}
#[derive(Clone, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq, Default)]
pub struct ColorInput {
pub value: Option<String>,
pub label: Option<String>,
#[serde(rename = "noTransparency")]
#[derivative(Default(value = "true"))]
pub no_transparency: bool,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<ColorInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct DropdownInput {
pub entries: DropdownInputEntries,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
#[serde(rename = "drawIcon")]
pub draw_icon: bool,
#[derivative(Default(value = "true"))]
pub interactive: bool,
pub disabled: bool,
//
// Callbacks
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
}
pub type DropdownInputEntries = Vec<Vec<DropdownEntryData>>;
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct DropdownEntryData {
pub value: String,
pub label: String,
pub icon: String,
pub shortcut: Vec<String>,
#[serde(rename = "shortcutRequiresLock")]
pub shortcut_requires_lock: bool,
pub disabled: bool,
pub children: DropdownInputEntries,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct FontInput {
#[serde(rename = "fontFamily")]
pub font_family: String,
#[serde(rename = "fontStyle")]
pub font_style: String,
#[serde(rename = "isStyle")]
pub is_style_picker: bool,
pub disabled: bool,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<FontInput>,
}
/// This widget allows for the flexible use of the layout system.
/// In a custom layout, one can define a widget that is just used to trigger code on the backend.
/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend.
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derivative(Debug, PartialEq)]
pub struct InvisibleStandinInput {
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {
pub label: String,
pub value: Option<f64>,
pub min: Option<f64>,
pub max: Option<f64>,
#[serde(rename = "isInteger")]
pub is_integer: bool,
#[serde(rename = "displayDecimalPlaces")]
#[derivative(Default(value = "3"))]
pub display_decimal_places: u32,
pub unit: String,
#[serde(rename = "unitIsHiddenWhenEditing")]
#[derivative(Default(value = "true"))]
pub unit_is_hidden_when_editing: bool,
#[serde(rename = "incrementBehavior")]
pub increment_behavior: NumberInputIncrementBehavior,
#[serde(rename = "incrementFactor")]
#[derivative(Default(value = "1."))]
pub increment_factor: f64,
pub disabled: bool,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_increase: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_decrease: WidgetCallback<NumberInput>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
pub enum NumberInputIncrementBehavior {
#[default]
Add,
Multiply,
Callback,
}
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct OptionalInput {
pub checked: bool,
pub icon: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<OptionalInput>,
}
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct RadioInput {
pub entries: Vec<RadioEntryData>,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: u32,
}
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct RadioEntryData {
pub value: String,
pub label: String,
pub icon: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct SwatchPairInput {
pub primary: Color,
pub secondary: Color,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct TextAreaInput {
pub value: String,
pub label: Option<String>,
pub disabled: bool,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextAreaInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct TextInput {
pub value: String,
pub label: Option<String>,
pub disabled: bool,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<TextInput>,
}
@@ -0,0 +1,56 @@
use derivative::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
pub struct IconLabel {
pub icon: String,
#[serde(rename = "iconStyle")]
pub icon_style: IconStyle,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
pub enum IconStyle {
#[default]
Normal,
Node,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Separator {
pub direction: SeparatorDirection,
#[serde(rename = "type")]
pub separator_type: SeparatorType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SeparatorDirection {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SeparatorType {
Related,
Unrelated,
Section,
List,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)]
pub struct TextLabel {
pub bold: bool,
pub italic: bool,
#[serde(rename = "tableAlign")]
pub table_align: bool,
pub multiline: bool,
// Body
pub value: String,
}
// TODO: Add UserInputLabel
@@ -0,0 +1,134 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::layout_widget::WidgetHolder;
use crate::messages::layout::utility_types::layout_widget::{Widget, WidgetCallback};
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
use super::input_widgets::InvisibleStandinInput;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MenuEntryGroups(pub Vec<Vec<MenuEntry>>);
impl MenuEntryGroups {
pub fn empty() -> Self {
Self(Vec::new())
}
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>) {
let entries = self.0.iter_mut().flatten();
for entry in entries {
if let Some(action_keys) = &mut entry.shortcut {
action_keys.to_keys(action_input_mapping);
}
// Recursively do this for the children also
entry.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MenuEntry {
pub label: String,
pub icon: Option<String>,
pub children: MenuEntryGroups,
pub action: WidgetHolder,
pub shortcut: Option<ActionKeys>,
}
impl MenuEntry {
pub fn create_action(callback: impl Fn(&()) -> Message + 'static) -> WidgetHolder {
WidgetHolder::new(Widget::InvisibleStandinInput(InvisibleStandinInput {
on_update: WidgetCallback::new(callback),
}))
}
pub fn no_action() -> WidgetHolder {
MenuEntry::create_action(|_| Message::NoOp)
}
}
impl Default for MenuEntry {
fn default() -> Self {
Self {
action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()),
label: "".into(),
icon: None,
children: MenuEntryGroups::empty(),
shortcut: None,
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct MenuColumn {
pub label: String,
pub children: MenuEntryGroups,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct MenuLayout {
pub layout: Vec<MenuColumn>,
}
impl MenuLayout {
pub fn new(layout: Vec<MenuColumn>) -> Self {
Self { layout }
}
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
MenuLayoutIter {
stack: self.layout.iter().flat_map(|column| column.children.0.iter()).flat_map(|group| group.iter()).collect(),
}
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut WidgetHolder> + '_ {
MenuLayoutIterMut {
stack: self.layout.iter_mut().flat_map(|column| column.children.0.iter_mut()).flat_map(|group| group.iter_mut()).collect(),
}
}
}
#[derive(Debug, Default)]
pub struct MenuLayoutIter<'a> {
pub stack: Vec<&'a MenuEntry>,
}
impl<'a> Iterator for MenuLayoutIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter());
self.stack.extend(more_entries);
Some(&menu_entry.action)
}
None => None,
}
}
}
pub struct MenuLayoutIterMut<'a> {
pub stack: Vec<&'a mut MenuEntry>,
}
impl<'a> Iterator for MenuLayoutIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut());
self.stack.extend(more_entries);
Some(&mut menu_entry.action)
}
None => None,
}
}
}
@@ -0,0 +1,4 @@
pub mod button_widgets;
pub mod input_widgets;
pub mod label_widgets;
pub mod menu_widgets;
+60
View File
@@ -0,0 +1,60 @@
use crate::messages::prelude::*;
use graphite_proc_macros::*;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[remain::sorted]
#[impl_message]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Message {
#[remain::unsorted]
NoOp,
#[remain::unsorted]
Init,
#[child]
Broadcast(BroadcastMessage),
#[child]
Debug(DebugMessage),
#[child]
Dialog(DialogMessage),
#[child]
Frontend(FrontendMessage),
#[child]
InputMapper(InputMapperMessage),
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
Layout(LayoutMessage),
#[child]
Portfolio(PortfolioMessage),
#[child]
Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
}
impl Message {
/// Returns the byte representation of the message.
///
/// # Safety
/// This function reads from uninitialized memory!!!
/// Only use if you know what you are doing.
unsafe fn as_slice(&self) -> &[u8] {
core::slice::from_raw_parts(self as *const Message as *const u8, std::mem::size_of::<Message>())
}
/// Returns a pseudo hash that should uniquely identify the message.
/// This is needed because `Hash` is not implemented for `f64`s
///
/// # Safety
/// This function reads from uninitialized memory but the generated value should be fine.
pub fn pseudo_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
unsafe { self.as_slice() }.hash(&mut s);
s.finish()
}
}
+14
View File
@@ -0,0 +1,14 @@
//! The root-level messages forming the first layer of the message system architecture.
pub mod broadcast;
pub mod debug;
pub mod dialog;
pub mod frontend;
pub mod input_mapper;
pub mod input_preprocessor;
pub mod layout;
pub mod message;
pub mod portfolio;
pub mod prelude;
pub mod tool;
pub mod workspace;
@@ -0,0 +1,37 @@
use crate::messages::prelude::*;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Artboard)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ArtboardMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
// Messages
AddArtboard {
id: Option<LayerId>,
position: (f64, f64),
size: (f64, f64),
},
DeleteArtboard {
artboard: LayerId,
},
RenderArtboards,
ResizeArtboard {
artboard: LayerId,
position: (f64, f64),
size: (f64, f64),
},
}
impl From<DocumentOperation> for ArtboardMessage {
fn from(operation: DocumentOperation) -> Self {
Self::DispatchOperation(Box::new(operation))
}
}
@@ -0,0 +1,116 @@
use crate::messages::prelude::*;
use crate::application::generate_uuid;
use graphene::color::Color;
use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::{self, Fill, RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
use graphene::DocumentResponse;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ArtboardMessageHandler {
pub artboards_graphene_document: GrapheneDocument,
pub artboard_ids: Vec<LayerId>,
}
impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
#[remain::check]
fn process_message(&mut self, message: ArtboardMessage, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
use ArtboardMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(*operation, font_cache) {
Ok(Some(document_responses)) => {
for response in document_responses {
match &response {
DocumentResponse::LayerChanged { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: path.clone() }.into()),
DocumentResponse::DeletedLayer { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasDeleted { path: path.clone() }.into()),
DocumentResponse::DocumentChanged => responses.push_back(ArtboardMessage::RenderArtboards.into()),
_ => {}
};
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
}
Ok(None) => {}
Err(e) => log::error!("Artboard Error: {:?}", e),
},
// Messages
AddArtboard { id, position, size } => {
let artboard_id = id.unwrap_or_else(generate_uuid);
self.artboard_ids.push(artboard_id);
responses.push_back(
ArtboardMessage::DispatchOperation(
DocumentOperation::AddRect {
path: vec![artboard_id],
insert_index: -1,
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
style: style::PathStyle::new(None, Fill::solid(Color::WHITE)),
}
.into(),
)
.into(),
);
responses.push_back(DocumentMessage::RenderDocument.into());
}
DeleteArtboard { artboard } => {
self.artboard_ids.retain(|&id| id != artboard);
responses.push_back(ArtboardMessage::DispatchOperation(Box::new(DocumentOperation::DeleteLayer { path: vec![artboard] })).into());
responses.push_back(DocumentMessage::RenderDocument.into());
}
RenderArtboards => {
// Render an infinite canvas if there are no artboards
if self.artboard_ids.is_empty() {
responses.push_back(
FrontendMessage::UpdateDocumentArtboards {
svg: r##"<rect width="100%" height="100%" fill="#ffffff" />"##.to_string(),
}
.into(),
)
} else {
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
responses.push_back(
FrontendMessage::UpdateDocumentArtboards {
svg: self.artboards_graphene_document.render_root(render_data),
}
.into(),
);
}
}
ResizeArtboard { artboard, position, size } => {
responses.push_back(
ArtboardMessage::DispatchOperation(Box::new(DocumentOperation::SetLayerTransform {
path: vec![artboard],
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
}))
.into(),
);
responses.push_back(DocumentMessage::RenderDocument.into());
}
}
}
fn actions(&self) -> ActionList {
actions!(ArtboardMessageDiscriminant;)
}
}
impl ArtboardMessageHandler {
pub fn is_infinite_canvas(&self) -> bool {
self.artboard_ids.is_empty()
}
}
@@ -0,0 +1,7 @@
mod artboard_message;
mod artboard_message_handler;
#[doc(inline)]
pub use artboard_message::{ArtboardMessage, ArtboardMessageDiscriminant};
#[doc(inline)]
pub use artboard_message_handler::ArtboardMessageHandler;
@@ -0,0 +1,185 @@
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::prelude::*;
use graphene::boolean_ops::BooleanOperation as BooleanOperationType;
use graphene::layers::blend_mode::BlendMode;
use graphene::layers::style::ViewMode;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, PortfolioMessage, Document)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum DocumentMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
#[remain::unsorted]
#[child]
Artboard(ArtboardMessage),
#[remain::unsorted]
#[child]
Movement(MovementMessage),
#[remain::unsorted]
#[child]
Overlays(OverlaysMessage),
#[remain::unsorted]
#[child]
TransformLayer(TransformLayerMessage),
#[remain::unsorted]
#[child]
PropertiesPanel(PropertiesPanelMessage),
// Messages
AbortTransaction,
AddSelectedLayers {
additional_layers: Vec<Vec<LayerId>>,
},
AlignSelectedLayers {
axis: AlignAxis,
aggregate: AlignAggregate,
},
BooleanOperation(BooleanOperationType),
CommitTransaction,
CreateEmptyFolder {
container_path: Vec<LayerId>,
},
DebugPrintDocument,
DeleteLayer {
layer_path: Vec<LayerId>,
},
DeleteSelectedLayers,
DeleteSelectedManipulatorPoints,
DeselectAllLayers,
DeselectAllManipulatorPoints,
DirtyRenderDocument,
DirtyRenderDocumentInOutlineView,
DocumentHistoryBackward,
DocumentHistoryForward,
DocumentStructureChanged,
DuplicateSelectedLayers,
ExportDocument {
file_name: String,
file_type: FileType,
scale_factor: f64,
bounds: ExportBounds,
},
FlipSelectedLayers {
flip_axis: FlipAxis,
},
FolderChanged {
affected_folder_path: Vec<LayerId>,
},
GroupSelectedLayers,
LayerChanged {
affected_layer_path: Vec<LayerId>,
},
MoveSelectedLayersTo {
folder_path: Vec<LayerId>,
insert_index: isize,
reverse_index: bool,
},
MoveSelectedManipulatorPoints {
layer_path: Vec<LayerId>,
delta: (f64, f64),
absolute_position: (f64, f64),
},
NudgeSelectedLayers {
delta_x: f64,
delta_y: f64,
},
PasteImage {
mime: String,
image_data: Vec<u8>,
mouse: Option<(f64, f64)>,
},
Redo,
RenameLayer {
layer_path: Vec<LayerId>,
new_name: String,
},
RenderDocument,
RollbackTransaction,
SaveDocument,
SelectAllLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
SelectedLayersReorder {
relative_index_offset: isize,
},
SelectLayer {
layer_path: Vec<LayerId>,
ctrl: bool,
shift: bool,
},
SetBlendModeForSelectedLayers {
blend_mode: BlendMode,
},
SetLayerExpansion {
layer_path: Vec<LayerId>,
set_expanded: bool,
},
SetLayerName {
layer_path: Vec<LayerId>,
name: String,
},
SetOpacityForSelectedLayers {
opacity: f64,
},
SetOverlaysVisibility {
visible: bool,
},
SetSelectedLayers {
replacement_selected_layers: Vec<Vec<LayerId>>,
},
SetSnapping {
snap: bool,
},
SetTexboxEditability {
path: Vec<LayerId>,
editable: bool,
},
SetViewMode {
view_mode: ViewMode,
},
StartTransaction,
ToggleLayerExpansion {
layer_path: Vec<LayerId>,
},
ToggleLayerVisibility {
layer_path: Vec<LayerId>,
},
ToggleSelectedHandleMirroring {
layer_path: Vec<LayerId>,
toggle_distance: bool,
toggle_angle: bool,
},
Undo,
UngroupLayers {
folder_path: Vec<LayerId>,
},
UngroupSelectedLayers,
UpdateLayerMetadata {
layer_path: Vec<LayerId>,
layer_metadata: LayerMetadata,
},
ZoomCanvasToFitAll,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
DocumentMessage::DispatchOperation(Box::new(operation))
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(Box::new(operation)).into()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
mod document_message;
mod document_message_handler;
pub mod artboard;
pub mod movement;
pub mod overlays;
pub mod properties_panel;
pub mod transform_layer;
pub mod utility_types;
#[doc(inline)]
pub use document_message::{DocumentMessage, DocumentMessageDiscriminant};
#[doc(inline)]
pub use document_message_handler::DocumentMessageHandler;
@@ -0,0 +1,7 @@
mod movement_message;
mod movement_message_handler;
#[doc(inline)]
pub use movement_message::{MovementMessage, MovementMessageDiscriminant};
#[doc(inline)]
pub use movement_message_handler::MovementMessageHandler;
@@ -0,0 +1,49 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Movement)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum MovementMessage {
// Messages
DecreaseCanvasZoom {
center_on_mouse: bool,
},
FitViewportToBounds {
bounds: [DVec2; 2],
padding_scale_factor: Option<f32>,
prevent_zoom_past_100: bool,
},
IncreaseCanvasZoom {
center_on_mouse: bool,
},
PointerMove {
snap_angle: Key,
wait_for_snap_angle_release: bool,
snap_zoom: Key,
zoom_from_viewport: Option<DVec2>,
},
RotateCanvasBegin,
SetCanvasRotation {
angle_radians: f64,
},
SetCanvasZoom {
zoom_factor: f64,
},
TransformCanvasEnd,
TranslateCanvas {
delta: DVec2,
},
TranslateCanvasBegin,
TranslateCanvasByViewportFraction {
delta: DVec2,
},
WheelCanvasTranslate {
use_y_as_x: bool,
},
WheelCanvasZoom,
ZoomCanvasBegin,
}
@@ -0,0 +1,353 @@
use crate::consts::{VIEWPORT_ROTATE_SNAP_INTERVAL, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup};
use crate::messages::input_mapper::utility_types::input_mouse::{ViewportBounds, ViewportPosition};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::document::Document;
use graphene::Operation as DocumentOperation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MovementMessageHandler {
pub pan: DVec2,
panning: bool,
snap_tilt: bool,
snap_tilt_released: bool,
pub tilt: f64,
tilting: bool,
pub zoom: f64,
zooming: bool,
snap_zoom: bool,
mouse_position: ViewportPosition,
}
impl Default for MovementMessageHandler {
fn default() -> Self {
Self {
pan: DVec2::ZERO,
panning: false,
snap_tilt: false,
snap_tilt_released: false,
tilt: 0.,
tilting: false,
zoom: 1.,
zooming: false,
snap_zoom: false,
mouse_position: ViewportPosition::default(),
}
}
}
impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandler)> for MovementMessageHandler {
#[remain::check]
fn process_message(&mut self, message: MovementMessage, data: (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
use MovementMessage::*;
let (document, ipp) = data;
#[remain::sorted]
match message {
DecreaseCanvasZoom { center_on_mouse } => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom);
if center_on_mouse {
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position));
}
responses.push_back(SetCanvasZoom { zoom_factor: new_scale }.into());
}
FitViewportToBounds {
bounds: [bounds_corner_a, bounds_corner_b],
padding_scale_factor,
prevent_zoom_past_100,
} => {
let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a);
let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b);
let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size());
let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
let size = (pos2 - pos1) / (v2 - v1);
let size = 1. / size;
let new_scale = size.min_element();
self.pan += center;
self.zoom *= new_scale;
self.zoom /= padding_scale_factor.unwrap_or(1.) as f64;
if self.zoom > 1. && prevent_zoom_past_100 {
self.zoom = 1.
}
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
IncreaseCanvasZoom { center_on_mouse } => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom);
if center_on_mouse {
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position));
}
responses.push_back(SetCanvasZoom { zoom_factor: new_scale }.into());
}
PointerMove {
snap_angle,
wait_for_snap_angle_release,
snap_zoom,
zoom_from_viewport,
} => {
if self.panning {
let delta = ipp.mouse.position - self.mouse_position;
responses.push_back(TranslateCanvas { delta }.into());
}
if self.tilting {
let new_snap = ipp.keyboard.get(snap_angle as usize);
if !(wait_for_snap_angle_release && new_snap && !self.snap_tilt_released) {
// When disabling snap, keep the viewed rotation as it was previously.
if !new_snap && self.snap_tilt {
self.tilt = self.snapped_angle();
}
self.snap_tilt = new_snap;
self.snap_tilt_released = true;
}
let half_viewport = ipp.viewport_bounds.size() / 2.;
let rotation = {
let start_offset = self.mouse_position - half_viewport;
let end_offset = ipp.mouse.position - half_viewport;
start_offset.angle_between(end_offset)
};
responses.push_back(SetCanvasRotation { angle_radians: self.tilt + rotation }.into());
}
if self.zooming {
let zoom_start = self.snapped_scale();
let new_snap = ipp.keyboard.get(snap_zoom as usize);
// When disabling snap, keep the viewed zoom as it was previously
if !new_snap && self.snap_zoom {
self.zoom = self.snapped_scale();
}
self.snap_zoom = new_snap;
let difference = self.mouse_position.y as f64 - ipp.mouse.position.y as f64;
let amount = 1. + difference * VIEWPORT_ZOOM_MOUSE_RATE;
self.zoom *= amount;
if let Some(mouse) = zoom_from_viewport {
let zoom_factor = self.snapped_scale() / zoom_start;
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom }.into());
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, mouse));
} else {
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom }.into());
}
}
self.mouse_position = ipp.mouse.position;
}
RotateCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
responses.push_back(
FrontendMessage::UpdateInputHints {
hint_data: 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,
}])]),
}
.into(),
);
self.tilting = true;
self.mouse_position = ipp.mouse.position;
}
SetCanvasRotation { angle_radians } => {
self.tilt = angle_radians;
self.create_document_transform(&ipp.viewport_bounds, responses);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
}
SetCanvasZoom { zoom_factor } => {
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TransformCanvasEnd => {
self.tilt = self.snapped_angle();
self.zoom = self.snapped_scale();
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(ToolMessage::UpdateCursor.into());
responses.push_back(ToolMessage::UpdateHints.into());
self.snap_tilt = false;
self.snap_tilt_released = false;
self.snap_zoom = false;
self.panning = false;
self.tilting = false;
self.zooming = false;
}
TranslateCanvas { delta } => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
self.pan += transformed_delta;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TranslateCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Grabbing }.into());
responses.push_back(FrontendMessage::UpdateInputHints { hint_data: HintData(Vec::new()) }.into());
self.panning = true;
self.mouse_position = ipp.mouse.position;
}
TranslateCanvasByViewportFraction { delta } => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size());
self.pan += transformed_delta;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
responses.push_back(TranslateCanvas { delta }.into());
}
WheelCanvasZoom => {
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mut zoom_factor = 1. + scroll.abs() * VIEWPORT_ZOOM_WHEEL_RATE;
if ipp.mouse.scroll_delta.y > 0 {
zoom_factor = 1. / zoom_factor
};
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position));
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom * zoom_factor }.into());
}
ZoomCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::ZoomIn }.into());
responses.push_back(
FrontendMessage::UpdateInputHints {
hint_data: 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,
}])]),
}
.into(),
);
self.zooming = true;
self.mouse_position = ipp.mouse.position;
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(MovementMessageDiscriminant;
TranslateCanvasBegin,
RotateCanvasBegin,
ZoomCanvasBegin,
SetCanvasZoom,
SetCanvasRotation,
WheelCanvasZoom,
IncreaseCanvasZoom,
DecreaseCanvasZoom,
WheelCanvasTranslate,
TranslateCanvas,
TranslateCanvasByViewportFraction,
);
if self.panning || self.tilting || self.zooming {
let transforming = actions!(MovementMessageDiscriminant;
PointerMove,
TransformCanvasEnd,
);
common.extend(transforming);
}
common
}
}
impl MovementMessageHandler {
pub fn snapped_angle(&self) -> f64 {
let increment_radians: f64 = VIEWPORT_ROTATE_SNAP_INTERVAL.to_radians();
if self.snap_tilt {
(self.tilt / increment_radians).round() * increment_radians
} else {
self.tilt
}
}
pub fn snapped_scale(&self) -> f64 {
if self.snap_zoom {
*VIEWPORT_ZOOM_LEVELS
.iter()
.min_by(|a, b| (**a - self.zoom).abs().partial_cmp(&(**b - self.zoom).abs()).unwrap())
.unwrap_or(&self.zoom)
} else {
self.zoom
}
}
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(offset);
let scale_transform = DAffine2::from_scale(DVec2::splat(self.snapped_scale()));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(self.pan);
scale_transform * offset_transform * angle_transform * translation_transform
}
fn create_document_transform(&self, viewport_bounds: &ViewportBounds, responses: &mut VecDeque<Message>) {
let half_viewport = viewport_bounds.size() / 2.;
let scaled_half_viewport = half_viewport / self.snapped_scale();
responses.push_back(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: self.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
);
responses.push_back(
ArtboardMessage::DispatchOperation(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: self.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
)
.into(),
);
}
pub fn center_zoom(&self, viewport_bounds: DVec2, zoom_factor: f64, mouse: DVec2) -> Message {
let new_viewport_bounds = viewport_bounds / zoom_factor;
let delta_size = viewport_bounds - new_viewport_bounds;
let mouse_fraction = mouse / viewport_bounds;
let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction);
MovementMessage::TranslateCanvas { delta }.into()
}
}
@@ -0,0 +1,7 @@
mod overlays_message;
mod overlays_message_handler;
#[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
#[doc(inline)]
pub use overlays_message_handler::OverlaysMessageHandler;
@@ -0,0 +1,24 @@
use crate::messages::prelude::*;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Overlays)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum OverlaysMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
// Messages
ClearAllOverlays,
Rerender,
}
impl From<DocumentOperation> for OverlaysMessage {
fn from(operation: DocumentOperation) -> OverlaysMessage {
Self::DispatchOperation(Box::new(operation))
}
}
@@ -0,0 +1,51 @@
use crate::messages::prelude::*;
use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
#[derive(Debug, Clone, Default)]
pub struct OverlaysMessageHandler {
pub overlays_graphene_document: GrapheneDocument,
}
impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessageHandler)> for OverlaysMessageHandler {
#[remain::check]
fn process_message(&mut self, message: OverlaysMessage, (overlays_visible, font_cache, ipp): (bool, &FontCache, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
use OverlaysMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(*operation, font_cache) {
Ok(_) => responses.push_back(OverlaysMessage::Rerender.into()),
Err(e) => log::error!("OverlaysError: {:?}", e),
},
// Messages
ClearAllOverlays => todo!(),
Rerender =>
// Render overlays
{
responses.push_back(
FrontendMessage::UpdateDocumentOverlays {
svg: if overlays_visible {
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false);
self.overlays_graphene_document.render_root(render_data)
} else {
String::from("")
},
}
.into(),
)
}
}
}
fn actions(&self) -> ActionList {
actions!(OverlaysMessageDiscriminant;
ClearAllOverlays,
)
}
}
@@ -0,0 +1,10 @@
mod properties_panel_message;
mod properties_panel_message_handler;
pub mod utility_functions;
pub mod utility_types;
#[doc(inline)]
pub use properties_panel_message::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant};
#[doc(inline)]
pub use properties_panel_message_handler::PropertiesPanelMessageHandler;
@@ -0,0 +1,29 @@
use super::utility_types::TransformOp;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use graphene::layers::style::{Fill, Stroke};
use graphene::LayerId;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, PropertiesPanel)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PropertiesPanelMessage {
// Messages
CheckSelectedWasDeleted { path: Vec<LayerId> },
CheckSelectedWasUpdated { path: Vec<LayerId> },
ClearSelection,
Deactivate,
Init,
ModifyFill { fill: Fill },
ModifyFont { font_family: String, font_style: String, size: f64 },
ModifyName { name: String },
ModifyStroke { stroke: Stroke },
ModifyText { new_text: String },
ModifyTransform { value: f64, transform_op: TransformOp },
ResendActiveProperties,
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
UpdateSelectedDocumentProperties,
}
@@ -0,0 +1,166 @@
use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties};
use super::utility_types::PropertiesPanelMessageHandlerData;
use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use graphene::{LayerId, Operation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PropertiesPanelMessageHandler {
active_selection: Option<(Vec<LayerId>, TargetDocument)>,
}
impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerData<'a>> for PropertiesPanelMessageHandler {
#[remain::check]
fn process_message(&mut self, message: PropertiesPanelMessage, data: PropertiesPanelMessageHandlerData, responses: &mut VecDeque<Message>) {
let PropertiesPanelMessageHandlerData {
artwork_document,
artboard_document,
selected_layers,
font_cache,
} = data;
let get_document = |document_selector: TargetDocument| match document_selector {
TargetDocument::Artboard => artboard_document,
TargetDocument::Artwork => artwork_document,
};
use PropertiesPanelMessage::*;
match message {
SetActiveLayers { paths, document } => {
if paths.len() != 1 {
// TODO: Allow for multiple selected layers
responses.push_back(PropertiesPanelMessage::ClearSelection.into())
} else {
let path = paths.into_iter().next().unwrap();
self.active_selection = Some((path, document));
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
}
}
ClearSelection => {
responses.push_back(
LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesOptions,
}
.into(),
);
responses.push_back(
LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections,
}
.into(),
);
self.active_selection = None;
}
Deactivate => responses.push_back(
BroadcastMessage::UnsubscribeEvent {
on: BroadcastEvent::SelectionChanged,
message: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
}
.into(),
),
Init => responses.push_back(
BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged,
send: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
}
.into(),
),
ModifyFont { font_family, font_style, size } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::ModifyFont { path, font_family, font_style, size }));
responses.push_back(ResendActiveProperties.into());
}
ModifyTransform { value, transform_op } => {
let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer");
let layer = get_document(*target_document).layer(path).unwrap();
let transform = apply_transform_operation(layer, transform_op, value, font_cache);
responses.push_back(self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }));
}
ModifyName { name } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name }))
}
ModifyFill { fill } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill }));
}
ModifyStroke { stroke } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerStroke { path, stroke }))
}
ModifyText { new_text } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::SetTextContent { path, new_text }.into())
}
CheckSelectedWasUpdated { path } => {
if self.matches_selected(&path) {
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
}
}
CheckSelectedWasDeleted { path } => {
if self.matches_selected(&path) {
self.active_selection = None;
responses.push_back(
LayoutMessage::SendLayout {
layout_target: LayoutTarget::PropertiesOptions,
layout: Layout::WidgetLayout(WidgetLayout::default()),
}
.into(),
);
responses.push_back(
LayoutMessage::SendLayout {
layout_target: LayoutTarget::PropertiesSections,
layout: Layout::WidgetLayout(WidgetLayout::default()),
}
.into(),
);
}
}
ResendActiveProperties => {
if let Some((path, target_document)) = self.active_selection.clone() {
let layer = get_document(target_document).layer(&path).unwrap();
match target_document {
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, font_cache),
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, font_cache),
}
}
}
UpdateSelectedDocumentProperties => responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: selected_layers.map(|path| path.to_vec()).collect(),
document: TargetDocument::Artwork,
}
.into(),
),
}
}
fn actions(&self) -> ActionList {
actions!(PropertiesMessageDiscriminant;)
}
}
impl PropertiesPanelMessageHandler {
fn matches_selected(&self, path: &[LayerId]) -> bool {
let last_active_path_id = self.active_selection.as_ref().and_then(|(v, _)| v.last().copied());
let last_modified = path.last().copied();
matches!((last_active_path_id, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last)
}
fn create_document_operation(&self, operation: Operation) -> Message {
let (_, target_document) = self.active_selection.as_ref().unwrap();
match *target_document {
TargetDocument::Artboard => ArtboardMessage::DispatchOperation(Box::new(operation)).into(),
TargetDocument::Artwork => DocumentMessage::DispatchOperation(Box::new(operation)).into(),
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
use graphene::document::Document as GrapheneDocument;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
pub struct PropertiesPanelMessageHandlerData<'a> {
pub artwork_document: &'a GrapheneDocument,
pub artboard_document: &'a GrapheneDocument,
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
pub font_cache: &'a FontCache,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum TransformOp {
X,
Y,
ScaleX,
ScaleY,
Width,
Height,
Rotation,
}
@@ -0,0 +1,7 @@
mod transform_layer_message;
mod transform_layer_message_handler;
#[doc(inline)]
pub use transform_layer_message::{TransformLayerMessage, TransformLayerMessageDiscriminant};
#[doc(inline)]
pub use transform_layer_message_handler::TransformLayerMessageHandler;
@@ -0,0 +1,23 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, TransformLayer)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum TransformLayerMessage {
// Messages
ApplyTransformOperation,
BeginGrab,
BeginRotate,
BeginScale,
CancelTransformOperation,
ConstrainX,
ConstrainY,
PointerMove { slow_key: Key, snap_key: Key },
TypeBackspace,
TypeDecimalPoint,
TypeDigit { digit: u8 },
TypeNegate,
}
@@ -0,0 +1,186 @@
use crate::consts::SLOWING_DIVISOR;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*;
use graphene::document::Document;
use graphene::LayerId;
use glam::DVec2;
use graphene::layers::text_layer::FontCache;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TransformLayerMessageHandler {
transform_operation: TransformOperation,
slow: bool,
snap: bool,
typing: Typing,
mouse_position: ViewportPosition,
start_mouse: ViewportPosition,
original_transforms: OriginalTransforms,
pivot: DVec2,
}
type TransformData<'a> = (&'a mut HashMap<Vec<LayerId>, LayerMetadata>, &'a mut Document, &'a InputPreprocessorMessageHandler, &'a FontCache);
impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformLayerMessageHandler {
#[remain::check]
fn process_message(&mut self, message: TransformLayerMessage, (layer_metadata, document, ipp, font_cache): TransformData, responses: &mut VecDeque<Message>) {
use TransformLayerMessage::*;
let selected_layers = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path)).collect::<Vec<_>>();
let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, &selected_layers, responses, document);
let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| {
if operation != TransformOperation::None {
selected.revert_operation();
typing.clear();
} else {
*selected.pivot = selected.calculate_pivot(font_cache);
}
*mouse_position = ipp.mouse.position;
*start_mouse = ipp.mouse.position;
};
#[remain::sorted]
match message {
ApplyTransformOperation => {
self.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginGrab => {
if let TransformOperation::Grabbing(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Grabbing(Default::default());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginRotate => {
if let TransformOperation::Rotating(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Rotating(Default::default());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginScale => {
if let TransformOperation::Scaling(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Scaling(Default::default());
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
CancelTransformOperation => {
selected.revert_operation();
selected.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
PointerMove { slow_key, snap_key } => {
self.slow = ipp.keyboard.get(slow_key as usize);
let new_snap = ipp.keyboard.get(snap_key as usize);
if new_snap != self.snap {
self.snap = new_snap;
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
if self.typing.digits.is_empty() {
let delta_pos = ipp.mouse.position - self.mouse_position;
match self.transform_operation {
TransformOperation::None => unreachable!(),
TransformOperation::Grabbing(translation) => {
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
TransformOperation::Rotating(rotation) => {
let selected_pivot = selected.calculate_pivot(font_cache);
let angle = {
let start_offset = self.mouse_position - selected_pivot;
let end_offset = ipp.mouse.position - selected_pivot;
start_offset.angle_between(end_offset)
};
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
TransformOperation::Scaling(scale) => {
let change = {
let previous_frame_dist = (self.mouse_position - *selected.pivot).length();
let current_frame_dist = (ipp.mouse.position - *selected.pivot).length();
let start_transform_dist = (self.start_mouse - *selected.pivot).length();
(current_frame_dist - previous_frame_dist) / start_transform_dist
};
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
};
}
self.mouse_position = ipp.mouse.position;
}
TypeBackspace => self.transform_operation.handle_typed(self.typing.type_backspace(), &mut selected, self.snap),
TypeDecimalPoint => self.transform_operation.handle_typed(self.typing.type_decimal_point(), &mut selected, self.snap),
TypeDigit { digit } => self.transform_operation.handle_typed(self.typing.type_number(digit), &mut selected, self.snap),
TypeNegate => self.transform_operation.handle_typed(self.typing.type_negate(), &mut selected, self.snap),
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(TransformLayerMessageDiscriminant;
BeginGrab,
BeginScale,
BeginRotate,
);
if self.transform_operation != TransformOperation::None {
let active = actions!(TransformLayerMessageDiscriminant;
PointerMove,
CancelTransformOperation,
ApplyTransformOperation,
TypeDigit,
TypeBackspace,
TypeDecimalPoint,
TypeNegate,
ConstrainX,
ConstrainY,
);
common.extend(active);
}
common
}
}
@@ -0,0 +1,23 @@
use super::layer_panel::LayerMetadata;
use graphene::layers::layer_info::Layer;
use serde::{Deserialize, Serialize};
#[repr(u8)]
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Clipboard {
Internal,
_InternalClipboardCount, // Keep this as the last entry in internal clipboards since it is used for counting the number of enum variants
Device,
}
pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CopyBufferEntry {
pub layer: Layer,
pub layer_metadata: LayerMetadata,
}
@@ -0,0 +1,41 @@
use graphene::color::Color;
use graphene::DocumentError;
use thiserror::Error;
/// The error type used by the Graphite editor.
#[derive(Clone, Debug, Error)]
pub enum EditorError {
#[error("Failed to execute operation:\n{0}")]
InvalidOperation(String),
#[error("Tried to construct an invalid color:\n{0:?}")]
Color(String),
#[error("The requested tool does not exist")]
UnknownTool,
#[error("The operation caused a document error:\n{0:?}")]
Document(String),
#[error("A rollback was initiated but no transaction was in progress")]
NoTransactionInProgress,
#[error("{0}")]
Misc(String),
}
macro_rules! derive_from {
($type:ty, $kind:ident) => {
impl From<$type> for EditorError {
fn from(error: $type) -> Self {
EditorError::$kind(format!("{:?}", error))
}
}
};
}
derive_from!(&str, Misc);
derive_from!(String, Misc);
derive_from!(Color, Color);
derive_from!(DocumentError, Document);
@@ -0,0 +1,92 @@
use graphene::layers::layer_info::{Layer, LayerData, LayerDataTypeDiscriminant};
use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use glam::{DAffine2, DVec2};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct RawBuffer(Vec<u8>);
impl From<Vec<u64>> for RawBuffer {
fn from(iter: Vec<u64>) -> Self {
// https://github.com/rust-lang/rust-clippy/issues/4484
let v_from_raw: Vec<u8> = unsafe {
// prepare for an auto-forget of the initial vec:
let v_orig: &mut Vec<_> = &mut *std::mem::ManuallyDrop::new(iter);
Vec::from_raw_parts(v_orig.as_mut_ptr() as *mut u8, v_orig.len() * 8, v_orig.capacity() * 8)
// v_orig is never used again, so no aliasing issue
};
Self(v_from_raw)
}
}
impl Serialize for RawBuffer {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut buffer = serializer.serialize_struct("Buffer", 2)?;
buffer.serialize_field("pointer", &(self.0.as_ptr() as usize))?;
buffer.serialize_field("length", &(self.0.len()))?;
buffer.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)]
pub struct LayerMetadata {
pub selected: bool,
pub expanded: bool,
}
impl LayerMetadata {
pub fn new(expanded: bool) -> Self {
Self { selected: false, expanded }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LayerPanelEntry {
pub name: String,
pub visible: bool,
pub layer_type: LayerDataTypeDiscriminant,
pub layer_metadata: LayerMetadata,
pub path: Vec<LayerId>,
pub thumbnail: String,
}
impl LayerPanelEntry {
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, font_cache: &FontCache) -> Self {
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
let arr = layer.data.bounding_box(transform, font_cache).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new();
let mut svg_defs = String::new();
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><defs>{}</defs><g transform="matrix({})">{}</g></svg>"#,
x_min,
y_min,
x_max - x_min,
y_max - y_min,
svg_defs,
transform,
thumbnail,
)
} else {
String::new()
};
LayerPanelEntry {
name,
visible: layer.visible,
layer_type: (&layer.data).into(),
layer_metadata: *layer_metadata,
path,
thumbnail,
}
}
}
@@ -0,0 +1,95 @@
pub use super::layer_panel::{LayerMetadata, LayerPanelEntry};
use graphene::document::Document as GrapheneDocument;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
pub type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerMetadata>);
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis {
X,
Y,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAxis {
X,
Y,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAggregate {
Min,
Max,
Center,
Average,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum TargetDocument {
Artboard,
Artwork,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DocumentMode {
DesignMode,
SelectMode,
GuideMode,
}
impl fmt::Display for DocumentMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
DocumentMode::DesignMode => "Design Mode".to_string(),
DocumentMode::SelectMode => "Select Mode".to_string(),
DocumentMode::GuideMode => "Guide Mode".to_string(),
};
write!(f, "{}", text)
}
}
impl DocumentMode {
pub fn icon_name(&self) -> String {
match self {
DocumentMode::DesignMode => "ViewportDesignMode".to_string(),
DocumentMode::SelectMode => "ViewportSelectMode".to_string(),
DocumentMode::GuideMode => "ViewportGuideMode".to_string(),
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub enum Platform {
#[default]
Unknown,
Windows,
Mac,
Linux,
}
impl Platform {
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
match self {
Platform::Mac => KeyboardPlatformLayout::Mac,
Platform::Unknown => {
log::warn!("The platform has not been set, remember to send `PortfolioMessage::SetPlatform` during editor initialization.");
KeyboardPlatformLayout::Standard
}
_ => KeyboardPlatformLayout::Standard,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub enum KeyboardPlatformLayout {
/// Standard keyboard mapping used by Windows and Linux
#[default]
Standard,
/// Keyboard mapping used by Macs where Command is sometimes used in favor of Control
Mac,
}
@@ -0,0 +1,6 @@
pub mod clipboards;
pub mod error;
pub mod layer_panel;
pub mod misc;
pub mod transformation;
pub mod vectorize_layer_metadata;
@@ -0,0 +1,361 @@
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
use crate::messages::prelude::*;
use graphene::document::Document;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use glam::{DAffine2, DVec2};
use std::collections::{HashMap, VecDeque};
pub type OriginalTransforms = HashMap<Vec<LayerId>, DAffine2>;
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum Axis {
Both,
X,
Y,
}
impl Default for Axis {
fn default() -> Self {
Self::Both
}
}
impl Axis {
pub fn set_or_toggle(&mut self, target: Axis) {
// If constrained to an axis and target is requesting the same axis, toggle back to Both
if *self == target {
*self = Axis::Both;
}
// If current axis is different from the target axis, switch to the target
else {
*self = target;
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Copy)]
pub struct Translation {
pub dragged_distance: DVec2,
pub typed_distance: Option<f64>,
pub constraint: Axis,
}
impl Translation {
pub fn to_dvec(self) -> DVec2 {
if let Some(value) = self.typed_distance {
if self.constraint == Axis::Y {
return DVec2::new(0., value);
} else {
return DVec2::new(value, 0.);
}
}
match self.constraint {
Axis::Both => self.dragged_distance,
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
Axis::Y => DVec2::new(0., self.dragged_distance.y),
}
}
#[must_use]
pub fn increment_amount(self, delta: DVec2) -> Self {
Self {
dragged_distance: self.dragged_distance + delta,
typed_distance: None,
constraint: self.constraint,
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Copy)]
pub struct Rotation {
pub dragged_angle: f64,
pub typed_angle: Option<f64>,
}
impl Rotation {
pub fn to_f64(self, snap: bool) -> f64 {
if let Some(value) = self.typed_angle {
value.to_radians()
} else if snap {
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
(self.dragged_angle / snap_resolution).round() * snap_resolution
} else {
self.dragged_angle
}
}
#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_angle: self.dragged_angle + delta,
typed_angle: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct Scale {
pub dragged_factor: f64,
pub typed_factor: Option<f64>,
pub constraint: Axis,
}
impl Default for Scale {
fn default() -> Self {
Self {
dragged_factor: 1.,
typed_factor: None,
constraint: Axis::default(),
}
}
}
impl Scale {
pub fn to_dvec(self, snap: bool) -> DVec2 {
let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
let factor = if snap { (factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL } else { factor };
match self.constraint {
Axis::Both => DVec2::splat(factor),
Axis::X => DVec2::new(factor, 1.),
Axis::Y => DVec2::new(1., factor),
}
}
#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_factor: self.dragged_factor + delta,
typed_factor: None,
constraint: self.constraint,
}
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum TransformOperation {
None,
Grabbing(Translation),
Rotating(Rotation),
Scaling(Scale),
}
impl Default for TransformOperation {
fn default() -> Self {
TransformOperation::None
}
}
impl TransformOperation {
pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool) {
if self != &TransformOperation::None {
let transformation = match self {
TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec()),
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(snapping)),
TransformOperation::None => unreachable!(),
};
selected.update_transforms(transformation);
}
}
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.constraint.set_or_toggle(axis),
TransformOperation::Rotating(_) => (),
TransformOperation::Scaling(scale) => scale.constraint.set_or_toggle(axis),
};
self.apply_transform_operation(selected, snapping);
}
pub fn handle_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: bool) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
TransformOperation::Rotating(rotation) => rotation.typed_angle = typed,
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
};
self.apply_transform_operation(selected, snapping);
}
}
pub struct Selected<'a> {
pub selected: &'a [&'a Vec<LayerId>],
pub responses: &'a mut VecDeque<Message>,
pub document: &'a Document,
pub original_transforms: &'a mut OriginalTransforms,
pub pivot: &'a mut DVec2,
}
impl<'a> Selected<'a> {
pub fn new(original_transforms: &'a mut OriginalTransforms, pivot: &'a mut DVec2, selected: &'a [&'a Vec<LayerId>], responses: &'a mut VecDeque<Message>, document: &'a Document) -> Self {
for path in selected {
if !original_transforms.contains_key(*path) {
if let Ok(layer) = document.layer(path) {
original_transforms.insert(path.to_vec(), layer.transform);
} else {
log::warn!("Didn't find a layer for {:?}", path);
}
}
}
Self {
selected,
responses,
document,
original_transforms,
pivot,
}
}
pub fn calculate_pivot(&mut self, font_cache: &FontCache) -> DVec2 {
let xy_summation = self
.selected
.iter()
.map(|path| {
let multiplied_transform = self.document.multiply_transforms(path).unwrap();
let bounds = self
.document
.layer(path)
.unwrap()
.aabb_for_transform(multiplied_transform, font_cache)
.unwrap_or([multiplied_transform.translation; 2]);
(bounds[0] + bounds[1]) / 2.
})
.fold(DVec2::ZERO, |summation, next| summation + next);
xy_summation / self.selected.len() as f64
}
pub fn update_transforms(&mut self, delta: DAffine2) {
if !self.selected.is_empty() {
let pivot = DAffine2::from_translation(*self.pivot);
let transformation = pivot * delta * pivot.inverse();
// 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 layer_path in Document::shallowest_unique_layers(self.selected.iter()) {
let parent_folder_path = &layer_path[..layer_path.len() - 1];
let original_layer_transforms = *self.original_transforms.get(*layer_path).unwrap();
let to = self.document.generate_transform_across_scope(parent_folder_path, None).unwrap();
let new = to.inverse() * transformation * to * original_layer_transforms;
self.responses.push_back(
DocumentOperation::SetLayerTransform {
path: layer_path.to_vec(),
transform: new.to_cols_array(),
}
.into(),
);
}
self.responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
}
pub fn revert_operation(&mut self) {
for path in self.selected {
if let Some(transform) = self.original_transforms.get(*path) {
// Push front to stop document switching before sending the transform
self.responses.push_front(
DocumentOperation::SetLayerTransform {
path: path.to_vec(),
transform: transform.to_cols_array(),
}
.into(),
);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Typing {
pub digits: Vec<u8>,
pub contains_decimal: bool,
pub negative: bool,
}
const DECIMAL_POINT: u8 = 10;
impl Typing {
pub fn type_number(&mut self, number: u8) -> Option<f64> {
self.digits.push(number);
self.evaluate()
}
pub fn type_backspace(&mut self) -> Option<f64> {
if self.digits.is_empty() {
return None;
}
match self.digits.pop() {
Some(DECIMAL_POINT) => self.contains_decimal = false,
Some(_) => (),
None => self.negative = false,
}
self.evaluate()
}
pub fn type_decimal_point(&mut self) -> Option<f64> {
if !self.contains_decimal {
self.contains_decimal = true;
self.digits.push(DECIMAL_POINT);
}
self.evaluate()
}
pub fn type_negate(&mut self) -> Option<f64> {
self.negative = !self.negative;
self.evaluate()
}
pub fn evaluate(&self) -> Option<f64> {
if self.digits.is_empty() {
return None;
}
let mut result = 0_f64;
let mut running_decimal_place = 0_i32;
for digit in &self.digits {
if *digit == DECIMAL_POINT {
if running_decimal_place == 0 {
running_decimal_place = 1;
}
} else if running_decimal_place == 0 {
result *= 10.;
result += *digit as f64;
} else {
result += *digit as f64 * 0.1_f64.powi(running_decimal_place);
running_decimal_place += 1;
}
}
if self.negative {
result = -result;
}
Some(result)
}
pub fn clear(&mut self) {
self.digits.clear();
self.contains_decimal = false;
self.negative = false;
}
}
@@ -0,0 +1,25 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::iter::FromIterator;
/// Necessary because serde can't serialize hashmaps when the keys don't implement display.
pub fn serialize<'a, T, K, V, S>(target: T, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: IntoIterator<Item = (&'a K, &'a V)>,
K: Serialize + 'a,
V: Serialize + 'a,
{
let container: Vec<_> = target.into_iter().collect();
serde::Serialize::serialize(&container, ser)
}
pub fn deserialize<'de, T, K, V, D>(des: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromIterator<(K, V)>,
K: Deserialize<'de>,
V: Deserialize<'de>,
{
let container: Vec<_> = serde::Deserialize::deserialize(des)?;
Ok(T::from_iter(container.into_iter()))
}
@@ -0,0 +1,11 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, PortfolioMessage, MenuBar)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum MenuBarMessage {
// Messages
SendLayout,
}
@@ -0,0 +1,334 @@
use super::MenuBarMessage;
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::layout_widget::{Layout, PropertyHolder};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::menu_widgets::{MenuColumn, MenuEntry, MenuEntryGroups, MenuLayout};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use std::collections::VecDeque;
#[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler {}
impl MessageHandler<MenuBarMessage, ()> for MenuBarMessageHandler {
#[remain::check]
fn process_message(&mut self, message: MenuBarMessage, _data: (), responses: &mut VecDeque<Message>) {
use MenuBarMessage::*;
#[remain::sorted]
match message {
SendLayout => self.register_properties(responses, LayoutTarget::MenuBar),
}
}
fn actions(&self) -> ActionList {
actions!(MenuBarMessageDiscriminant;)
}
}
impl PropertyHolder for MenuBarMessageHandler {
fn properties(&self) -> Layout {
Layout::MenuLayout(MenuLayout::new(vec![
MenuColumn {
label: "File".into(),
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "New…".into(),
icon: Some("File".into()),
action: MenuEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestNewDocumentDialog),
children: MenuEntryGroups::empty(),
},
MenuEntry {
label: "Open…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Open Recent".into(),
shortcut: None,
action: MenuEntry::no_action(),
icon: None,
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Reopen Last Closed".into(),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyT],
..MenuEntry::default()
},
MenuEntry {
label: "Clear Recently Opened".into(),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Some Recent File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Another Recent File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "An Older File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Some Other Older File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Yet Another Older File.gdd".into(),
..MenuEntry::default()
},
],
]),
},
],
vec![
MenuEntry {
label: "Close".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation),
action: MenuEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Close All".into(),
shortcut: action_keys!(DialogMessageDiscriminant::CloseAllDocumentsWithConfirmation),
action: MenuEntry::create_action(|_| DialogMessage::CloseAllDocumentsWithConfirmation.into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Save".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Save As…".into(),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
label: "Save All".into(),
// shortcut: [Key::KeyControl, Key::KeyAlt, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
label: "Auto-Save".into(),
icon: Some("CheckboxChecked".into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Import…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Export…".into(),
shortcut: action_keys!(DialogMessageDiscriminant::RequestExportDialog),
action: MenuEntry::create_action(|_| DialogMessage::RequestExportDialog.into()),
..MenuEntry::default()
},
],
vec![MenuEntry {
label: "Quit".into(),
// shortcut: [Key::KeyControl, Key::KeyQ],
..MenuEntry::default()
}],
]),
},
MenuColumn {
label: "Edit".into(),
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Undo".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::Undo),
action: MenuEntry::create_action(|_| DocumentMessage::Undo.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Redo".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::Redo),
action: MenuEntry::create_action(|_| DocumentMessage::Redo.into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Cut".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Cut),
action: MenuEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Copy".into(),
icon: Some("Copy".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::Copy),
action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Paste".into(),
icon: Some("Paste".into()),
shortcut: action_keys!(FrontendMessageDiscriminant::TriggerPaste),
action: MenuEntry::create_action(|_| FrontendMessage::TriggerPaste.into()),
..MenuEntry::default()
},
],
]),
},
MenuColumn {
label: "Layer".into(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Select All".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Deselect All".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::DeselectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Order".into(),
action: MenuEntry::no_action(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Raise To Front".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Raise".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLower),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower to Back".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()),
..MenuEntry::default()
},
]]),
..MenuEntry::default()
},
]]),
},
MenuColumn {
label: "Document".into(),
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Menu entries coming soon".into(),
..MenuEntry::default()
}]]),
},
MenuColumn {
label: "View".into(),
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Show/Hide Node Graph (In Development)".into(),
action: MenuEntry::create_action(|_| WorkspaceMessage::NodeGraphToggleVisibility.into()),
..MenuEntry::default()
}]]),
},
MenuColumn {
label: "Help".into(),
children: MenuEntryGroups(vec![
vec![MenuEntry {
label: "About Graphite".into(),
action: MenuEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()),
..MenuEntry::default()
}],
vec![
MenuEntry {
label: "Report a Bug".into(),
action: MenuEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
}),
..MenuEntry::default()
},
MenuEntry {
label: "Visit on GitHub".into(),
action: MenuEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite".into(),
}
.into()
}),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Debug: Print Messages".into(),
action: MenuEntry::no_action(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Off".into(),
// icon: Some("Checkmark".into()), // TODO: Find a way to set this icon on the active mode
shortcut: action_keys!(DebugMessageDiscriminant::MessageOff),
action: MenuEntry::create_action(|_| DebugMessage::MessageOff.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Only Names".into(),
shortcut: action_keys!(DebugMessageDiscriminant::MessageNames),
action: MenuEntry::create_action(|_| DebugMessage::MessageNames.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Full Contents".into(),
shortcut: action_keys!(DebugMessageDiscriminant::MessageContents),
action: MenuEntry::create_action(|_| DebugMessage::MessageContents.into()),
..MenuEntry::default()
},
]]),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Print Trace Logs".into(),
icon: Some(if let log::LevelFilter::Trace = log::max_level() { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(DebugMessageDiscriminant::ToggleTraceLogs),
action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Print Document".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::DebugPrintDocument),
action: MenuEntry::create_action(|_| DocumentMessage::DebugPrintDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Panic (DANGER)".into(),
action: MenuEntry::create_action(|_| panic!()),
..MenuEntry::default()
},
],
]),
},
]))
}
}
@@ -0,0 +1,7 @@
mod menu_bar_message;
mod menu_bar_message_handler;
#[doc(inline)]
pub use menu_bar_message::{MenuBarMessage, MenuBarMessageDiscriminant};
#[doc(inline)]
pub use menu_bar_message_handler::MenuBarMessageHandler;
+10
View File
@@ -0,0 +1,10 @@
mod portfolio_message;
mod portfolio_message_handler;
pub mod document;
pub mod menu_bar;
#[doc(inline)]
pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
#[doc(inline)]
pub use portfolio_message_handler::PortfolioMessageHandler;
@@ -0,0 +1,92 @@
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::Platform;
use crate::messages::prelude::*;
use graphene::layers::text_layer::Font;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PortfolioMessage {
// Sub-messages
#[remain::unsorted]
#[child]
Document(DocumentMessage),
#[remain::unsorted]
#[child]
MenuBar(MenuBarMessage),
// Messages
AutoSaveActiveDocument,
AutoSaveDocument {
document_id: u64,
},
CloseActiveDocumentWithConfirmation,
CloseAllDocuments,
CloseDocument {
document_id: u64,
},
CloseDocumentWithConfirmation {
document_id: u64,
},
Copy {
clipboard: Clipboard,
},
Cut {
clipboard: Clipboard,
},
DestroyAllDocuments,
FontLoaded {
font_family: String,
font_style: String,
preview_url: String,
data: Vec<u8>,
is_default: bool,
},
Import,
LoadFont {
font: Font,
is_default: bool,
},
NewDocumentWithName {
name: String,
},
NextDocument,
OpenDocument,
OpenDocumentFile {
document_name: String,
document_serialized_content: String,
},
OpenDocumentFileWithId {
document_id: u64,
document_name: String,
document_is_saved: bool,
document_serialized_content: String,
},
Paste {
clipboard: Clipboard,
},
PasteIntoFolder {
clipboard: Clipboard,
folder_path: Vec<LayerId>,
insert_index: isize,
},
PasteSerializedData {
data: String,
},
PrevDocument,
SelectDocument {
document_id: u64,
},
SetActiveDocument {
document_id: u64,
},
SetPlatform {
platform: Platform,
},
UpdateDocumentWidgets,
UpdateOpenDocumentsList,
}
@@ -0,0 +1,526 @@
use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::FrontendDocumentDetails;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::misc::Platform;
use crate::messages::prelude::*;
use graphene::layers::layer_info::LayerDataTypeDiscriminant;
use graphene::layers::text_layer::{Font, FontCache};
use graphene::Operation as DocumentOperation;
use log::warn;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Default)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
documents: HashMap<u64, DocumentMessageHandler>,
document_ids: Vec<u64>,
active_document_id: Option<u64>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
font_cache: FontCache,
pub platform: Platform,
}
impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for PortfolioMessageHandler {
#[remain::check]
fn process_message(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
use PortfolioMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
Document(message) => {
if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) {
document.process_message(message, (ipp, &self.font_cache), responses)
}
}
#[remain::unsorted]
MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
// Messages
AutoSaveActiveDocument => {
if let Some(document_id) = self.active_document_id {
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into());
}
}
AutoSaveDocument { document_id } => {
let document = self.documents.get(&document_id).unwrap();
responses.push_back(
FrontendMessage::TriggerIndexedDbWriteDocument {
document: document.serialize_document(),
details: FrontendDocumentDetails {
is_saved: document.is_saved(),
id: document_id,
name: document.name.clone(),
},
version: GRAPHITE_DOCUMENT_VERSION.to_string(),
}
.into(),
)
}
CloseActiveDocumentWithConfirmation => {
if let Some(document_id) = self.active_document_id {
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into());
}
}
CloseAllDocuments => {
if self.active_document_id.is_some() {
responses.push_back(PropertiesPanelMessage::Deactivate.into());
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(ToolMessage::DeactivateTools.into());
}
for document_id in &self.document_ids {
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id: *document_id }.into());
}
responses.push_back(PortfolioMessage::DestroyAllDocuments.into());
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
}
CloseDocument { document_id } => {
let document_index = self.document_index(document_id);
self.documents.remove(&document_id);
self.document_ids.remove(document_index);
if self.document_ids.is_empty() {
self.active_document_id = None;
} else if Some(document_id) == self.active_document_id {
if document_index == self.document_ids.len() {
// If we closed the last document take the one previous (same as last)
responses.push_back(
PortfolioMessage::SelectDocument {
document_id: *self.document_ids.last().unwrap(),
}
.into(),
);
} else {
// Move to the next tab
responses.push_back(
PortfolioMessage::SelectDocument {
document_id: self.document_ids[document_index],
}
.into(),
);
}
}
// Send the new list of document tab names
responses.push_back(UpdateOpenDocumentsList.into());
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }.into());
responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
if let Some(document) = self.active_document() {
for layer in document.layer_metadata.keys() {
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
}
}
}
CloseDocumentWithConfirmation { document_id } => {
let target_document = self.documents.get(&document_id).unwrap();
if target_document.is_saved() {
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
} else {
let dialog = simple_dialogs::CloseDocumentDialog {
document_name: target_document.name.clone(),
document_id,
};
dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
// Select the document being closed
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
}
}
Copy { clipboard } => {
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
if let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get(&id)) {
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
for layer_path in active_document.selected_layers_without_children() {
match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) {
(Ok(layer), layer_metadata) => {
buffer.push(CopyBufferEntry { layer, layer_metadata });
}
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
}
}
};
if clipboard == Clipboard::Device {
let mut buffer = Vec::new();
copy_val(&mut buffer);
let mut copy_text = String::from("graphite/layer: ");
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste");
responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into());
} else {
let copy_buffer = &mut self.copy_buffer;
copy_buffer[clipboard as usize].clear();
copy_val(&mut copy_buffer[clipboard as usize]);
}
}
}
Cut { clipboard } => {
responses.push_back(Copy { clipboard }.into());
responses.push_back(DeleteSelectedLayers.into());
}
DestroyAllDocuments => {
// Empty the list of internal document data
self.documents.clear();
self.document_ids.clear();
self.active_document_id = None;
}
FontLoaded {
font_family,
font_style,
preview_url,
data,
is_default,
} => {
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
if let Some(document) = self.active_document_mut() {
document.graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text);
responses.push_back(DocumentMessage::RenderDocument.into());
}
}
Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
if self.active_document().is_some() {
responses.push_back(FrontendMessage::TriggerImport.into());
}
}
LoadFont { font, is_default } => {
if !self.font_cache.loaded_font(&font) {
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into());
}
}
NewDocumentWithName { name } => {
let new_document = DocumentMessageHandler::with_name(name, ipp);
let document_id = generate_uuid();
if self.active_document().is_some() {
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
}
self.load_document(new_document, document_id, responses);
}
NextDocument => {
if let Some(active_document_id) = self.active_document_id {
let current_index = self.document_index(active_document_id);
let next_index = (current_index + 1) % self.document_ids.len();
let next_id = self.document_ids[next_index];
responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into());
}
}
OpenDocument => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.push_back(FrontendMessage::TriggerOpenDocument.into());
}
OpenDocumentFile {
document_name,
document_serialized_content,
} => {
responses.push_back(
PortfolioMessage::OpenDocumentFileWithId {
document_id: generate_uuid(),
document_name,
document_is_saved: true,
document_serialized_content,
}
.into(),
);
}
OpenDocumentFileWithId {
document_id,
document_name,
document_is_saved,
document_serialized_content,
} => {
let document = DocumentMessageHandler::with_name_and_content(document_name, document_serialized_content);
match document {
Ok(mut document) => {
document.set_save_state(document_is_saved);
self.load_document(document, document_id, responses);
}
Err(e) => responses.push_back(
DialogMessage::DisplayDialogError {
title: "Failed to open document".to_string(),
description: e.to_string(),
}
.into(),
),
}
}
Paste { clipboard } => {
let shallowest_common_folder = self.active_document().map(|document| {
document
.graphene_document
.shallowest_common_folder(document.selected_layers())
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion")
});
if let Some(folder) = shallowest_common_folder {
responses.push_back(DeselectAllLayers.into());
responses.push_back(StartTransaction.into());
responses.push_back(
PasteIntoFolder {
clipboard,
folder_path: folder.to_vec(),
insert_index: -1,
}
.into(),
);
responses.push_back(CommitTransaction.into());
}
}
PasteIntoFolder {
clipboard,
folder_path: path,
insert_index,
} => {
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| {
if let Some(document) = self.active_document() {
log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index);
let destination_path = [path.to_vec(), vec![generate_uuid()]].concat();
responses.push_front(
DocumentMessage::UpdateLayerMetadata {
layer_path: destination_path.clone(),
layer_metadata: entry.layer_metadata,
}
.into(),
);
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone());
responses.push_front(
DocumentOperation::InsertLayer {
layer: entry.layer.clone(),
destination_path,
insert_index,
}
.into(),
);
}
};
if insert_index == -1 {
for entry in self.copy_buffer[clipboard as usize].iter().rev() {
paste(entry, responses)
}
} else {
for entry in self.copy_buffer[clipboard as usize].iter() {
paste(entry, responses)
}
}
}
PasteSerializedData { data } => {
if let Some(document) = self.active_document() {
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
let shallowest_common_folder = document
.graphene_document
.shallowest_common_folder(document.selected_layers())
.expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
responses.push_back(DeselectAllLayers.into());
responses.push_back(StartTransaction.into());
for entry in data.iter().rev() {
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
responses.push_front(
DocumentMessage::UpdateLayerMetadata {
layer_path: destination_path.clone(),
layer_metadata: entry.layer_metadata,
}
.into(),
);
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone());
responses.push_front(
DocumentOperation::InsertLayer {
layer: entry.layer.clone(),
destination_path,
insert_index: -1,
}
.into(),
);
}
responses.push_back(CommitTransaction.into());
}
}
}
PrevDocument => {
if let Some(active_document_id) = self.active_document_id {
let len = self.document_ids.len();
let current_index = self.document_index(active_document_id);
let prev_index = (current_index + len - 1) % len;
let prev_id = self.document_ids[prev_index];
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
}
}
SelectDocument { document_id } => {
if let Some(document) = self.active_document() {
if !document.is_saved() {
// Safe to unwrap since we know that there is an active document
responses.push_back(
PortfolioMessage::AutoSaveDocument {
document_id: self.active_document_id.unwrap(),
}
.into(),
);
}
}
if self.active_document().is_some() {
responses.push_back(BroadcastEvent::ToolAbort.into());
}
// TODO: Remove this message in favor of having tools have specific data per document instance
responses.push_back(SetActiveDocument { document_id }.into());
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.documents.get(&document_id).unwrap().layer_metadata.keys() {
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
}
responses.push_back(BroadcastEvent::SelectionChanged.into());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
}
SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
SetPlatform { platform } => self.platform = platform,
UpdateDocumentWidgets => {
if let Some(document) = self.active_document() {
document.update_document_widgets(responses);
}
}
UpdateOpenDocumentsList => {
// Send the list of document tab names
let open_documents = self
.document_ids
.iter()
.filter_map(|id| {
self.documents.get(id).map(|doc| FrontendDocumentDetails {
is_saved: doc.is_saved(),
id: *id,
name: doc.name.clone(),
})
})
.collect::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(PortfolioMessageDiscriminant;
CloseActiveDocumentWithConfirmation,
CloseAllDocuments,
Import,
NextDocument,
OpenDocument,
Paste,
PasteIntoFolder,
PrevDocument,
);
if let Some(document) = self.active_document() {
if document.layer_metadata.values().any(|data| data.selected) {
let select = actions!(PortfolioMessageDiscriminant;
Copy,
Cut,
);
common.extend(select);
}
common.extend(document.actions());
}
common
}
}
impl PortfolioMessageHandler {
pub fn active_document(&self) -> Option<&DocumentMessageHandler> {
self.active_document_id.and_then(|id| self.documents.get(&id))
}
pub fn active_document_mut(&mut self) -> Option<&mut DocumentMessageHandler> {
self.active_document_id.and_then(|id| self.documents.get_mut(&id))
}
pub fn generate_new_document_name(&self) -> String {
let mut doc_title_numbers = self
.ordered_document_iterator()
.filter_map(|doc| {
doc.name
.rsplit_once(DEFAULT_DOCUMENT_NAME)
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
})
.collect::<Vec<isize>>();
doc_title_numbers.sort_unstable();
doc_title_numbers.iter_mut().enumerate().for_each(|(i, number)| *number = *number - i as isize - 2);
// Uses binary search to find the index of the element where number is bigger than i
let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1;
match new_doc_title_num {
1 => DEFAULT_DOCUMENT_NAME.to_string(),
_ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num),
}
}
// TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load*
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, responses: &mut VecDeque<Message>) {
self.document_ids.push(document_id);
responses.extend(
new_document
.layer_metadata
.keys()
.filter_map(|path| new_document.layer_panel_entry_from_path(path, &self.font_cache))
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
.collect::<Vec<_>>(),
);
new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache);
new_document.load_layer_resources(responses, &new_document.graphene_document.root.data, Vec::new());
self.documents.insert(document_id, new_document);
if self.active_document().is_some() {
responses.push_back(PropertiesPanelMessage::Deactivate.into());
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(ToolMessage::DeactivateTools.into());
}
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
responses.push_back(ToolMessage::InitTools.into());
responses.push_back(PropertiesPanelMessage::Init.into());
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into())
}
/// Returns an iterator over the open documents in order.
pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> {
self.document_ids.iter().map(|id| self.documents.get(id).expect("document id was not found in the document hashmap"))
}
fn document_index(&self, document_id: u64) -> usize {
self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids")
}
pub fn font_cache(&self) -> &FontCache {
&self.font_cache
}
}
+46
View File
@@ -0,0 +1,46 @@
// Message, MessageDiscriminant, MessageHandler
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::dialog::export_dialog::ExportDialogMessageHandler;
pub use crate::messages::dialog::new_document_dialog::NewDocumentDialogMessageHandler;
pub use crate::messages::dialog::{DialogMessage, DialogMessageDiscriminant, DialogMessageHandler};
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler};
pub use crate::messages::portfolio::document::artboard::{ArtboardMessage, ArtboardMessageDiscriminant, ArtboardMessageHandler};
pub use crate::messages::portfolio::document::movement::{MovementMessage, MovementMessageDiscriminant, MovementMessageHandler};
pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageDiscriminant, OverlaysMessageHandler};
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
// Message, MessageDiscriminant
pub use crate::messages::message::{Message, MessageDiscriminant};
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
// Other
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
pub use crate::utility_traits::{ActionList, AsMessage, MessageHandler, ToDiscriminant, TransitiveChild};
pub use graphite_proc_macros::*;
pub use std::collections::VecDeque;
@@ -0,0 +1,6 @@
pub mod overlay_renderer;
pub mod path_outline;
pub mod resize;
pub mod shape_editor;
pub mod snapping;
pub mod transformation_cage;
@@ -0,0 +1,314 @@
use crate::application::generate_uuid;
use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS;
use crate::consts::{COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::document::Document;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
use graphene::layers::vector::subpath::Subpath;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::{HashMap, VecDeque};
/// [ManipulatorGroupOverlay]s is the collection of overlays that make up an [ManipulatorGroup] visible in the editor.
type ManipulatorGroupOverlays = [Option<Vec<LayerId>>; 5];
type ManipulatorId = u64;
const POINT_STROKE_WEIGHT: f64 = 2.;
#[derive(Clone, Debug, Default)]
pub struct OverlayRenderer {
shape_overlay_cache: HashMap<LayerId, Vec<LayerId>>,
manipulator_group_overlay_cache: HashMap<(LayerId, ManipulatorId), ManipulatorGroupOverlays>,
}
impl OverlayRenderer {
pub fn new() -> Self {
OverlayRenderer {
manipulator_group_overlay_cache: HashMap::new(),
shape_overlay_cache: HashMap::new(),
}
}
pub fn render_subpath_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
let transform = document.generate_transform_relative_to_viewport(&layer_path).ok().unwrap();
if let Ok(layer) = document.layer(&layer_path) {
let layer_id = layer_path.last().unwrap();
self.layer_overlay_visibility(document, layer_path.clone(), true, responses);
if let Some(shape) = layer.as_subpath() {
let outline_cache = self.shape_overlay_cache.get(layer_id);
log::trace!("Overlay: Outline cache {:?}", &outline_cache);
// Create an outline if we do not have a cached one
if outline_cache == None {
let outline_path = self.create_shape_outline_overlay(shape.clone(), responses);
self.shape_overlay_cache.insert(*layer_id, outline_path.clone());
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
log::trace!("Overlay: Creating new outline {:?}", &outline_path);
} else if let Some(outline_path) = outline_cache {
log::trace!("Overlay: Updating overlays for {:?} owning layer: {:?}", outline_path, layer_id);
Self::modify_outline_overlays(outline_path.clone(), shape.clone(), responses);
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
}
// Create, place, and style the manipulator overlays
for (manipulator_group_id, manipulator_group) in shape.manipulator_groups().enumerate() {
let manipulator_group_cache = self.manipulator_group_overlay_cache.get_mut(&(*layer_id, *manipulator_group_id));
// If cached update placement and style
if let Some(manipulator_group_overlays) = manipulator_group_cache {
log::trace!("Overlay: Updating detail overlays for {:?}", manipulator_group_overlays);
Self::place_manipulator_group_overlays(manipulator_group, manipulator_group_overlays, &transform, responses);
Self::style_overlays(manipulator_group, manipulator_group_overlays, responses);
} else {
// Create if not cached
let mut manipulator_group_overlays = [
Some(self.create_anchor_overlay(responses)),
Self::create_handle_overlay_if_exists(&manipulator_group.points[ManipulatorType::InHandle], responses),
Self::create_handle_overlay_if_exists(&manipulator_group.points[ManipulatorType::OutHandle], responses),
Self::create_handle_line_overlay_if_exists(&manipulator_group.points[ManipulatorType::InHandle], responses),
Self::create_handle_line_overlay_if_exists(&manipulator_group.points[ManipulatorType::OutHandle], responses),
];
Self::place_manipulator_group_overlays(manipulator_group, &mut manipulator_group_overlays, &transform, responses);
Self::style_overlays(manipulator_group, &manipulator_group_overlays, responses);
self.manipulator_group_overlay_cache.insert((*layer_id, *manipulator_group_id), manipulator_group_overlays);
}
}
// TODO Handle removing shapes from cache so we don't memory leak
// Eventually will get replaced with am immediate mode renderer for overlays
}
}
}
pub fn clear_subpath_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
let layer_id = layer_path.last().unwrap();
// Remove the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
Self::remove_outline_overlays(overlay_path.clone(), responses)
}
self.shape_overlay_cache.remove(layer_id);
// Remove the ManipulatorGroup overlays
if let Ok(layer) = document.layer(&layer_path) {
if let Some(shape) = layer.as_subpath() {
for (id, _) in shape.manipulator_groups().enumerate() {
if let Some(manipulator_group_overlays) = self.manipulator_group_overlay_cache.get(&(*layer_id, *id)) {
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
self.manipulator_group_overlay_cache.remove(&(*layer_id, *id));
}
}
}
}
}
pub fn layer_overlay_visibility(&mut self, document: &Document, layer_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
let layer_id = layer_path.last().unwrap();
// Hide the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
}
// Hide the manipulator group overlays
if let Ok(layer) = document.layer(&layer_path) {
if let Some(shape) = layer.as_subpath() {
for (id, _) in shape.manipulator_groups().enumerate() {
if let Some(manipulator_group_overlays) = self.manipulator_group_overlay_cache.get(&(*layer_id, *id)) {
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
}
}
}
}
}
/// Create the kurbo shape that matches the selected viewport shape.
fn create_shape_outline_overlay(&self, subpath: Subpath, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddShape {
path: layer_path.clone(),
subpath,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single anchor overlay and return its layer ID.
fn create_anchor_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single handle overlay and return its layer ID.
fn create_handle_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single handle overlay and return its layer id if it exists.
fn create_handle_overlay_if_exists(handle: &Option<ManipulatorPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_overlay(responses))
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_front(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay_if_exists(handle: &Option<ManipulatorPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_line_overlay(responses))
}
fn place_outline_overlays(outline_path: Vec<LayerId>, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let transform_message = Self::overlay_transform_message(outline_path, parent_transform.to_cols_array());
responses.push_back(transform_message);
}
fn modify_outline_overlays(outline_path: Vec<LayerId>, subpath: Subpath, responses: &mut VecDeque<Message>) {
let outline_modify_message = Self::overlay_modify_message(outline_path, subpath);
responses.push_back(outline_modify_message);
}
/// Updates the position of the overlays based on the [Subpath] points.
fn place_manipulator_group_overlays(manipulator_group: &ManipulatorGroup, overlays: &mut ManipulatorGroupOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
if let Some(manipulator_point) = &manipulator_group.points[ManipulatorType::Anchor] {
// Helper function to keep things DRY (don't-repeat-yourself)
let mut place_handle_and_line = |handle: &ManipulatorPoint, line_source: &mut Option<Vec<LayerId>>, marker_source: &mut Option<Vec<LayerId>>| {
let line_overlay = line_source.take().unwrap_or_else(|| Self::create_handle_line_overlay(responses));
let line_vector = parent_transform.transform_point2(manipulator_point.position) - parent_transform.transform_point2(handle.position);
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (parent_transform.transform_point2(handle.position) + VIEWPORT_GRID_ROUNDING_BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(Self::overlay_transform_message(line_overlay.clone(), transform));
*line_source = Some(line_overlay);
let marker_overlay = marker_source.take().unwrap_or_else(|| Self::create_handle_overlay(responses));
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(handle.position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(Self::overlay_transform_message(marker_overlay.clone(), transform));
*marker_source = Some(marker_overlay);
};
// Place the handle overlays
let [_, h1, h2] = &manipulator_group.points;
let [a, b, c, line1, line2] = overlays;
let markers = [a, b, c];
if let Some(handle) = &h1 {
place_handle_and_line(handle, line1, markers[handle.manipulator_type as usize]);
}
if let Some(handle) = &h2 {
place_handle_and_line(handle, line2, markers[handle.manipulator_type as usize]);
}
// Place the anchor point overlay
if let Some(anchor_overlay) = &overlays[ManipulatorType::Anchor as usize] {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(manipulator_point.position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let message = Self::overlay_transform_message(anchor_overlay.clone(), transform);
responses.push_back(message);
}
}
}
/// Removes the manipulator overlays from the overlay document.
fn remove_manipulator_group_overlays(overlay_paths: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
overlay_paths.iter().flatten().for_each(|layer_id| {
log::trace!("Overlay: Sending delete message for: {:?}", layer_id);
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()).into());
});
}
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.into()).into());
}
/// Sets the visibility of the handles overlay.
fn set_manipulator_group_overlay_visibility(manipulator_group_overlays: &ManipulatorGroupOverlays, visibility: bool, responses: &mut VecDeque<Message>) {
manipulator_group_overlays.iter().flatten().for_each(|layer_id| {
responses.push_back(Self::overlay_visibility_message(layer_id.clone(), visibility));
});
}
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
responses.push_back(Self::overlay_visibility_message(overlay_path, visibility));
}
/// Create a visibility message for an overlay.
fn overlay_visibility_message(layer_path: Vec<LayerId>, visibility: bool) -> Message {
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: layer_path,
visible: visibility,
}
.into(),
)
.into()
}
/// Create a transform message for an overlay.
fn overlay_transform_message(layer_path: Vec<LayerId>, transform: [f64; 6]) -> Message {
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: layer_path, transform }.into()).into()
}
/// Create an update message for an overlay.
fn overlay_modify_message(layer_path: Vec<LayerId>, subpath: Subpath) -> Message {
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, subpath }.into()).into()
}
/// Sets the overlay style for this point.
fn style_overlays(manipulator_group: &ManipulatorGroup, overlays: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
// TODO Move the style definitions out of the Subpath, should be looked up from a stylesheet or similar
let selected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
// Update if the manipulator points are shown as selected
// Here the index is important, even though overlays[..] has five elements we only care about the first three
for (index, point) in manipulator_group.points.iter().enumerate() {
if let Some(point) = point {
if let Some(overlay) = &overlays[index] {
let style = if point.editor_state.is_selected { selected_style.clone() } else { deselected_style.clone() };
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay.clone(), style }.into()).into());
}
}
}
}
}
@@ -0,0 +1,132 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT, SELECTION_TOLERANCE};
use crate::messages::prelude::*;
use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::text_layer::FontCache;
use graphene::layers::vector::subpath::Subpath;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::VecDeque;
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
#[derive(Clone, Debug, Default)]
pub struct PathOutline {
hovered_layer_path: Option<Vec<LayerId>>,
hovered_overlay_path: Option<Vec<LayerId>>,
selected_overlay_paths: Vec<Vec<LayerId>>,
}
impl PathOutline {
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one
fn create_outline(
document_layer_path: Vec<LayerId>,
overlay_path: Option<Vec<LayerId>>,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
font_cache: &FontCache,
) -> Option<Vec<LayerId>> {
// Get layer data
let document_layer = document.graphene_document.layer(&document_layer_path).ok()?;
// TODO Purge this area of BezPath and Kurbo
// Get the bezpath from the shape or text
let subpath = match &document_layer.data {
LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()),
LayerDataType::Text(text) => Some(text.to_subpath_nonmut(font_cache)),
_ => document_layer.aabb_for_transform(DAffine2::IDENTITY, font_cache).map(|[p1, p2]| Subpath::new_rect(p1, p2)),
}?;
// Generate a new overlay layer if necessary
let overlay = match overlay_path {
Some(path) => path,
None => {
let overlay_path = vec![generate_uuid()];
let operation = Operation::AddShape {
path: overlay_path.clone(),
subpath: Default::default(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
overlay_path
}
};
// Update the shape bezpath
let operation = Operation::SetShapePath { path: overlay.clone(), subpath };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
// Update the transform to match the document
let operation = Operation::SetLayerTransform {
path: overlay.clone(),
transform: document.graphene_document.multiply_transforms(&document_layer_path).unwrap().to_cols_array(),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
Some(overlay)
}
/// Removes the hovered overlay and deletes path references
pub fn clear_hovered(&mut self, responses: &mut VecDeque<Message>) {
if let Some(path) = self.hovered_overlay_path.take() {
let operation = Operation::DeleteLayer { path };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
self.hovered_layer_path = None;
}
/// Performs an intersect test and generates a hovered overlay if necessary
pub fn intersect_test_hovered(&mut self, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
// Get the layer the user is hovering over
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
let mut intersection = document.graphene_document.intersects_quad_root(quad, font_cache);
// If the user is hovering over a layer they have not already selected, then update outline
if let Some(path) = intersection.pop() {
if !document.selected_visible_layers().any(|visible| visible == path.as_slice()) {
// Updates the overlay, generating a new one if necessary
self.hovered_overlay_path = Self::create_outline(path.clone(), self.hovered_overlay_path.take(), document, responses, font_cache);
if self.hovered_overlay_path.is_none() {
self.clear_hovered(responses);
}
self.hovered_layer_path = Some(path);
} else {
self.clear_hovered(responses);
}
} else {
self.clear_hovered(responses);
}
}
/// Clears overlays for the selected paths and removes references
pub fn clear_selected(&mut self, responses: &mut VecDeque<Message>) {
while let Some(path) = self.selected_overlay_paths.pop() {
let operation = Operation::DeleteLayer { path };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
}
/// Updates the selected overlays, generating or removing overlays if necessary
pub fn update_selected<'a>(&mut self, selected: impl Iterator<Item = &'a [LayerId]>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
for document_layer_path in selected {
if let Some(overlay_path) = Self::create_outline(document_layer_path.to_vec(), old_overlay_paths.pop(), document, responses, font_cache) {
self.selected_overlay_paths.push(overlay_path);
}
}
for path in old_overlay_paths {
let operation = Operation::DeleteLayer { path };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
}
}
@@ -0,0 +1,65 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2, Vec2Swizzles};
#[derive(Clone, Debug, Default)]
pub struct Resize {
pub drag_start: ViewportPosition,
pub path: Option<Vec<LayerId>>,
snap_manager: SnapManager,
}
impl Resize {
/// Starts a resize, assigning the snap targets and snapping the starting position.
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, font_cache: &FontCache) {
self.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
self.snap_manager.add_all_document_handles(document, &[], &[], &[]);
self.drag_start = self.snap_manager.snap_position(responses, document, mouse_position);
}
pub fn calculate_transform(
&mut self,
responses: &mut VecDeque<Message>,
document: &DocumentMessageHandler,
center: Key,
lock_ratio: Key,
ipp: &InputPreprocessorMessageHandler,
) -> Option<Message> {
if let Some(path) = &self.path {
let mut start = self.drag_start;
let stop = self.snap_manager.snap_position(responses, document, ipp.mouse.position);
let mut size = stop - start;
if ipp.keyboard.get(lock_ratio as usize) {
size = size.abs().max(size.abs().yx()) * size.signum();
}
if ipp.keyboard.get(center as usize) {
start -= size;
size *= 2.;
}
Some(
Operation::SetLayerTransformInViewport {
path: path.to_vec(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into(),
)
} else {
None
}
}
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_manager.cleanup(responses);
self.path = None;
}
}
@@ -0,0 +1,266 @@
use crate::messages::prelude::*;
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
use graphene::layers::vector::subpath::Subpath;
use graphene::{LayerId, Operation};
use glam::DVec2;
use graphene::document::Document;
use std::collections::VecDeque;
/// ShapeEditor is the container for all of the layer paths that are represented as [Subpath]s and provides
/// functionality required to query and create the [Subpath] / [ManipulatorGroup]s / [ManipulatorPoint]s.
///
/// Overview:
/// ```text
/// ShapeEditor
/// |
/// selected_layers <- Paths to selected layers that may contain Subpaths
/// / | \
/// Subpath ... Subpath <- Reference from layer paths, one Subpath per layer (for now, will eventually be a CompoundPath)
/// / | \
/// ManipulatorGroup ... ManipulatorGroup <- Subpath contains many ManipulatorGroups
/// ```
#[derive(Clone, Debug, Default)]
pub struct ShapeEditor {
// The layers we can select and edit manipulators (anchors and handles) from
selected_layers: Vec<Vec<LayerId>>,
}
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
impl ShapeEditor {
/// Select the first point within the selection threshold.
/// Returns the points if found, None otherwise.
pub fn select_point(
&self,
document: &Document,
mouse_position: DVec2,
select_threshold: f64,
add_to_selection: bool,
responses: &mut VecDeque<Message>,
) -> Option<Vec<(&[LayerId], u64, ManipulatorType)>> {
if self.selected_layers.is_empty() {
return None;
}
if let Some((shape_layer_path, manipulator_group_id, manipulator_point_index)) = self.find_nearest_point_indices(document, mouse_position, select_threshold) {
log::trace!("Selecting... manipulator group ID: {}, manipulator point index: {}", manipulator_group_id, manipulator_point_index);
// If the point we're selecting has already been selected
// we can assume this point exists.. since we did just click on it hence the unwrap
let is_point_selected = self.shape(document, shape_layer_path).unwrap().manipulator_groups().by_id(manipulator_group_id).unwrap().points[manipulator_point_index]
.as_ref()
.unwrap()
.editor_state
.is_selected;
let point_position = self.shape(document, shape_layer_path).unwrap().manipulator_groups().by_id(manipulator_group_id).unwrap().points[manipulator_point_index]
.as_ref()
.unwrap()
.position;
// The currently selected points (which are then modified to reflect the selection)
let mut points = self
.selected_layers()
.iter()
.filter_map(|path| document.layer(path).ok().map(|layer| (path, layer)))
.filter_map(|(path, shape)| shape.as_subpath().map(|subpath| (path, subpath)))
.flat_map(|(path, shape)| {
shape
.manipulator_groups()
.enumerate()
.filter(|(_id, manipulator_group)| manipulator_group.is_anchor_selected())
.flat_map(|(id, manipulator_group)| manipulator_group.selected_points().map(move |point| (id, point.manipulator_type)))
.map(|(anchor, manipulator_point)| (path.as_slice(), *anchor, manipulator_point))
})
.collect::<Vec<_>>();
// Should we select or deselect the point?
let should_select = if is_point_selected { !add_to_selection } else { true };
// This is selecting the manipulator only for now, next to generalize to points
if should_select {
let add = add_to_selection || is_point_selected;
let point = (manipulator_group_id, ManipulatorType::from_index(manipulator_point_index));
// Clear all point in other selected shapes
if !add {
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
points = vec![(shape_layer_path, point.0, point.1)];
} else {
points.push((shape_layer_path, point.0, point.1));
}
responses.push_back(
Operation::SelectManipulatorPoints {
layer_path: shape_layer_path.to_vec(),
point_ids: vec![point],
add,
}
.into(),
);
// Snap the selected point to the cursor
if let Ok(viewspace) = document.generate_transform_relative_to_viewport(shape_layer_path) {
self.move_selected_points(mouse_position - viewspace.transform_point2(point_position), mouse_position, responses)
}
} else {
responses.push_back(
Operation::DeselectManipulatorPoints {
layer_path: shape_layer_path.to_vec(),
point_ids: vec![(manipulator_group_id, ManipulatorType::from_index(manipulator_point_index))],
}
.into(),
);
points.retain(|x| *x != (shape_layer_path, manipulator_group_id, ManipulatorType::from_index(manipulator_point_index)))
}
return Some(points);
}
// Deselect all points if no nearby point
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
None
}
/// A wrapper for `find_nearest_point_indices()` and returns a [ManipulatorPoint].
pub fn find_nearest_point<'a>(&'a self, document: &'a Document, mouse_position: DVec2, select_threshold: f64) -> Option<&'a ManipulatorPoint> {
let (shape_layer_path, manipulator_group_id, manipulator_point_index) = self.find_nearest_point_indices(document, mouse_position, select_threshold)?;
let selected_shape = self.shape(document, shape_layer_path).unwrap();
if let Some(manipulator_group) = selected_shape.manipulator_groups().by_id(manipulator_group_id) {
return manipulator_group.points[manipulator_point_index].as_ref();
}
None
}
/// Set the shapes we consider for selection, we will choose draggable manipulators from these shapes.
pub fn set_selected_layers(&mut self, target_layers: Vec<Vec<LayerId>>) {
self.selected_layers = target_layers;
}
pub fn selected_layers(&self) -> &Vec<Vec<LayerId>> {
&self.selected_layers
}
pub fn selected_layers_ref(&self) -> Vec<&[LayerId]> {
self.selected_layers.iter().map(|l| l.as_slice()).collect::<Vec<_>>()
}
/// Clear all of the shapes we can modify.
pub fn clear_selected_layers(&mut self) {
self.selected_layers.clear();
}
pub fn has_selected_layers(&self) -> bool {
!self.selected_layers.is_empty()
}
/// Provide the currently selected manipulators by reference.
pub fn selected_manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorGroup> {
self.iter(document).flat_map(|shape| shape.selected_manipulator_groups())
}
/// A mutable iterator of all the manipulators, regardless of selection.
pub fn manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorGroup> {
self.iter(document).flat_map(|shape| shape.manipulator_groups().iter())
}
/// Provide the currently selected points by reference.
pub fn selected_points<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorPoint> {
self.selected_manipulator_groups(document).flat_map(|manipulator_group| manipulator_group.selected_points())
}
/// Move the selected points by dragging the mouse.
pub fn move_selected_points(&self, delta: DVec2, absolute_position: DVec2, responses: &mut VecDeque<Message>) {
for layer_path in &self.selected_layers {
responses.push_back(
DocumentMessage::MoveSelectedManipulatorPoints {
layer_path: layer_path.clone(),
delta: (delta.x, delta.y),
absolute_position: (absolute_position.x, absolute_position.y),
}
.into(),
);
}
}
/// Dissolve the selected points.
pub fn delete_selected_points(&self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeleteSelectedManipulatorPoints.into());
}
/// Toggle if the handles should mirror angle across the anchor position.
pub fn toggle_handle_mirroring_on_selected(&self, toggle_angle: bool, toggle_distance: bool, responses: &mut VecDeque<Message>) {
for layer_path in &self.selected_layers {
responses.push_back(
DocumentMessage::ToggleSelectedHandleMirroring {
layer_path: layer_path.clone(),
toggle_angle,
toggle_distance,
}
.into(),
);
}
}
/// Deselect all manipulators from the shapes that the manipulation handler has created.
pub fn deselect_all_points(&self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeselectAllManipulatorPoints.into());
}
/// Iterate over the shapes.
pub fn iter<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a Subpath> + 'a {
self.selected_layers.iter().flat_map(|layer_id| document.layer(layer_id)).filter_map(|shape| shape.as_subpath())
}
/// Find a [ManipulatorPoint] that is within the selection threshold and return the layer path, an index to the [ManipulatorGroup], and an enum index for [ManipulatorPoint].
/// Return value is an `Option` of the tuple representing `(layer path, ManipulatorGroup ID, ManipulatorType enum index)`.
fn find_nearest_point_indices(&self, document: &Document, mouse_position: DVec2, select_threshold: f64) -> Option<(&[LayerId], u64, usize)> {
if self.selected_layers.is_empty() {
return None;
}
let select_threshold_squared = select_threshold * select_threshold;
// Find the closest control point among all elements of shapes_to_modify
for layer in self.selected_layers.iter() {
if let Some((manipulator_id, manipulator_point_index, distance_squared)) = self.closest_point_in_layer(document, layer, mouse_position) {
// Choose the first point under the threshold
if distance_squared < select_threshold_squared {
log::trace!("Selecting... manipulator ID: {}, manipulator point index: {}", manipulator_id, manipulator_point_index);
return Some((layer, manipulator_id, manipulator_point_index));
}
}
}
None
}
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
/// Find the closest manipulator, manipulator point, and distance so we can select path elements.
/// Brute force comparison to determine which manipulator (handle or anchor) we want to select taking O(n) time.
/// Return value is an `Option` of the tuple representing `(manipulator ID, manipulator point index, distance squared)`.
fn closest_point_in_layer(&self, document: &Document, layer_path: &[LayerId], pos: glam::DVec2) -> Option<(u64, usize, f64)> {
let mut closest_distance_squared: f64 = f64::MAX; // Not ideal
let mut result: Option<(u64, usize, f64)> = None;
if let Some(shape) = document.layer(layer_path).ok()?.as_subpath() {
let viewspace = document.generate_transform_relative_to_viewport(layer_path).ok()?;
for (manipulator_id, manipulator) in shape.manipulator_groups().enumerate() {
let manipulator_point_index = manipulator.closest_point(&viewspace, pos);
if let Some(point) = &manipulator.points[manipulator_point_index] {
if point.editor_state.can_be_selected {
let distance_squared = viewspace.transform_point2(point.position).distance_squared(pos);
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
result = Some((*manipulator_id, manipulator_point_index, distance_squared));
}
}
}
}
}
result
}
fn shape<'a>(&'a self, document: &'a Document, layer_id: &[u64]) -> Option<&'a Subpath> {
document.layer(layer_id).ok()?.as_subpath()
}
}
@@ -0,0 +1,319 @@
use crate::application::generate_uuid;
use crate::consts::{
COLOR_ACCENT, SNAP_AXIS_OVERLAY_FADE_DISTANCE, SNAP_AXIS_TOLERANCE, SNAP_AXIS_UNSNAPPED_OPACITY, SNAP_POINT_OVERLAY_FADE_FAR, SNAP_POINT_OVERLAY_FADE_NEAR, SNAP_POINT_SIZE, SNAP_POINT_TOLERANCE,
SNAP_POINT_UNSNAPPED_OPACITY,
};
use crate::messages::prelude::*;
use graphene::layers::layer_info::{Layer, LayerDataType};
use graphene::layers::style::{self, Stroke};
use graphene::layers::vector::consts::ManipulatorType;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;
// Handles snap overlays
#[derive(Debug, Clone, Default)]
struct SnapOverlays {
axis_overlay_paths: Vec<Vec<LayerId>>,
point_overlay_paths: Vec<Vec<LayerId>>,
axis_index: usize,
point_index: usize,
}
impl SnapOverlays {
/// Draws an overlay (axis or point) with the correct transform and fade opacity, reusing lines from the pool if available.
fn add_overlay(is_axis: bool, responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: Option<f64>, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
// If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform
let layer_path = if index >= overlay_paths.len() {
let layer_path = vec![generate_uuid()];
responses.push_back(
DocumentMessage::Overlays(
if is_axis {
Operation::AddLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None),
insert_index: -1,
}
} else {
Operation::AddEllipse {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
}
.into(),
)
.into(),
);
overlay_paths.push(layer_path.clone());
layer_path
}
// Otherwise, reuse an overlay from the pool and update its new transform
else {
let layer_path = overlay_paths[index].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()).into());
layer_path
};
// Then set its opacity to the fade amount
if let Some(opacity) = opacity {
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into());
}
}
/// Draw the alignment lines for an axis
/// Note: horizontal refers to the overlay line being horizontal and the snap being along the Y axis
fn draw_alignment_lines(&mut self, is_horizontal: bool, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, goal, distance) in distances.filter(|(_target, _pos, dist)| dist.abs() < SNAP_AXIS_OVERLAY_FADE_DISTANCE) {
let offset = if is_horizontal { target.y } else { target.x }.round() - 0.5;
let offset_other = if is_horizontal { target.x } else { target.y }.round() - 0.5;
let goal_axis = if is_horizontal { goal.x } else { goal.y }.round() - 0.5;
let scale = DVec2::new(offset_other - goal_axis, 1.);
let angle = if is_horizontal { 0. } else { PI / 2. };
let translation = if is_horizontal { DVec2::new(goal_axis, offset) } else { DVec2::new(offset, goal_axis) };
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let closest = if is_horizontal { closest_distance.y } else { closest_distance.x };
let opacity = if (closest - distance).abs() < 1. {
1.
} else {
SNAP_AXIS_UNSNAPPED_OPACITY - distance.abs() / (SNAP_AXIS_OVERLAY_FADE_DISTANCE / SNAP_AXIS_UNSNAPPED_OPACITY)
};
// Add line
Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths);
self.axis_index += 1;
let size = DVec2::splat(SNAP_POINT_SIZE);
// Add point at target
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1;
// Add point along line but towards goal
let translation = if is_horizontal { DVec2::new(goal.x, target.y) } else { DVec2::new(target.x, goal.y) };
let transform = DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Draw the snap points
fn draw_snap_points(&mut self, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, offset, distance) in distances.filter(|(_pos, _offset, dist)| dist.abs() < SNAP_POINT_OVERLAY_FADE_FAR) {
let active = (closest_distance - offset).length_squared() < 1.;
if active {
continue;
}
let opacity = (1. - (distance - SNAP_POINT_OVERLAY_FADE_NEAR) / (SNAP_POINT_OVERLAY_FADE_FAR - SNAP_POINT_OVERLAY_FADE_NEAR)).min(1.) / SNAP_POINT_UNSNAPPED_OPACITY;
let size = DVec2::splat(SNAP_POINT_SIZE);
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Updates the snapping overlays with the specified distances.
/// `positions_and_distances` is a tuple of `x`, `y` & `point` iterators,, each with `(position, goal, distance)` values.
fn update_overlays<X, Y, P>(&mut self, responses: &mut VecDeque<Message>, positions_and_distances: (X, Y, P), closest_distance: DVec2)
where
X: Iterator<Item = (DVec2, DVec2, f64)>,
Y: Iterator<Item = (DVec2, DVec2, f64)>,
P: Iterator<Item = (DVec2, DVec2, f64)>,
{
self.axis_index = 0;
self.point_index = 0;
let (x, y, points) = positions_and_distances;
self.draw_alignment_lines(true, y, responses, closest_distance);
self.draw_alignment_lines(false, x, responses, closest_distance);
self.draw_snap_points(points, responses, closest_distance);
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, self.axis_index);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, self.point_index);
}
/// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept.
fn remove_unused_overlays(overlay_paths: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, remove_after_index: usize) {
while overlay_paths.len() > remove_after_index {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()).into());
}
}
/// Deletes all overlays
fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, 0);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, 0);
}
}
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
pub struct SnapManager {
point_targets: Option<Vec<DVec2>>,
bound_targets: Option<Vec<DVec2>>,
snap_overlays: SnapOverlays,
snap_x: bool,
snap_y: bool,
}
impl SnapManager {
/// Computes the necessary translation to the layer to snap it (as well as updating necessary overlays)
fn calculate_snap<R>(&mut self, targets: R, responses: &mut VecDeque<Message>) -> DVec2
where
R: Iterator<Item = DVec2> + Clone,
{
let empty = Vec::new();
let snap_points = self.snap_x && self.snap_y;
let axis = self.bound_targets.as_ref().unwrap_or(&empty);
let points = if snap_points { self.point_targets.as_ref().unwrap_or(&empty) } else { &empty };
let x_axis = if self.snap_x { axis } else { &empty }
.iter()
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).x)));
let y_axis = if self.snap_y { axis } else { &empty }
.iter()
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).y)));
let points = points.iter().flat_map(|&pos| targets.clone().map(move |goal| (pos, pos - goal, (pos - goal).length())));
let min_x = x_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
let min_y = y_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
// Snap to a point if possible
let clamped_closest_distance = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
min_points.1
} else {
// Do not move if over snap tolerance
let closest_distance = DVec2::new(min_x.unwrap_or_default().2, min_y.unwrap_or_default().2);
DVec2::new(
if closest_distance.x.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.x },
if closest_distance.y.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.y },
)
};
self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance);
clamped_closest_distance
}
/// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.)
/// This should be called at the start of a drag.
pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, bounding_boxes: impl Iterator<Item = [DVec2; 2]>, snap_x: bool, snap_y: bool) {
if document_message_handler.snapping_enabled {
self.snap_x = snap_x;
self.snap_y = snap_y;
// Could be made into sorted Vec or a HashSet for more performant lookups.
self.bound_targets = Some(bounding_boxes.flat_map(expand_bounds).collect());
self.point_targets = None;
}
}
/// Add arbitrary snapping points
///
/// This should be called after start_snap
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: impl Iterator<Item = DVec2>) {
if document_message_handler.snapping_enabled {
if let Some(targets) = &mut self.point_targets {
targets.extend(snap_points);
} else {
self.point_targets = Some(snap_points.collect());
}
}
}
/// Add the [ManipulatorGroup]s (optionally including handles) of the specified shape layer to the snapping points
///
/// This should be called after start_snap
pub fn add_snap_path(&mut self, document_message_handler: &DocumentMessageHandler, layer: &Layer, path: &[LayerId], include_handles: bool, ignore_points: &[(&[LayerId], u64, ManipulatorType)]) {
if let LayerDataType::Shape(shape_layer) = &layer.data {
let transform = document_message_handler.graphene_document.multiply_transforms(path).unwrap();
let snap_points = shape_layer
.shape
.manipulator_groups()
.enumerate()
.flat_map(|(id, shape)| {
if include_handles {
[
(*id, &shape.points[ManipulatorType::Anchor]),
(*id, &shape.points[ManipulatorType::InHandle]),
(*id, &shape.points[ManipulatorType::OutHandle]),
]
} else {
[(*id, &shape.points[ManipulatorType::Anchor]), (0, &None), (0, &None)]
}
})
.filter_map(|(id, point)| point.as_ref().map(|val| (id, val)))
.filter(|(id, point)| !ignore_points.contains(&(path, *id, point.manipulator_type)))
.map(|(_id, point)| DVec2::new(point.position.x, point.position.y))
.map(|pos| transform.transform_point2(pos));
self.add_snap_points(document_message_handler, snap_points);
}
}
/// Adds all of the shape handles in the document, including bézier handles of the points specified
pub fn add_all_document_handles(
&mut self,
document_message_handler: &DocumentMessageHandler,
include_handles: &[&[LayerId]],
exclude: &[&[LayerId]],
ignore_points: &[(&[LayerId], u64, ManipulatorType)],
) {
for path in document_message_handler.all_layers() {
if !exclude.contains(&path) {
let layer = document_message_handler.graphene_document.layer(path).expect("Could not get layer for snapping");
self.add_snap_path(document_message_handler, layer, path, include_handles.contains(&path), ignore_points);
}
}
}
/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
pub fn snap_layers(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec<DVec2>, mouse_delta: DVec2) -> DVec2 {
if document_message_handler.snapping_enabled {
self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses)
} else {
DVec2::ZERO
}
}
/// Handles snapping of a viewport position, returning another viewport position.
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
if document_message_handler.snapping_enabled {
self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport
} else {
position_viewport
}
}
/// Removes snap target data and overlays. Call this when snapping is done.
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_overlays.cleanup(responses);
self.bound_targets = None;
self.point_targets = None;
}
}
/// Converts a bounding box into a set of points for snapping
///
/// Puts a point in the middle of each edge (top, bottom, left, right)
pub fn expand_bounds([bound1, bound2]: [DVec2; 2]) -> [DVec2; 4] {
[
DVec2::new((bound1.x + bound2.x) / 2., bound1.y),
DVec2::new((bound1.x + bound2.x) / 2., bound2.y),
DVec2::new(bound1.x, (bound1.y + bound2.y) / 2.),
DVec2::new(bound2.x, (bound1.y + bound2.y) / 2.),
]
}
@@ -0,0 +1,332 @@
use crate::application::generate_uuid;
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_DRAG_ANGLE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::LayerId;
use graphene::Operation;
use glam::{DAffine2, DVec2};
/// Contains the edges that are being dragged along with the original bounds.
#[derive(Clone, Debug, Default)]
pub struct SelectedEdges {
bounds: [DVec2; 2],
top: bool,
bottom: bool,
left: bool,
right: bool,
// Aspect ratio in the form of width/height, so x:1 = width:height
aspect_ratio: f64,
}
impl SelectedEdges {
pub fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self {
let size = (bounds[0] - bounds[1]).abs();
let aspect_ratio = size.x / size.y;
Self {
top,
bottom,
left,
right,
bounds,
aspect_ratio,
}
}
/// Calculate the pivot for the operation (the opposite point to the edge dragged)
pub fn calculate_pivot(&self) -> DVec2 {
let min = self.bounds[0];
let max = self.bounds[1];
let x = if self.left {
max.x
} else if self.right {
min.x
} else {
(min.x + max.x) / 2.
};
let y = if self.top {
max.y
} else if self.bottom {
min.y
} else {
(min.y + max.y) / 2.
};
DVec2::new(x, y)
}
/// Computes the new bounds with the given mouse move and modifier keys
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> (DVec2, DVec2) {
let mouse = transform.inverse().transform_point2(mouse);
let mut min = self.bounds[0];
let mut max = self.bounds[1];
if self.top {
min.y = mouse.y;
} else if self.bottom {
max.y = mouse.y;
}
if self.left {
let delta = min.x - mouse.x;
min.x = mouse.x;
max.x += delta;
} else if self.right {
max.x = mouse.x;
}
let mut size = max - min;
if constrain {
size = match ((self.top || self.bottom), (self.left || self.right)) {
(true, true) => DVec2::new(size.x, size.x / self.aspect_ratio).abs().max(DVec2::new(size.y * self.aspect_ratio, size.y).abs()) * size.signum(),
(true, false) => DVec2::new(size.y * self.aspect_ratio, size.y),
(false, true) => DVec2::new(size.x, size.x / self.aspect_ratio),
_ => size,
};
}
if center {
if self.left || self.right {
size.x *= 2.;
}
if self.bottom || self.top {
size.y *= 2.;
}
}
(min, size)
}
/// Offsets the transformation pivot in order to scale from the center
fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 {
let mut offset = DVec2::ZERO;
if !center {
return offset;
}
if self.right {
offset.x -= size.x / 2.;
}
if self.left {
offset.x += size.x / 2.;
}
if self.bottom {
offset.y -= size.y / 2.;
}
if self.top {
offset.y += size.y / 2.;
}
offset
}
/// Moves the position to account for centering (only necessary with absolute transforms - e.g. with artboards)
pub fn center_position(&self, mut position: DVec2, size: DVec2) -> DVec2 {
if self.right {
position.x -= size.x / 2.;
}
if self.bottom {
position.y -= size.y / 2.;
}
position
}
/// Calculates the required scaling to resize the bounding box
pub fn bounds_to_scale_transform(&self, center: bool, size: DVec2) -> DAffine2 {
DAffine2::from_translation(self.offset_pivot(center, size)) * DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0]))
}
}
/// Create a viewport relative bounding box overlay with no transform handles
pub fn add_bounding_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
path
}
/// Add the transform handle overlay
fn add_transform_handles(responses: &mut VecDeque<Message>) -> [Vec<LayerId>; 8] {
const EMPTY_VEC: Vec<LayerId> = Vec::new();
let mut transform_handle_paths = [EMPTY_VEC; 8];
for item in &mut transform_handle_paths {
let current_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: current_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
*item = current_path;
}
transform_handle_paths
}
/// Converts a bounding box to a rounded transform (with translation and scale)
pub fn transform_from_box(pos1: DVec2, pos2: DVec2, transform: DAffine2) -> DAffine2 {
let inverse = transform.inverse();
transform
* DAffine2::from_scale_angle_translation(
inverse.transform_vector2(transform.transform_vector2(pos2 - pos1).round()),
0.,
inverse.transform_point2(transform.transform_point2(pos1).round() - DVec2::splat(0.5)),
)
}
/// Aligns the mouse position to the closest axis
pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2 {
if axis_align {
let mouse_position = position - start;
let snap_resolution = SELECTION_DRAG_ANGLE.to_radians();
let angle = -mouse_position.angle_between(DVec2::X);
let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length() + start
} else {
position
}
}
/// Contains info on the overlays for the bounding box and transform handles
#[derive(Clone, Debug, Default)]
pub struct BoundingBoxOverlays {
pub bounding_box: Vec<LayerId>,
pub transform_handles: [Vec<LayerId>; 8],
pub bounds: [DVec2; 2],
pub transform: DAffine2,
pub selected_edges: Option<SelectedEdges>,
pub original_transforms: OriginalTransforms,
pub pivot: DVec2,
}
impl BoundingBoxOverlays {
#[must_use]
pub fn new(responses: &mut VecDeque<Message>) -> Self {
Self {
bounding_box: add_bounding_box(responses),
transform_handles: add_transform_handles(responses),
..Default::default()
}
}
/// Calculates the transformed handle positions based on the bounding box and the transform
pub fn evaluate_transform_handle_positions(&self) -> [DVec2; 8] {
let (left, top): (f64, f64) = self.bounds[0].into();
let (right, bottom): (f64, f64) = self.bounds[1].into();
[
self.transform.transform_point2(DVec2::new(left, top)),
self.transform.transform_point2(DVec2::new(left, (top + bottom) / 2.)),
self.transform.transform_point2(DVec2::new(left, bottom)),
self.transform.transform_point2(DVec2::new((left + right) / 2., top)),
self.transform.transform_point2(DVec2::new((left + right) / 2., bottom)),
self.transform.transform_point2(DVec2::new(right, top)),
self.transform.transform_point2(DVec2::new(right, (top + bottom) / 2.)),
self.transform.transform_point2(DVec2::new(right, bottom)),
]
}
/// Update the position of the bounding box and transform handles
pub fn transform(&mut self, responses: &mut VecDeque<Message>) {
let transform = transform_from_box(self.bounds[0], self.bounds[1], self.transform).to_cols_array();
let path = self.bounding_box.clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
const BIAS: f64 = 0.0001;
for (position, path) in self.evaluate_transform_handle_positions().into_iter().zip(&self.transform_handles) {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
let path = path.clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
}
}
/// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right)
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
let cursor = self.transform.inverse().transform_point2(cursor);
let select_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_SELECT_THRESHOLD)).length();
let min = self.bounds[0].min(self.bounds[1]);
let max = self.bounds[0].max(self.bounds[1]);
if min.x - cursor.x < select_threshold && min.y - cursor.y < select_threshold && cursor.x - max.x < select_threshold && cursor.y - max.y < select_threshold {
let mut top = (cursor.y - min.y).abs() < select_threshold;
let mut bottom = (max.y - cursor.y).abs() < select_threshold;
let mut left = (cursor.x - min.x).abs() < select_threshold;
let mut right = (max.x - cursor.x).abs() < select_threshold;
if cursor.y - min.y + max.y - cursor.y < select_threshold * 2. && (left || right) {
top = false;
bottom = false;
}
if cursor.x - min.x + max.x - cursor.x < select_threshold * 2. && (top || bottom) {
left = false;
right = false;
}
if top || bottom || left || right {
return Some((top, bottom, left, right));
}
}
None
}
/// Check if the user is rotating with the bounds
pub fn check_rotate(&self, cursor: DVec2) -> bool {
let cursor = self.transform.inverse().transform_point2(cursor);
let rotate_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_ROTATE_THRESHOLD)).length();
let min = self.bounds[0].min(self.bounds[1]);
let max = self.bounds[0].max(self.bounds[1]);
let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y);
let inside_extended_bounds = min.x - cursor.x < rotate_threshold && min.y - cursor.y < rotate_threshold && cursor.x - max.x < rotate_threshold && cursor.y - max.y < rotate_threshold;
outside_bounds & inside_extended_bounds
}
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
if let Some(directions) = self.check_selected_edges(input.mouse.position) {
match directions {
(true, false, false, false) | (false, true, false, false) => MouseCursorIcon::NSResize,
(false, false, true, false) | (false, false, false, true) => MouseCursorIcon::EWResize,
(true, false, true, false) | (false, true, false, true) => MouseCursorIcon::NWSEResize,
(true, false, false, true) | (false, true, true, false) => MouseCursorIcon::NESWResize,
_ => MouseCursorIcon::Default,
}
} else if rotate && self.check_rotate(input.mouse.position) {
MouseCursorIcon::Grabbing
} else {
MouseCursorIcon::Default
}
}
/// Removes the overlays
pub fn delete(self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()).into());
responses.extend(
self.transform_handles
.iter()
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
);
}
}
+11
View File
@@ -0,0 +1,11 @@
mod tool_message;
mod tool_message_handler;
pub mod common_functionality;
pub mod tool_messages;
pub mod utility_types;
#[doc(inline)]
pub use tool_message::{ToolMessage, ToolMessageDiscriminant};
#[doc(inline)]
pub use tool_message_handler::ToolMessageHandler;
+128
View File
@@ -0,0 +1,128 @@
use super::utility_types::ToolType;
use crate::messages::prelude::*;
use graphene::color::Color;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ToolMessage {
// Sub-messages
#[remain::unsorted]
#[child]
Select(SelectToolMessage),
#[remain::unsorted]
#[child]
Artboard(ArtboardToolMessage),
#[remain::unsorted]
#[child]
Navigate(NavigateToolMessage),
#[remain::unsorted]
#[child]
Eyedropper(EyedropperToolMessage),
#[remain::unsorted]
#[child]
Fill(FillToolMessage),
#[remain::unsorted]
#[child]
Gradient(GradientToolMessage),
#[remain::unsorted]
#[child]
Path(PathToolMessage),
#[remain::unsorted]
#[child]
Pen(PenToolMessage),
#[remain::unsorted]
#[child]
Freehand(FreehandToolMessage),
#[remain::unsorted]
#[child]
Spline(SplineToolMessage),
#[remain::unsorted]
#[child]
Line(LineToolMessage),
#[remain::unsorted]
#[child]
Rectangle(RectangleToolMessage),
#[remain::unsorted]
#[child]
Ellipse(EllipseToolMessage),
#[remain::unsorted]
#[child]
Shape(ShapeToolMessage),
#[remain::unsorted]
#[child]
Text(TextToolMessage),
// #[remain::unsorted]
// #[child]
// Brush(BrushToolMessage),
// #[remain::unsorted]
// #[child]
// Heal(HealToolMessage),
// #[remain::unsorted]
// #[child]
// Clone(CloneToolMessage),
// #[remain::unsorted]
// #[child]
// Patch(PatchToolMessage),
// #[remain::unsorted]
// #[child]
// Relight(RelightToolMessage),
// #[remain::unsorted]
// #[child]
// Detail(DetailToolMessage),
// Messages
#[remain::unsorted]
ActivateToolSelect,
#[remain::unsorted]
ActivateToolArtboard,
#[remain::unsorted]
ActivateToolNavigate,
#[remain::unsorted]
ActivateToolEyedropper,
#[remain::unsorted]
ActivateToolText,
#[remain::unsorted]
ActivateToolFill,
#[remain::unsorted]
ActivateToolGradient,
#[remain::unsorted]
ActivateToolPath,
#[remain::unsorted]
ActivateToolPen,
#[remain::unsorted]
ActivateToolFreehand,
#[remain::unsorted]
ActivateToolSpline,
#[remain::unsorted]
ActivateToolLine,
#[remain::unsorted]
ActivateToolRectangle,
#[remain::unsorted]
ActivateToolEllipse,
#[remain::unsorted]
ActivateToolShape,
ActivateTool {
tool_type: ToolType,
},
DeactivateTools,
InitTools,
ResetColors,
SelectPrimaryColor {
color: Color,
},
SelectRandomPrimaryColor,
SelectSecondaryColor {
color: Color,
},
SwapColors,
UpdateCursor,
UpdateHints,
}
@@ -0,0 +1,221 @@
use super::utility_types::{tool_message_to_tool_type, ToolFsmState};
use crate::application::generate_uuid;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType;
use graphene::color::Color;
use graphene::layers::text_layer::FontCache;
use std::collections::VecDeque;
#[derive(Debug, Default)]
pub struct ToolMessageHandler {
tool_state: ToolFsmState,
}
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache)> for ToolMessageHandler {
#[remain::check]
fn process_message(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) {
let (document, input, font_cache) = data;
#[remain::sorted]
match message {
// Messages
#[remain::unsorted]
ToolMessage::ActivateToolSelect => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Select }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolArtboard => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Artboard }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolNavigate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Navigate }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolEyedropper => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolText => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolFill => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Fill }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolGradient => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Gradient }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolPath => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolPen => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolFreehand => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolSpline => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolLine => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolRectangle => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolEllipse => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()),
ToolMessage::ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data;
let old_tool = tool_data.active_tool_type;
// Do nothing if switching to the same tool
if tool_type == old_tool {
return;
}
// Send the Abort state transition to the tool
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
tool.process_message(tool_abort_message, (document, document_data, input, font_cache), responses);
}
if update_hints_and_cursor {
tool.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses);
tool.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses);
}
}
};
// Send the old and new tools a transition to their FSM Abort states
send_abort_to_tool(tool_type, true);
send_abort_to_tool(old_tool, false);
// Unsubscribe old tool from the broadcaster
tool_data.tools.get(&tool_type).unwrap().deactivate(responses);
// Store the new active tool
tool_data.active_tool_type = tool_type;
// Subscribe new tool
tool_data.tools.get(&tool_type).unwrap().activate(responses);
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated
responses.push_back(BroadcastEvent::SelectionChanged.into());
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
// Send Properties to the frontend
tool_data.tools.get(&tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
// Notify the frontend about the new active tool to be displayed
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
}
ToolMessage::DeactivateTools => {
let tool_data = &mut self.tool_state.tool_data;
tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses);
}
ToolMessage::InitTools => {
let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data;
let active_tool = &tool_data.active_tool_type;
// Subscribe tool to broadcast messages
tool_data.tools.get(active_tool).unwrap().activate(responses);
// Register initial properties
tool_data.tools.get(active_tool).unwrap().register_properties(responses, LayoutTarget::ToolOptions);
// Notify the frontend about the initial active tool
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
// Notify the frontend about the initial working colors
document_data.update_working_colors(responses);
responses.push_back(FrontendMessage::TriggerRefreshBoundsOfViewports.into());
// Set initial hints and cursor
tool_data
.active_tool_mut()
.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses);
tool_data
.active_tool_mut()
.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses);
}
ToolMessage::ResetColors => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = Color::BLACK;
document_data.secondary_color = Color::WHITE;
document_data.update_working_colors(responses);
}
ToolMessage::SelectPrimaryColor { color } => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = color;
self.tool_state.document_tool_data.update_working_colors(responses);
}
ToolMessage::SelectRandomPrimaryColor => {
// Select a random primary color (rgba) based on an UUID
let document_data = &mut self.tool_state.document_tool_data;
let random_number = generate_uuid();
let r = (random_number >> 16) as u8;
let g = (random_number >> 8) as u8;
let b = random_number as u8;
let random_color = Color::from_rgba8(r, g, b, 255);
document_data.primary_color = random_color;
document_data.update_working_colors(responses);
}
ToolMessage::SelectSecondaryColor { color } => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.secondary_color = color;
document_data.update_working_colors(responses);
}
ToolMessage::SwapColors => {
let document_data = &mut self.tool_state.document_tool_data;
std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color);
document_data.update_working_colors(responses);
}
// Sub-messages
#[remain::unsorted]
tool_message => {
let tool_type = match &tool_message {
ToolMessage::UpdateCursor | ToolMessage::UpdateHints => self.tool_state.tool_data.active_tool_type,
tool_message => tool_message_to_tool_type(tool_message),
};
let document_data = &self.tool_state.document_tool_data;
let tool_data = &mut self.tool_state.tool_data;
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
if tool_type == tool_data.active_tool_type {
tool.process_message(tool_message, (document, document_data, input, font_cache), responses);
}
}
}
}
}
fn actions(&self) -> ActionList {
let mut list = actions!(ToolMessageDiscriminant;
ActivateToolSelect,
ActivateToolArtboard,
ActivateToolNavigate,
ActivateToolEyedropper,
ActivateToolText,
ActivateToolFill,
ActivateToolGradient,
ActivateToolPath,
ActivateToolPen,
ActivateToolFreehand,
ActivateToolSpline,
ActivateToolLine,
ActivateToolRectangle,
ActivateToolEllipse,
ActivateToolShape,
SelectRandomPrimaryColor,
ResetColors,
SwapColors,
);
list.extend(self.tool_state.tool_data.active_tool().actions());
list
}
}
@@ -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()
}

Some files were not shown because too many files have changed in this diff Show More