mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 18:48:11 +08:00
Restructure the entire editor codebase to consistently match the message hierarchy
Closes #744
This commit is contained in:
@@ -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![]
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
+158
@@ -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 },
|
||||
}
|
||||
@@ -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<_>>()
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
+166
@@ -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,
|
||||
}
|
||||
+186
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user