Integrate Stable Diffusion with the Imaginate layer (#784)

* Add AI Artist layer

* WIP add a button to download the rendered folder under an AI Artist layer

* Successfully download the correct image

* Break out image downloading JS into helper function

* Change file download from using data URLs to blob URLs

* WIP rasterize to blob

* Remove dimensions from AI Artist layer

* Successfully draw rasterized image on layer after calculation

* Working txt2img generation based on user prompt

* Add img2img and the main parameters

* Fix ability to rasterize multi-depth documents with blob URL images by switching them to base64

* Fix test

* Rasterize with artboard background color

* Allow aspect ratio stretch of AI Artist images

* Add automatic resolution choosing

* Add a terminate button, and make the lifecycle more robust

* Add negative prompt

* Add range bounds for parameter inputs

* Add seed

* Add tiling and restore faces

* Add server status check, server hostname customization, and resizing layer to fit AI Artist resolution

* Fix background color of infinite canvas rasterization

* Escape prompt text sent in the JSON

* Revoke blob URLs when cleared/replaced to reduce memory leak

* Fix welcome screen logo color

* Add PreferencesMessageHandler

* Add persistent storage of preferences

* Fix crash introduced in previous commit when moving mouse on page load

* Add tooltips to the AI Artist layer properties

* Integrate AI Artist tool into the raster section of the tool shelf

* Add a refresh button to the connection status

* Fix crash when generating and switching to a different document tab

* Add persistent image storage to AI Artist layers and fix duplication bugs

* Add a generate with random seed button

* Simplify and standardize message names

* Majorly improve robustness of networking code

* Fix race condition causing default server hostname to show disconnected when app loads with AI Artist layer selected (probably, not confirmed fixed)

* Clean up messages and function calls by changing arguments into structs

* Update API to more recent server commit

* Add support for picking the sampling method

* Add machinery for filtering selected layers with type

* Replace placeholder button icons

* Improve the random icon by tilting the dice

* Use selected_layers() instead of repeating that code

* Fix borrow error

* Change message flow in progress towards fixing #797

* Allow loading image on non-active document (fixes #797)

* Reduce code duplication with rasterization

* Add AI Artist tool and layer icons, and remove ugly node layer icon style

* Rename "AI Artist" codename to "Imaginate" feature name

Co-authored-by: otdavies <oliver@psyfer.io>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2022-10-18 22:33:27 -07:00
committed by GitHub
co-authored by otdavies 0hypercube
parent 06acd45a81
commit 30719bdc72
118 changed files with 3767 additions and 678 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ fn main() {
let try_git_command = |args: &[&str]| -> Option<String> { let try_git_command = |args: &[&str]| -> Option<String> {
let git_output = Command::new("git").args(args).output().ok()?; let git_output = Command::new("git").args(args).output().ok()?;
let maybe_empty = String::from_utf8(git_output.stdout).ok()?; let maybe_empty = String::from_utf8(git_output.stdout).ok()?;
let command_result = (!maybe_empty.is_empty()).then(|| maybe_empty)?; let command_result = (!maybe_empty.is_empty()).then_some(maybe_empty)?;
Some(command_result) Some(command_result)
}; };
// Execute a Git command for its output. Return "unknown" if it fails for any of the possible reasons. // Execute a Git command for its output. Return "unknown" if it fails for any of the possible reasons.
+22 -11
View File
@@ -22,6 +22,7 @@ struct DispatcherMessageHandlers {
input_preprocessor_message_handler: InputPreprocessorMessageHandler, input_preprocessor_message_handler: InputPreprocessorMessageHandler,
layout_message_handler: LayoutMessageHandler, layout_message_handler: LayoutMessageHandler,
portfolio_message_handler: PortfolioMessageHandler, portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler, tool_message_handler: ToolMessageHandler,
workspace_message_handler: WorkspaceMessageHandler, workspace_message_handler: WorkspaceMessageHandler,
} }
@@ -92,14 +93,16 @@ impl Dispatcher {
NoOp => {} NoOp => {}
#[remain::unsorted] #[remain::unsorted]
Init => { Init => {
// Load persistent data from the browser database
queue.push_back(FrontendMessage::TriggerLoadAutoSaveDocuments.into());
queue.push_back(FrontendMessage::TriggerLoadPreferences.into());
// Display the menu bar at the top of the window // Display the menu bar at the top of the window
let message = MenuBarMessage::SendLayout.into(); queue.push_back(MenuBarMessage::SendLayout.into());
queue.push_back(message);
// Load the default font // Load the default font
let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into()); let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into());
let message = FrontendMessage::TriggerFontLoad { font, is_default: true }.into(); queue.push_back(FrontendMessage::TriggerFontLoad { font, is_default: true }.into());
queue.push_back(message);
} }
Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, (), &mut queue), Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, (), &mut queue),
@@ -107,9 +110,11 @@ impl Dispatcher {
self.message_handlers.debug_message_handler.process_message(message, (), &mut queue); self.message_handlers.debug_message_handler.process_message(message, (), &mut queue);
} }
Dialog(message) => { Dialog(message) => {
self.message_handlers self.message_handlers.dialog_message_handler.process_message(
.dialog_message_handler message,
.process_message(message, &self.message_handlers.portfolio_message_handler, &mut queue); (&self.message_handlers.portfolio_message_handler, &self.message_handlers.preferences_message_handler),
&mut queue,
);
} }
Frontend(message) => { Frontend(message) => {
// Handle these messages immediately by returning early // Handle these messages immediately by returning early
@@ -145,9 +150,14 @@ impl Dispatcher {
self.message_handlers.layout_message_handler.process_message(message, action_input_mapping, &mut queue); self.message_handlers.layout_message_handler.process_message(message, action_input_mapping, &mut queue);
} }
Portfolio(message) => { Portfolio(message) => {
self.message_handlers self.message_handlers.portfolio_message_handler.process_message(
.portfolio_message_handler message,
.process_message(message, &self.message_handlers.input_preprocessor_message_handler, &mut queue); (&self.message_handlers.input_preprocessor_message_handler, &self.message_handlers.preferences_message_handler),
&mut queue,
);
}
Preferences(message) => {
self.message_handlers.preferences_message_handler.process_message(message, (), &mut queue);
} }
Tool(message) => { Tool(message) => {
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() { if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() {
@@ -155,8 +165,9 @@ impl Dispatcher {
message, message,
( (
document, document,
self.message_handlers.portfolio_message_handler.active_document_id().unwrap(),
&self.message_handlers.input_preprocessor_message_handler, &self.message_handlers.input_preprocessor_message_handler,
self.message_handlers.portfolio_message_handler.font_cache(), &self.message_handlers.portfolio_message_handler.persistent_data,
), ),
&mut queue, &mut queue,
); );
-2
View File
@@ -3,8 +3,6 @@ extern crate graphite_proc_macros;
// `macro_use` puts these macros into scope for all descendant code files // `macro_use` puts these macros into scope for all descendant code files
#[macro_use] #[macro_use]
mod macros; mod macros;
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@@ -13,6 +13,9 @@ pub enum DialogMessage {
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
NewDocumentDialog(NewDocumentDialogMessage), NewDocumentDialog(NewDocumentDialogMessage),
#[remain::unsorted]
#[child]
PreferencesDialog(PreferencesDialogMessage),
// Messages // Messages
CloseAllDocumentsWithConfirmation, CloseAllDocumentsWithConfirmation,
@@ -32,4 +35,5 @@ pub enum DialogMessage {
}, },
RequestExportDialog, RequestExportDialog,
RequestNewDocumentDialog, RequestNewDocumentDialog,
RequestPreferencesDialog,
} }
@@ -7,17 +7,20 @@ use crate::messages::prelude::*;
pub struct DialogMessageHandler { pub struct DialogMessageHandler {
export_dialog: ExportDialogMessageHandler, export_dialog: ExportDialogMessageHandler,
new_document_dialog: NewDocumentDialogMessageHandler, new_document_dialog: NewDocumentDialogMessageHandler,
preferences_dialog: PreferencesDialogMessageHandler,
} }
impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHandler { impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessageHandler)> for DialogMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: DialogMessage, portfolio: &PortfolioMessageHandler, responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: DialogMessage, (portfolio, preferences): (&PortfolioMessageHandler, &PreferencesMessageHandler), responses: &mut VecDeque<Message>) {
#[remain::sorted] #[remain::sorted]
match message { match message {
#[remain::unsorted] #[remain::unsorted]
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, (), responses), DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, (), responses),
#[remain::unsorted] #[remain::unsorted]
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, (), responses), DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, (), responses),
#[remain::unsorted]
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, preferences, responses),
DialogMessage::CloseAllDocumentsWithConfirmation => { DialogMessage::CloseAllDocumentsWithConfirmation => {
let dialog = simple_dialogs::CloseAllDocumentsDialog; let dialog = simple_dialogs::CloseAllDocumentsDialog;
@@ -97,12 +100,18 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails); self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into()); responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
} }
DialogMessage::RequestPreferencesDialog => {
self.preferences_dialog = PreferencesDialogMessageHandler {};
self.preferences_dialog.register_properties(responses, LayoutTarget::DialogDetails, preferences);
responses.push_back(FrontendMessage::DisplayDialog { icon: "Settings".to_string() }.into());
}
} }
} }
advertise_actions!(DialogMessageDiscriminant; advertise_actions!(DialogMessageDiscriminant;
RequestNewDocumentDialog,
RequestExportDialog,
CloseAllDocumentsWithConfirmation, CloseAllDocumentsWithConfirmation,
RequestExportDialog,
RequestNewDocumentDialog,
RequestPreferencesDialog,
); );
} }
+1
View File
@@ -10,6 +10,7 @@ mod dialog_message_handler;
pub mod export_dialog; pub mod export_dialog;
pub mod new_document_dialog; pub mod new_document_dialog;
pub mod preferences_dialog;
pub mod simple_dialogs; pub mod simple_dialogs;
#[doc(inline)] #[doc(inline)]
@@ -83,7 +83,6 @@ impl PropertyHolder for NewDocumentDialogMessageHandler {
})), })),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: self.infinite, checked: self.infinite,
icon: "Checkmark".to_string(),
on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()), on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()),
..Default::default() ..Default::default()
})), })),
@@ -0,0 +1,7 @@
mod preferences_dialog_message;
mod preferences_dialog_message_handler;
#[doc(inline)]
pub use preferences_dialog_message::{PreferencesDialogMessage, PreferencesDialogMessageDiscriminant};
#[doc(inline)]
pub use preferences_dialog_message_handler::PreferencesDialogMessageHandler;
@@ -0,0 +1,9 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[impl_message(Message, DialogMessage, PreferencesDialog)]
#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PreferencesDialogMessage {
Confirm,
}
@@ -0,0 +1,115 @@
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, 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::{NumberInput, TextInput};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::*;
/// A dialog to allow users to customize Graphite editor options
#[derive(Debug, Clone, Default)]
pub struct PreferencesDialogMessageHandler {}
impl MessageHandler<PreferencesDialogMessage, &PreferencesMessageHandler> for PreferencesDialogMessageHandler {
fn process_message(&mut self, message: PreferencesDialogMessage, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) {
match message {
PreferencesDialogMessage::Confirm => {}
}
self.register_properties(responses, LayoutTarget::DialogDetails, preferences);
}
advertise_actions! {PreferencesDialogUpdate;}
}
impl PreferencesDialogMessageHandler {
pub fn register_properties(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
responses.push_back(
LayoutMessage::SendLayout {
layout: self.properties(preferences),
layout_target,
}
.into(),
)
}
fn properties(&self, preferences: &PreferencesMessageHandler) -> Layout {
let imaginate_server_hostname = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Imaginate".into(),
min_width: 60,
italic: true,
..Default::default()
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Server Hostname".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextInput(TextInput {
value: preferences.imaginate_server_hostname.clone(),
min_width: 200,
on_update: WidgetCallback::new(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into()),
..Default::default()
})),
];
let imaginate_refresh_frequency = vec![
WidgetHolder::new(Widget::TextLabel(TextLabel { min_width: 60, ..Default::default() })),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Refresh Frequency".into(),
table_align: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
unit: " seconds".into(),
value: Some(preferences.imaginate_refresh_frequency),
min: Some(0.),
min_width: 200,
on_update: WidgetCallback::new(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into()),
..Default::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![PreferencesDialogMessage::Confirm.into()],
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Reset to Defaults".to_string(),
min_width: 96,
on_update: WidgetCallback::new(|_| PreferencesMessage::ResetToDefaults.into()),
..Default::default()
})),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Editor Preferences".to_string(),
bold: true,
..Default::default()
}))],
},
LayoutGroup::Row { widgets: imaginate_server_hostname },
LayoutGroup::Row { widgets: imaginate_refresh_frequency },
LayoutGroup::Row { widgets: button_widgets },
]))
}
}
@@ -7,7 +7,9 @@ use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData; use crate::messages::tool::utility_types::HintData;
use graphene::color::Color; use graphene::color::Color;
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters};
use graphene::layers::text_layer::Font; use graphene::layers::text_layer::Font;
use graphene::LayerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -50,6 +52,28 @@ pub enum FrontendMessage {
#[serde(rename = "isDefault")] #[serde(rename = "isDefault")]
is_default: bool, is_default: bool,
}, },
TriggerImaginateCheckServerStatus {
hostname: String,
},
TriggerImaginateGenerate {
parameters: ImaginateGenerationParameters,
#[serde(rename = "baseImage")]
base_image: Option<ImaginateBaseImage>,
hostname: String,
#[serde(rename = "refreshFrequency")]
refresh_frequency: f64,
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "layerPath")]
layer_path: Vec<LayerId>,
},
TriggerImaginateTerminate {
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "layerPath")]
layer_path: Vec<LayerId>,
hostname: String,
},
TriggerImport, TriggerImport,
TriggerIndexedDbRemoveDocument { TriggerIndexedDbRemoveDocument {
#[serde(rename = "documentId")] #[serde(rename = "documentId")]
@@ -60,6 +84,8 @@ pub enum FrontendMessage {
details: FrontendDocumentDetails, details: FrontendDocumentDetails,
version: String, version: String,
}, },
TriggerLoadAutoSaveDocuments,
TriggerLoadPreferences,
TriggerOpenDocument, TriggerOpenDocument,
TriggerPaste, TriggerPaste,
TriggerRasterDownload { TriggerRasterDownload {
@@ -69,6 +95,12 @@ pub enum FrontendMessage {
size: (f64, f64), size: (f64, f64),
}, },
TriggerRefreshBoundsOfViewports, TriggerRefreshBoundsOfViewports,
TriggerRevokeBlobUrl {
url: String,
},
TriggerSavePreferences {
preferences: PreferencesMessageHandler,
},
TriggerTextCommit, TriggerTextCommit,
TriggerTextCopy { TriggerTextCopy {
#[serde(rename = "copyText")] #[serde(rename = "copyText")]
@@ -126,6 +158,8 @@ pub enum FrontendMessage {
multiplier: (f64, f64), multiplier: (f64, f64),
}, },
UpdateImageData { UpdateImageData {
#[serde(rename = "documentId")]
document_id: u64,
#[serde(rename = "imageData")] #[serde(rename = "imageData")]
image_data: Vec<FrontendImageData>, image_data: Vec<FrontendImageData>,
}, },
@@ -1,4 +1,4 @@
use crate::messages::portfolio::document::utility_types::misc::Platform; use crate::messages::portfolio::utility_types::Platform;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -1,4 +1,4 @@
use crate::messages::portfolio::document::utility_types::misc::Platform; use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -8,7 +8,9 @@ impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
fn process_message(&mut self, message: GlobalsMessage, _data: (), _responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: GlobalsMessage, _data: (), _responses: &mut VecDeque<Message>) {
match message { match message {
GlobalsMessage::SetPlatform { platform } => { GlobalsMessage::SetPlatform { platform } => {
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM"); if GLOBAL_PLATFORM.get() != Some(&platform) {
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
}
} }
} }
} }
@@ -112,6 +112,13 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::Resize { center: Alt, lock_ratio: Shift }), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::Resize { center: Alt, lock_ratio: Shift }),
// //
// ImaginateToolMessage
entry!(KeyDown(Lmb); action_dispatch=ImaginateToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=ImaginateToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=ImaginateToolMessage::Abort),
entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }),
//
// EllipseToolMessage // EllipseToolMessage
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart), entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop), entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
@@ -263,9 +270,10 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste), entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
// //
// DialogMessage // DialogMessage
entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog),
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation), entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog), entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog),
entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog),
entry!(KeyDown(Comma); modifiers=[Accel], action_dispatch=DialogMessage::RequestPreferencesDialog),
// //
// DebugMessage // DebugMessage
entry!(KeyDown(KeyT); modifiers=[Alt], action_dispatch=DebugMessage::ToggleTraceLogs), entry!(KeyDown(KeyT); modifiers=[Alt], action_dispatch=DebugMessage::ToggleTraceLogs),
@@ -11,9 +11,7 @@ pub struct InputMapperMessageHandler {
} }
impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, ActionList)> for InputMapperMessageHandler { impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, ActionList)> for InputMapperMessageHandler {
fn process_message(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: InputMapperMessage, (input, actions): (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque<Message>) {
let (input, actions) = data;
if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions) { if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions) {
responses.push_back(message); responses.push_back(message);
} }
@@ -1,4 +1,4 @@
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout; use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*; use crate::messages::prelude::*;
pub use graphene::DocumentResponse; pub use graphene::DocumentResponse;
@@ -1,6 +1,6 @@
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys}; 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::input_mapper::utility_types::input_mouse::{MouseKeys, MouseState, ViewportBounds};
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout; use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*; use crate::messages::prelude::*;
pub use graphene::DocumentResponse; pub use graphene::DocumentResponse;
@@ -183,7 +183,7 @@ impl InputPreprocessorMessageHandler {
mod test { mod test {
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; 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::utility_types::input_mouse::EditorMouseState;
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout; use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*; use crate::messages::prelude::*;
#[test] #[test]
@@ -63,7 +63,20 @@ impl Layout {
Widget::ColorInput(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::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
_ => None, Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
| Widget::InvisibleStandinInput(_)
| Widget::PivotAssist(_)
| Widget::RadioInput(_)
| Widget::Separator(_)
| Widget::SwatchPairInput(_)
| Widget::TextAreaInput(_)
| Widget::TextInput(_)
| Widget::TextLabel(_) => None,
}; };
if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut { if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut {
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip); apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
@@ -1,9 +1,9 @@
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use derivative::*; use derivative::*;
use glam::DVec2; use glam::DVec2;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct PivotAssist { pub struct PivotAssist {
@@ -33,6 +33,11 @@ pub struct PopoverButton {
pub header: String, pub header: String,
pub text: String, pub text: String,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Default)]
@@ -50,6 +55,11 @@ pub struct TextButton {
pub disabled: bool, pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -6,7 +6,7 @@ use graphene::color::Color;
use derivative::*; use derivative::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct CheckboxInput { pub struct CheckboxInput {
pub checked: bool, pub checked: bool,
@@ -24,6 +24,18 @@ pub struct CheckboxInput {
pub on_update: WidgetCallback<CheckboxInput>, pub on_update: WidgetCallback<CheckboxInput>,
} }
impl Default for CheckboxInput {
fn default() -> Self {
Self {
checked: false,
icon: "Checkmark".into(),
tooltip: Default::default(),
tooltip_shortcut: Default::default(),
on_update: Default::default(),
}
}
}
#[derive(Clone, Derivative, Serialize, Deserialize)] #[derive(Clone, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct ColorInput { pub struct ColorInput {
@@ -64,6 +76,11 @@ pub struct DropdownInput {
pub interactive: bool, pub interactive: bool,
pub disabled: bool, pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// //
// Callbacks // Callbacks
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput` // `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
@@ -109,6 +126,11 @@ pub struct FontInput {
pub disabled: bool, pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -159,11 +181,15 @@ pub struct NumberInput {
pub disabled: bool, pub disabled: bool,
// Callbacks #[serde(rename = "minWidth")]
#[serde(skip)] pub min_width: u32,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<NumberInput>,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_increase: WidgetCallback<NumberInput>, pub increment_callback_increase: WidgetCallback<NumberInput>,
@@ -171,6 +197,10 @@ pub struct NumberInput {
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
pub increment_callback_decrease: WidgetCallback<NumberInput>, pub increment_callback_decrease: WidgetCallback<NumberInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<NumberInput>,
} }
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] #[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
@@ -246,6 +276,8 @@ pub struct TextAreaInput {
pub disabled: bool, pub disabled: bool,
pub tooltip: String,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -261,6 +293,11 @@ pub struct TextInput {
pub disabled: bool, pub disabled: bool,
pub tooltip: String,
#[serde(rename = "minWidth")]
pub min_width: u32,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -5,15 +5,7 @@ use serde::{Deserialize, Serialize};
pub struct IconLabel { pub struct IconLabel {
pub icon: String, pub icon: String,
#[serde(rename = "iconStyle")] pub tooltip: String,
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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -49,6 +41,11 @@ pub struct TextLabel {
pub multiline: bool, pub multiline: bool,
#[serde(rename = "minWidth")]
pub min_width: u32,
pub tooltip: String,
// Body // Body
pub value: String, pub value: String,
} }
+2
View File
@@ -34,6 +34,8 @@ pub enum Message {
#[child] #[child]
Portfolio(PortfolioMessage), Portfolio(PortfolioMessage),
#[child] #[child]
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage), Tool(ToolMessage),
#[child] #[child]
Workspace(WorkspaceMessage), Workspace(WorkspaceMessage),
+1
View File
@@ -10,6 +10,7 @@ pub mod input_preprocessor;
pub mod layout; pub mod layout;
pub mod message; pub mod message;
pub mod portfolio; pub mod portfolio;
pub mod preferences;
pub mod prelude; pub mod prelude;
pub mod tool; pub mod tool;
pub mod workspace; pub mod workspace;
@@ -85,7 +85,7 @@ impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
.into(), .into(),
) )
} else { } else {
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false); let render_data = RenderData::new(ViewMode::Normal, font_cache, None);
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentArtboards { FrontendMessage::UpdateDocumentArtboards {
svg: self.artboards_graphene_document.render_root(render_data), svg: self.artboards_graphene_document.render_root(render_data),
@@ -8,7 +8,6 @@ use graphene::layers::blend_mode::BlendMode;
use graphene::layers::style::ViewMode; use graphene::layers::style::ViewMode;
use graphene::LayerId; use graphene::LayerId;
use graphene::Operation as DocumentOperation; use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[remain::sorted] #[remain::sorted]
@@ -75,6 +74,9 @@ pub enum DocumentMessage {
affected_folder_path: Vec<LayerId>, affected_folder_path: Vec<LayerId>,
}, },
GroupSelectedLayers, GroupSelectedLayers,
ImaginateClear,
ImaginateGenerate,
ImaginateTerminate,
LayerChanged { LayerChanged {
affected_layer_path: Vec<LayerId>, affected_layer_path: Vec<LayerId>,
}, },
@@ -120,6 +122,12 @@ pub enum DocumentMessage {
SetBlendModeForSelectedLayers { SetBlendModeForSelectedLayers {
blend_mode: BlendMode, blend_mode: BlendMode,
}, },
SetImageBlobUrl {
layer_path: Vec<LayerId>,
blob_url: String,
resolution: (f64, f64),
document_id: u64,
},
SetLayerExpansion { SetLayerExpansion {
layer_path: Vec<LayerId>, layer_path: Vec<LayerId>,
set_expanded: bool, set_expanded: bool,
@@ -140,7 +148,7 @@ pub enum DocumentMessage {
SetSnapping { SetSnapping {
snap: bool, snap: bool,
}, },
SetTexboxEditability { SetTextboxEditability {
path: Vec<LayerId>, path: Vec<LayerId>,
editable: bool, editable: bool,
}, },
@@ -1,6 +1,8 @@
use super::utility_types::error::EditorError; use super::utility_types::error::EditorError;
use super::utility_types::misc::DocumentRenderMode;
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR}; use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
use crate::messages::frontend::utility_types::ExportBounds;
use crate::messages::frontend::utility_types::{FileType, FrontendImageData}; use crate::messages::frontend::utility_types::{FileType, FrontendImageData};
use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
@@ -14,12 +16,14 @@ use crate::messages::portfolio::document::utility_types::layer_panel::{LayerMeta
use crate::messages::portfolio::document::utility_types::misc::DocumentMode; use crate::messages::portfolio::document::utility_types::misc::DocumentMode;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata; use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::color::Color; use graphene::color::Color;
use graphene::document::Document as GrapheneDocument; use graphene::document::{pick_layer_safe_imaginate_resolution, Document as GrapheneDocument};
use graphene::layers::blend_mode::BlendMode; use graphene::layers::blend_mode::BlendMode;
use graphene::layers::folder_layer::FolderLayer; use graphene::layers::folder_layer::FolderLayer;
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateStatus};
use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, RenderData, ViewMode}; use graphene::layers::style::{Fill, RenderData, ViewMode};
use graphene::layers::text_layer::{Font, FontCache}; use graphene::layers::text_layer::{Font, FontCache};
@@ -87,16 +91,21 @@ impl Default for DocumentMessageHandler {
} }
} }
impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCache)> for DocumentMessageHandler { impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler)> for DocumentMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: DocumentMessage, (ipp, font_cache): (&InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) { fn process_message(
&mut self,
message: DocumentMessage,
(document_id, ipp, persistent_data, preferences): (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler),
responses: &mut VecDeque<Message>,
) {
use DocumentMessage::*; use DocumentMessage::*;
#[remain::sorted] #[remain::sorted]
match message { match message {
// Sub-messages // Sub-messages
#[remain::unsorted] #[remain::unsorted]
DispatchOperation(op) => match self.graphene_document.handle_operation(*op, font_cache) { DispatchOperation(op) => match self.graphene_document.handle_operation(*op, &persistent_data.font_cache) {
Ok(Some(document_responses)) => { Ok(Some(document_responses)) => {
for response in document_responses { for response in document_responses {
match &response { match &response {
@@ -130,7 +139,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
}, },
#[remain::unsorted] #[remain::unsorted]
Artboard(message) => { Artboard(message) => {
self.artboard_message_handler.process_message(message, font_cache, responses); self.artboard_message_handler.process_message(message, &persistent_data.font_cache, responses);
} }
#[remain::unsorted] #[remain::unsorted]
Navigation(message) => { Navigation(message) => {
@@ -138,25 +147,23 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
#[remain::unsorted] #[remain::unsorted]
Overlays(message) => { Overlays(message) => {
self.overlays_message_handler.process_message(message, (self.overlays_visible, font_cache, ipp), responses); self.overlays_message_handler
.process_message(message, (self.overlays_visible, &persistent_data.font_cache, ipp), responses);
} }
#[remain::unsorted] #[remain::unsorted]
TransformLayer(message) => { TransformLayer(message) => {
self.transform_layer_handler self.transform_layer_handler
.process_message(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp, font_cache), responses); .process_message(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp, &persistent_data.font_cache), responses);
} }
#[remain::unsorted] #[remain::unsorted]
PropertiesPanel(message) => { PropertiesPanel(message) => {
self.properties_panel_message_handler.process_message( let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
message, artwork_document: &self.graphene_document,
PropertiesPanelMessageHandlerData { artboard_document: &self.artboard_message_handler.artboards_graphene_document,
artwork_document: &self.graphene_document, selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())),
artboard_document: &self.artboard_message_handler.artboards_graphene_document, };
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())), self.properties_panel_message_handler
font_cache, .process_message(message, (persistent_data, properties_panel_message_handler_data), responses);
},
responses,
);
} }
// Messages // Messages
@@ -166,20 +173,20 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
AddSelectedLayers { additional_layers } => { AddSelectedLayers { additional_layers } => {
for layer_path in &additional_layers { for layer_path in &additional_layers {
responses.extend(self.select_layer(layer_path, font_cache)); responses.extend(self.select_layer(layer_path, &persistent_data.font_cache));
} }
// TODO: Correctly update layer panel in clear_selection instead of here // TODO: Correctly update layer panel in clear_selection instead of here
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into()); responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
responses.push_back(BroadcastEvent::SelectionChanged.into()); responses.push_back(BroadcastEvent::SelectionChanged.into());
self.update_layer_tree_options_bar_widgets(responses, font_cache); self.update_layer_tree_options_bar_widgets(responses, &persistent_data.font_cache);
} }
AlignSelectedLayers { axis, aggregate } => { AlignSelectedLayers { axis, aggregate } => {
self.backup(responses); self.backup(responses);
let (paths, boxes): (Vec<_>, Vec<_>) = self let (paths, boxes): (Vec<_>, Vec<_>) = self
.selected_layers() .selected_layers()
.filter_map(|path| self.graphene_document.viewport_bounding_box(path, font_cache).ok()?.map(|b| (path, b))) .filter_map(|path| self.graphene_document.viewport_bounding_box(path, &persistent_data.font_cache).ok()?.map(|b| (path, b)))
.unzip(); .unzip();
let axis = match axis { let axis = match axis {
@@ -187,7 +194,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
AlignAxis::Y => DVec2::Y, AlignAxis::Y => DVec2::Y,
}; };
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), font_cache) { if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), &persistent_data.font_cache) {
let aggregated = match aggregate { let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0], AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1], AlignAggregate::Max => combined_box[1],
@@ -308,21 +315,21 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
scale_factor, scale_factor,
bounds, bounds,
} => { } => {
// Allows the user's transform to be restored
let old_transform = self.graphene_document.root.transform;
// Reset the root's transform (required to avoid any rotation by the user)
self.graphene_document.root.transform = DAffine2::IDENTITY;
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
// Calculate the bounding box of the region to be exported // Calculate the bounding box of the region to be exported
use crate::messages::frontend::utility_types::ExportBounds; let bounds = match bounds {
let bbox = match bounds { ExportBounds::AllArtwork => self.all_layer_bounds(&persistent_data.font_cache),
ExportBounds::AllArtwork => self.all_layer_bounds(font_cache), ExportBounds::Selection => self.selected_visible_layers_bounding_box(&persistent_data.font_cache),
ExportBounds::Selection => self.selected_visible_layers_bounding_box(font_cache), ExportBounds::Artboard(id) => self
ExportBounds::Artboard(id) => self.artboard_message_handler.artboards_graphene_document.layer(&[id]).ok().and_then(|layer| layer.aabb(font_cache)), .artboard_message_handler
.artboards_graphene_document
.layer(&[id])
.ok()
.and_then(|layer| layer.aabb(&persistent_data.font_cache)),
} }
.unwrap_or_default(); .unwrap_or_default();
let size = bbox[1] - bbox[0]; let size = bounds[1] - bounds[0];
let document = self.render_document(bounds, persistent_data, DocumentRenderMode::Root);
let file_suffix = &format!(".{file_type:?}").to_lowercase(); let file_suffix = &format!(".{file_type:?}").to_lowercase();
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) { let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
@@ -330,16 +337,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
false => file_name + file_suffix, false => file_name + file_suffix,
}; };
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, true);
let rendered = self.graphene_document.render_root(render_data);
let document = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}">{}{}</svg>"#,
bbox[0].x, bbox[0].y, size.x, size.y, size.x, size.y, "\n", rendered
);
self.graphene_document.root.transform = old_transform;
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
if file_type == FileType::Svg { if file_type == FileType::Svg {
responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into()); responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into());
} else { } else {
@@ -354,7 +351,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.), FlipAxis::Y => DVec2::new(1., -1.),
}; };
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), font_cache) { if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), &persistent_data.font_cache) {
let center = (max + min) / 2.; let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center); let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() { for path in self.selected_layers() {
@@ -403,12 +400,73 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
.into(), .into(),
); );
} }
ImaginateClear => {
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
// Get what is hopefully the only selected Imaginate layer
let layer_path = selected_imaginate_layers.next();
// Abort if we didn't have any Imaginate layer, or if there are additional ones also selected
if layer_path.is_none() || selected_imaginate_layers.next().is_some() {
return;
}
let layer_path = layer_path.unwrap();
let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate image for invalid layer");
let previous_blob_url = &layer.as_imaginate().unwrap().blob_url;
if let Some(url) = previous_blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
responses.push_back(DocumentOperation::ImaginateClear { path: layer_path.into() }.into());
}
ImaginateGenerate => {
if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) {
// TODO: Eventually remove this after a message system ordering architectural change
// This message is a workaround for the fact that, when `imaginate.ts` calls...
// `editor.instance.setImaginateGeneratingStatus(layerPath, 0, true);`
// ...execution transfers from the Rust part of the call stack into the JS part of the call stack (before the Rust message queue is empty,
// and there is a Properties panel refresh queued next). Then the JS calls that line shown above and enters the Rust part of the callstack
// again, so it's gone through JS (user initiation) -> Rust (process the button press) -> JS (beginning server request) -> Rust (set
// progress percentage to 0). As that call stack returns back from the Rust and back from the JS, it returns to the Rust and finishes
// processing the queue. That's where it then processes the Properties panel refresh that sent the "Ready" or "Done" state that existed
// before pressing the Generate button causing it to show "0%". So "Ready" or "Done" immediately overwrites the "0%". This block below,
// therefore, adds a redundant call to set it to 0% progress so the message execution order ends with this as the final percentage shown
// to the user.
responses.push_back(
DocumentOperation::ImaginateSetGeneratingStatus {
path: self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate).next().unwrap().to_vec(),
percent: Some(0.),
status: ImaginateStatus::Beginning,
}
.into(),
);
responses.push_back(message);
}
}
ImaginateTerminate => {
let hostname = preferences.imaginate_server_hostname.clone();
let layer_path = {
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
// Get what is hopefully the only selected Imaginate layer
match selected_imaginate_layers.next() {
// Continue only if there are no additional Imaginate layers also selected
Some(layer_path) if selected_imaginate_layers.next().is_none() => Some(layer_path.to_owned()),
_ => None,
}
};
if let Some(layer_path) = layer_path {
responses.push_back(FrontendMessage::TriggerImaginateTerminate { document_id, layer_path, hostname }.into());
}
}
LayerChanged { affected_layer_path } => { LayerChanged { affected_layer_path } => {
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), font_cache) { if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), &persistent_data.font_cache) {
responses.push_back(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }.into()); responses.push_back(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }.into());
} }
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into()); responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
self.update_layer_tree_options_bar_widgets(responses, font_cache); self.update_layer_tree_options_bar_widgets(responses, &persistent_data.font_cache);
} }
MoveSelectedLayersTo { MoveSelectedLayersTo {
folder_path, folder_path,
@@ -466,6 +524,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
); );
responses.push_back( responses.push_back(
FrontendMessage::UpdateImageData { FrontendMessage::UpdateImageData {
document_id,
image_data: vec![FrontendImageData { path: path.clone(), image_data, mime }], image_data: vec![FrontendImageData { path: path.clone(), image_data, mime }],
} }
.into(), .into(),
@@ -490,7 +549,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
RenameLayer { layer_path, new_name } => responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()), RenameLayer { layer_path, new_name } => responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()),
RenderDocument => { RenderDocument => {
let render_data = RenderData::new(self.view_mode, font_cache, Some(ipp.document_bounds()), false); let render_data = RenderData::new(self.view_mode, &persistent_data.font_cache, Some(ipp.document_bounds()));
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentArtwork { FrontendMessage::UpdateDocumentArtwork {
svg: self.graphene_document.render_root(render_data), svg: self.graphene_document.render_root(render_data),
@@ -503,7 +562,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform_scale * SCALE_EFFECT; let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform_scale * SCALE_EFFECT;
let viewport_size = ipp.viewport_bounds.size(); let viewport_size = ipp.viewport_bounds.size();
let viewport_mid = ipp.viewport_bounds.center(); let viewport_mid = ipp.viewport_bounds.center();
let [bounds1, bounds2] = self.document_bounds(font_cache).unwrap_or([viewport_mid; 2]); let [bounds1, bounds2] = self.document_bounds(&persistent_data.font_cache).unwrap_or([viewport_mid; 2]);
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale; let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale; let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING); let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
@@ -621,17 +680,44 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
SetBlendModeForSelectedLayers { blend_mode } => { SetBlendModeForSelectedLayers { blend_mode } => {
self.backup(responses); self.backup(responses);
for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { for path in self.selected_layers() {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into()); responses.push_back(DocumentOperation::SetLayerBlendMode { path: path.to_vec(), blend_mode }.into());
} }
} }
SetImageBlobUrl {
layer_path,
blob_url,
resolution,
document_id,
} => {
let layer = self.graphene_document.layer(&layer_path).expect("Setting blob URL for invalid layer");
// Revoke the old blob URL
match &layer.data {
LayerDataType::Imaginate(imaginate) => {
if let Some(url) = &imaginate.blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
}
LayerDataType::Image(_) => {}
other => panic!("Setting blob URL for invalid layer type, which must be an `Imaginate` or `Image`. Found: `{:?}`", other),
}
responses.push_back(
PortfolioMessage::DocumentPassMessage {
document_id,
message: DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into(),
}
.into(),
);
}
SetLayerExpansion { layer_path, set_expanded } => { SetLayerExpansion { layer_path, set_expanded } => {
self.layer_metadata_mut(&layer_path).expanded = set_expanded; self.layer_metadata_mut(&layer_path).expanded = set_expanded;
responses.push_back(DocumentStructureChanged.into()); responses.push_back(DocumentStructureChanged.into());
responses.push_back(LayerChanged { affected_layer_path: layer_path }.into()) responses.push_back(LayerChanged { affected_layer_path: layer_path }.into())
} }
SetLayerName { layer_path, name } => { SetLayerName { layer_path, name } => {
if let Some(layer) = self.layer_panel_entry_from_path(&layer_path, font_cache) { if let Some(layer) = self.layer_panel_entry_from_path(&layer_path, &persistent_data.font_cache) {
// Only save the history state if the name actually changed to something different // Only save the history state if the name actually changed to something different
if layer.name != name { if layer.name != name {
self.backup(responses); self.backup(responses);
@@ -664,7 +750,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
SetSnapping { snap } => { SetSnapping { snap } => {
self.snapping_enabled = snap; self.snapping_enabled = snap;
} }
SetTexboxEditability { path, editable } => { SetTextboxEditability { path, editable } => {
let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap(); let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap();
responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into()); responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into());
if editable { if editable {
@@ -760,7 +846,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
responses.push_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. }.into()); responses.push_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. }.into());
} }
ZoomCanvasToFitAll => { ZoomCanvasToFitAll => {
if let Some(bounds) = self.document_bounds(font_cache) { if let Some(bounds) = self.document_bounds(&persistent_data.font_cache) {
responses.push_back( responses.push_back(
NavigationMessage::FitViewportToBounds { NavigationMessage::FitViewportToBounds {
bounds, bounds,
@@ -812,6 +898,99 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
impl DocumentMessageHandler { impl DocumentMessageHandler {
pub fn call_imaginate(&mut self, document_id: u64, preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option<Message> {
let layer_path = {
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
// Get what is hopefully the only selected Imaginate layer
match selected_imaginate_layers.next() {
// Continue only if there are no additional Imaginate layers also selected
Some(layer_path) if selected_imaginate_layers.next().is_none() => layer_path.to_owned(),
_ => return None,
}
};
// Prepare the Imaginate parameters and base image
let layer = self.graphene_document.layer(&layer_path).unwrap();
let imaginate_layer = layer.as_imaginate().unwrap();
let parameters = ImaginateGenerationParameters {
seed: imaginate_layer.seed,
samples: imaginate_layer.samples,
sampling_method: imaginate_layer.sampling_method.api_value().to_string(),
denoising_strength: imaginate_layer.use_img2img.then_some(imaginate_layer.denoising_strength),
cfg_scale: imaginate_layer.cfg_scale,
prompt: imaginate_layer.prompt.clone(),
negative_prompt: imaginate_layer.negative_prompt.clone(),
resolution: pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache),
restore_faces: imaginate_layer.restore_faces,
tiling: imaginate_layer.tiling,
};
let base_image = if imaginate_layer.use_img2img {
// Calculate the bounding box of the region to be exported
let bounds = layer.aabb(&persistent_data.font_cache).unwrap_or_default();
let size = bounds[1] - bounds[0];
let svg = self.render_document(bounds, persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
Some(ImaginateBaseImage { svg, size })
} else {
None
};
Some(
FrontendMessage::TriggerImaginateGenerate {
parameters,
base_image,
hostname: preferences.imaginate_server_hostname.clone(),
refresh_frequency: preferences.imaginate_refresh_frequency,
document_id,
layer_path,
}
.into(),
)
}
pub fn render_document(&mut self, bounds: [DVec2; 2], persistent_data: &PersistentData, render_mode: DocumentRenderMode) -> String {
// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end
let old_artwork_transform = self.graphene_document.root.transform;
self.graphene_document.root.transform = DAffine2::IDENTITY;
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
let old_artboard_transform = self.artboard_message_handler.artboards_graphene_document.root.transform;
self.artboard_message_handler.artboards_graphene_document.root.transform = DAffine2::IDENTITY;
GrapheneDocument::mark_children_as_dirty(&mut self.artboard_message_handler.artboards_graphene_document.root);
// Render the document SVG code
let size = bounds[1] - bounds[0];
let render_data = RenderData::new(ViewMode::Normal, &persistent_data.font_cache, None);
let artwork = match render_mode {
DocumentRenderMode::Root => self.graphene_document.render_root(render_data),
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(),
};
let artboards = self.artboard_message_handler.artboards_graphene_document.render_root(render_data);
let outside_artboards_color = if self.artboard_message_handler.artboard_ids.is_empty() { "#ffffff" } else { "#000000" };
let outside_artboards = format!(r#"<rect x="{}" y="{}" width="100%" height="100%" fill="{}" />"#, bounds[0].x, bounds[0].y, outside_artboards_color);
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}" height="{}">{}{}{}{}</svg>"#,
bounds[0].x, bounds[0].y, size.x, size.y, size.x, size.y, "\n", outside_artboards, artboards, artwork
);
// Transform the artwork and artboard back to their original scales
self.graphene_document.root.transform = old_artwork_transform;
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
self.artboard_message_handler.artboards_graphene_document.root.transform = old_artboard_transform;
GrapheneDocument::mark_children_as_dirty(&mut self.artboard_message_handler.artboards_graphene_document.root);
svg
}
pub fn serialize_document(&self) -> String { pub fn serialize_document(&self) -> String {
let val = serde_json::to_string(self); let val = serde_json::to_string(self);
// We fully expect the serialization to succeed // We fully expect the serialization to succeed
@@ -881,11 +1060,20 @@ impl DocumentMessageHandler {
} }
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> { pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())) self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice()))
}
pub fn selected_layers_with_type(&self, discriminant: LayerDataTypeDiscriminant) -> impl Iterator<Item = &[LayerId]> {
self.selected_layers().filter(move |path| {
self.graphene_document
.layer(path)
.map(|layer| LayerDataTypeDiscriminant::from(&layer.data) == discriminant)
.unwrap_or(false)
})
} }
pub fn non_selected_layers(&self) -> impl Iterator<Item = &[LayerId]> { pub fn non_selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then(|| path.as_slice())) self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then_some(path.as_slice()))
} }
pub fn selected_layers_without_children(&self) -> Vec<&[LayerId]> { pub fn selected_layers_without_children(&self) -> Vec<&[LayerId]> {
@@ -1014,7 +1202,7 @@ impl DocumentMessageHandler {
/// Returns an unsorted list of all layer paths including folders at all levels, except the document's top-level root folder itself /// Returns an unsorted list of all layer paths including folders at all levels, except the document's top-level root folder itself
pub fn all_layers(&self) -> impl Iterator<Item = &[LayerId]> { pub fn all_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then(|| path.as_slice())) self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then_some(path.as_slice()))
} }
/// Returns the paths to all layers in order /// Returns the paths to all layers in order
@@ -1022,7 +1210,7 @@ impl DocumentMessageHandler {
// Compute the indices for each layer to be able to sort them // Compute the indices for each layer to be able to sort them
let mut layers_with_indices: Vec<(&[LayerId], Vec<usize>)> = paths let mut layers_with_indices: Vec<(&[LayerId], Vec<usize>)> = paths
// 'path.len() > 0' filters out root layer since it has no indices // 'path.len() > 0' filters out root layer since it has no indices
.filter_map(|path| (!path.is_empty()).then(|| path)) .filter_map(|path| (!path.is_empty()).then_some(path))
.filter_map(|path| { .filter_map(|path| {
// TODO: `indices_for_path` can return an error. We currently skip these layers and log a warning. Once this problem is solved this code can be simplified. // TODO: `indices_for_path` can return an error. We currently skip these layers and log a warning. Once this problem is solved this code can be simplified.
match self.graphene_document.indices_for_path(path) { match self.graphene_document.indices_for_path(path) {
@@ -1093,7 +1281,7 @@ impl DocumentMessageHandler {
Some((document, layer_metadata)) => { Some((document, layer_metadata)) => {
// Update the currently displayed layer on the Properties panel if the selection changes after an undo action // Update the currently displayed layer on the Properties panel if the selection changes after an undo action
// Also appropriately update the Properties panel if an undo action results in a layer being deleted // Also appropriately update the Properties panel if an undo action results in a layer being deleted
let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then(|| layer_id.clone())).collect(); let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
if prev_selected_paths != selected_paths { if prev_selected_paths != selected_paths {
responses.push_back(BroadcastEvent::SelectionChanged.into()); responses.push_back(BroadcastEvent::SelectionChanged.into());
@@ -1123,7 +1311,7 @@ impl DocumentMessageHandler {
Some((document, layer_metadata)) => { Some((document, layer_metadata)) => {
// Update currently displayed layer on property panel if selection changes after redo action // Update currently displayed layer on property panel if selection changes after redo action
// Also appropriately update property panel if redo action results in a layer being added // Also appropriately update property panel if redo action results in a layer being added
let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then(|| layer_id.clone())).collect(); let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
if next_selected_paths != selected_paths { if next_selected_paths != selected_paths {
responses.push_back(BroadcastEvent::SelectionChanged.into()); responses.push_back(BroadcastEvent::SelectionChanged.into());
@@ -1231,24 +1419,33 @@ impl DocumentMessageHandler {
} }
/// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document /// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>) { pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>, document_id: u64) {
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>, fonts: &mut HashSet<Font>) { fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>, fonts: &mut HashSet<Font>) {
match data { match data {
LayerDataType::Folder(f) => { LayerDataType::Folder(folder) => {
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) { for (id, layer) in folder.layer_ids.iter().zip(folder.layers().iter()) {
path.push(*id); path.push(*id);
walk_layers(&layer.data, path, image_data, fonts); walk_layers(&layer.data, path, image_data, fonts);
path.pop(); path.pop();
} }
} }
LayerDataType::Text(txt) => { LayerDataType::Text(text) => {
fonts.insert(txt.font.clone()); fonts.insert(text.font.clone());
} }
LayerDataType::Image(img) => image_data.push(FrontendImageData { LayerDataType::Image(image) => image_data.push(FrontendImageData {
path: path.clone(), path: path.clone(),
image_data: img.image_data.clone(), image_data: image.image_data.clone(),
mime: img.mime.clone(), mime: image.mime.clone(),
}), }),
LayerDataType::Imaginate(imaginate) => {
if let Some(data) = &imaginate.image_data {
image_data.push(FrontendImageData {
path: path.clone(),
image_data: data.image_data.clone(),
mime: imaginate.mime.clone(),
});
}
}
_ => {} _ => {}
} }
} }
@@ -1257,7 +1454,7 @@ impl DocumentMessageHandler {
let mut fonts = HashSet::new(); let mut fonts = HashSet::new();
walk_layers(root, &mut path, &mut image_data, &mut fonts); walk_layers(root, &mut path, &mut image_data, &mut fonts);
if !image_data.is_empty() { if !image_data.is_empty() {
responses.push_front(FrontendMessage::UpdateImageData { image_data }.into()); responses.push_front(FrontendMessage::UpdateImageData { document_id, image_data }.into());
} }
for font in fonts { for font in fonts {
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default: false }.into()); responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default: false }.into());
@@ -50,11 +50,9 @@ impl Default for NavigationMessageHandler {
impl MessageHandler<NavigationMessage, (&Document, &InputPreprocessorMessageHandler)> for NavigationMessageHandler { impl MessageHandler<NavigationMessage, (&Document, &InputPreprocessorMessageHandler)> for NavigationMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: NavigationMessage, data: (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: NavigationMessage, (document, ipp): (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
use NavigationMessage::*; use NavigationMessage::*;
let (document, ipp) = data;
#[remain::sorted] #[remain::sorted]
match message { match message {
DecreaseCanvasZoom { center_on_mouse } => { DecreaseCanvasZoom { center_on_mouse } => {
@@ -31,7 +31,7 @@ impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentOverlays { FrontendMessage::UpdateDocumentOverlays {
svg: if overlays_visible { svg: if overlays_visible {
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false); let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()));
self.overlays_graphene_document.render_root(render_data) self.overlays_graphene_document.render_root(render_data)
} else { } else {
String::from("") String::from("")
@@ -3,6 +3,7 @@ use crate::messages::layout::utility_types::widgets::assist_widgets::PivotPositi
use crate::messages::portfolio::document::utility_types::misc::TargetDocument; use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::layers::imaginate_layer::ImaginateSamplingMethod;
use graphene::layers::style::{Fill, Stroke}; use graphene::layers::style::{Fill, Stroke};
use graphene::LayerId; use graphene::LayerId;
@@ -26,6 +27,19 @@ pub enum PropertiesPanelMessage {
ModifyTransform { value: f64, transform_op: TransformOp }, ModifyTransform { value: f64, transform_op: TransformOp },
ResendActiveProperties, ResendActiveProperties,
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument }, SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
SetImaginateCfgScale { cfg_scale: f64 },
SetImaginateDenoisingStrength { denoising_strength: f64 },
SetImaginateNegativePrompt { negative_prompt: String },
SetImaginatePrompt { prompt: String },
SetImaginateRestoreFaces { restore_faces: bool },
SetImaginateSamples { samples: u32 },
SetImaginateSamplingMethod { method: ImaginateSamplingMethod },
SetImaginateScaleFromResolution,
SetImaginateSeed { seed: u64 },
SetImaginateSeedRandomize,
SetImaginateSeedRandomizeAndGenerate,
SetImaginateTiling { tiling: bool },
SetImaginateUseImg2Img { use_img2img: bool },
SetPivot { new_position: PivotPosition }, SetPivot { new_position: PivotPosition },
UpdateSelectedDocumentProperties, UpdateSelectedDocumentProperties,
} }
@@ -1,9 +1,11 @@
use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties}; use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties};
use super::utility_types::PropertiesPanelMessageHandlerData; use super::utility_types::PropertiesPanelMessageHandlerData;
use crate::application::generate_uuid;
use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout}; use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget; 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::properties_panel::utility_functions::apply_transform_operation;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument; use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::{LayerId, Operation}; use graphene::{LayerId, Operation};
@@ -15,14 +17,13 @@ pub struct PropertiesPanelMessageHandler {
active_selection: Option<(Vec<LayerId>, TargetDocument)>, active_selection: Option<(Vec<LayerId>, TargetDocument)>,
} }
impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerData<'a>> for PropertiesPanelMessageHandler { impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPanelMessageHandlerData<'a>)> for PropertiesPanelMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: PropertiesPanelMessage, data: PropertiesPanelMessageHandlerData, responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: PropertiesPanelMessage, (persistent_data, data): (&PersistentData, PropertiesPanelMessageHandlerData), responses: &mut VecDeque<Message>) {
let PropertiesPanelMessageHandlerData { let PropertiesPanelMessageHandlerData {
artwork_document, artwork_document,
artboard_document, artboard_document,
selected_layers, selected_layers,
font_cache,
} = data; } = data;
let get_document = |document_selector: TargetDocument| match document_selector { let get_document = |document_selector: TargetDocument| match document_selector {
TargetDocument::Artboard => artboard_document, TargetDocument::Artboard => artboard_document,
@@ -81,7 +82,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer"); 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 layer = get_document(*target_document).layer(path).unwrap();
let transform = apply_transform_operation(layer, transform_op, value, font_cache); let transform = apply_transform_operation(layer, transform_op, value, &persistent_data.font_cache);
responses.push_back(self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform })); responses.push_back(self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }));
} }
@@ -136,8 +137,8 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
if let Some((path, target_document)) = self.active_selection.clone() { if let Some((path, target_document)) = self.active_selection.clone() {
let layer = get_document(target_document).layer(&path).unwrap(); let layer = get_document(target_document).layer(&path).unwrap();
match target_document { match target_document {
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, font_cache), TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, persistent_data),
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, font_cache), TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, persistent_data),
} }
} }
} }
@@ -148,6 +149,62 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
} }
.into(), .into(),
), ),
SetImaginatePrompt { prompt } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetPrompt { path, prompt }.into());
}
SetImaginateNegativePrompt { negative_prompt } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetNegativePrompt { path, negative_prompt }.into());
}
SetImaginateDenoisingStrength { denoising_strength } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetDenoisingStrength { path, denoising_strength }.into());
}
SetImaginateSamples { samples } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetSamples { path, samples }.into());
}
SetImaginateSamplingMethod { method } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::SetImaginateSamplingMethod { path, method }.into());
}
SetImaginateScaleFromResolution => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetScaleFromResolution { path }.into());
}
SetImaginateSeed { seed } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
}
SetImaginateSeedRandomize => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
let seed = generate_uuid();
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
}
SetImaginateSeedRandomizeAndGenerate => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
let seed = generate_uuid();
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
responses.push_back(DocumentMessage::ImaginateGenerate.into());
}
SetImaginateCfgScale { cfg_scale } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetCfgScale { path, cfg_scale }.into());
}
SetImaginateUseImg2Img { use_img2img } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetUseImg2Img { path, use_img2img }.into());
}
SetImaginateRestoreFaces { restore_faces } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetRestoreFaces { path, restore_faces }.into());
}
SetImaginateTiling { tiling } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetTiling { path, tiling }.into());
}
} }
} }
@@ -2,17 +2,22 @@ use super::utility_types::TransformOp;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist; use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
use crate::messages::layout::utility_types::widgets::button_widgets::PopoverButton; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton};
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput}; use crate::messages::layout::utility_types::widgets::input_widgets::{
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, IconStyle, Separator, SeparatorDirection, SeparatorType, TextLabel}; CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput,
};
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use glam::{DAffine2, DVec2};
use graphene::color::Color; use graphene::color::Color;
use graphene::document::pick_layer_safe_imaginate_resolution;
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
use graphene::layers::text_layer::{FontCache, TextLayer}; use graphene::layers::text_layer::{FontCache, TextLayer};
use glam::{DAffine2, DVec2};
use std::f64::consts::PI; use std::f64::consts::PI;
use std::rc::Rc; use std::rc::Rc;
@@ -34,12 +39,12 @@ pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value
transformation(layer.transform, value / scale).to_cols_array() transformation(layer.transform, value / scale).to_cols_array()
} }
pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) { pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
let options_bar = vec![LayoutGroup::Row { let options_bar = vec![LayoutGroup::Row {
widgets: vec![ widgets: vec![
WidgetHolder::new(Widget::IconLabel(IconLabel { WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeArtboard".into(), icon: "NodeArtboard".into(),
icon_style: IconStyle::Node, tooltip: "Artboard".into(),
})), })),
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related, separator_type: SeparatorType::Related,
@@ -81,7 +86,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
} else { } else {
panic!("Artboard must have a solid fill") panic!("Artboard must have a solid fill")
}; };
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache)); let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&persistent_data.font_cache));
vec![LayoutGroup::Section { vec![LayoutGroup::Section {
name: "Artboard".into(), name: "Artboard".into(),
@@ -139,7 +144,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(layer.bounding_transform(font_cache).scale_x()), value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()),
label: "W".into(), label: "W".into(),
unit: " px".into(), unit: " px".into(),
on_update: WidgetCallback::new(|number_input: &NumberInput| { on_update: WidgetCallback::new(|number_input: &NumberInput| {
@@ -156,7 +161,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(layer.bounding_transform(font_cache).scale_y()), value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()),
label: "H".into(), label: "H".into(),
unit: " px".into(), unit: " px".into(),
on_update: WidgetCallback::new(|number_input: &NumberInput| { on_update: WidgetCallback::new(|number_input: &NumberInput| {
@@ -219,25 +224,29 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
); );
} }
pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) { pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
let options_bar = vec![LayoutGroup::Row { let options_bar = vec![LayoutGroup::Row {
widgets: vec![ widgets: vec![
match &layer.data { match &layer.data {
LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeFolder".into(), icon: "NodeFolder".into(),
icon_style: IconStyle::Node, tooltip: "Folder".into(),
})), })),
LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeShape".into(), icon: "NodeShape".into(),
icon_style: IconStyle::Node, tooltip: "Shape".into(),
})), })),
LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeText".into(), icon: "NodeText".into(),
icon_style: IconStyle::Node, tooltip: "Text".into(),
})), })),
LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeImage".into(), icon: "NodeImage".into(),
icon_style: IconStyle::Node, tooltip: "Image".into(),
})),
LayerDataType::Imaginate(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
icon: "NodeImaginate".into(),
tooltip: "Imaginate".into(),
})), })),
}, },
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
@@ -272,24 +281,31 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
let properties_body = match &layer.data { let properties_body = match &layer.data {
LayerDataType::Shape(shape) => { LayerDataType::Shape(shape) => {
if let Some(fill_layout) = node_section_fill(shape.style.fill()) { if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
vec![node_section_transform(layer, font_cache), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())] vec![
node_section_transform(layer, persistent_data),
fill_layout,
node_section_stroke(&shape.style.stroke().unwrap_or_default()),
]
} else { } else {
vec![node_section_transform(layer, font_cache), node_section_stroke(&shape.style.stroke().unwrap_or_default())] vec![node_section_transform(layer, persistent_data), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
} }
} }
LayerDataType::Text(text) => { LayerDataType::Text(text) => {
vec![ vec![
node_section_transform(layer, font_cache), node_section_transform(layer, persistent_data),
node_section_font(text), node_section_font(text),
node_section_fill(text.path_style.fill()).expect("Text should have fill"), node_section_fill(text.path_style.fill()).expect("Text should have fill"),
node_section_stroke(&text.path_style.stroke().unwrap_or_default()), node_section_stroke(&text.path_style.stroke().unwrap_or_default()),
] ]
} }
LayerDataType::Image(_) => { LayerDataType::Image(_) => {
vec![node_section_transform(layer, font_cache)] vec![node_section_transform(layer, persistent_data)]
} }
_ => { LayerDataType::Imaginate(imaginate) => {
vec![] vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)]
}
LayerDataType::Folder(_) => {
vec![node_section_transform(layer, persistent_data)]
} }
}; };
@@ -309,8 +325,8 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
); );
} }
fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup { fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> LayoutGroup {
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache)); let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&persistent_data.font_cache));
LayoutGroup::Section { LayoutGroup::Section {
name: "Transform".into(), name: "Transform".into(),
layout: vec![ layout: vec![
@@ -442,7 +458,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(layer.bounding_transform(font_cache).scale_x()), value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()),
label: "W".into(), label: "W".into(),
unit: " px".into(), unit: " px".into(),
on_update: WidgetCallback::new(|number_input: &NumberInput| { on_update: WidgetCallback::new(|number_input: &NumberInput| {
@@ -459,7 +475,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(layer.bounding_transform(font_cache).scale_y()), value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()),
label: "H".into(), label: "H".into(),
unit: " px".into(), unit: " px".into(),
on_update: WidgetCallback::new(|number_input: &NumberInput| { on_update: WidgetCallback::new(|number_input: &NumberInput| {
@@ -477,6 +493,524 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
} }
} }
fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persistent_data: &PersistentData, responses: &mut VecDeque<Message>) -> LayoutGroup {
LayoutGroup::Section {
name: "Imaginate".into(),
layout: vec![
LayoutGroup::Row {
widgets: {
let tooltip = "Connection status to the server that computes generated images".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Server".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Settings".into(),
tooltip: "Preferences: Imaginate".into(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: {
match &persistent_data.imaginate_server_status {
ImaginateServerStatus::Unknown => {
responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into());
"Checking...".into()
}
ImaginateServerStatus::Checking => "Checking...".into(),
ImaginateServerStatus::Unavailable => "Unavailable".into(),
ImaginateServerStatus::Connected => "Connected".into(),
}
},
bold: true,
tooltip,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Reload".into(),
tooltip: "Refresh connection status".into(),
on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "When generating, the percentage represents how many sampling steps have so far been processed out of the target number".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Progress".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: {
// Since we don't serialize the status, we need to derive from other state whether the Idle state is actually supposed to be the Terminated state
let mut interpreted_status = imaginate_layer.status.clone();
if imaginate_layer.status == ImaginateStatus::Idle
&& imaginate_layer.blob_url.is_some()
&& imaginate_layer.percent_complete > 0.
&& imaginate_layer.percent_complete < 100.
{
interpreted_status = ImaginateStatus::Terminated;
}
match interpreted_status {
ImaginateStatus::Idle => match imaginate_layer.blob_url {
Some(_) => "Done".into(),
None => "Ready".into(),
},
ImaginateStatus::Beginning => "Beginning...".into(),
ImaginateStatus::Uploading(percent) => format!("Uploading Base Image: {:.0}%", percent),
ImaginateStatus::Generating => format!("Generating: {:.0}%", imaginate_layer.percent_complete),
ImaginateStatus::Terminating => "Terminating...".into(),
ImaginateStatus::Terminated => format!("{:.0}% (Terminated)", imaginate_layer.percent_complete),
}
},
bold: true,
tooltip,
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: [
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Image".into(),
tooltip: "Buttons that control the image generation process".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
],
{
match imaginate_layer.status {
ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "Beginning...".into(),
tooltip: "Sending image generation request to the server".into(),
disabled: true,
..Default::default()
}))],
ImaginateStatus::Generating => vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "Terminate".into(),
tooltip: "Cancel in-progress image generation and keep the latest progress".into(),
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateTerminate.into()),
..Default::default()
}))],
ImaginateStatus::Terminating => vec![WidgetHolder::new(Widget::TextButton(TextButton {
label: "Terminating...".into(),
tooltip: "Waiting on the final image generated after termination".into(),
disabled: true,
..Default::default()
}))],
ImaginateStatus::Idle | ImaginateStatus::Terminated => vec![
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Random".into(),
tooltip: "Generate with a random seed".into(),
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomizeAndGenerate.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Generate".into(),
tooltip: "Fill layer frame by generating a new image".into(),
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateGenerate.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextButton(TextButton {
label: "Clear".into(),
tooltip: "Remove generated image from the layer frame".into(),
disabled: imaginate_layer.blob_url == None,
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateClear.into()),
..Default::default()
})),
],
}
},
]
.concat(),
},
LayoutGroup::Row {
widgets: {
let tooltip = "Seed determines the random outcome, enabling limitless unique variations".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Seed".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Regenerate".into(),
tooltip: "Set a new random seed".into(),
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomize.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.seed as f64),
min: Some(-1.),
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateSeed {
seed: number_input.value.unwrap().round() as u64,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\
\n\
512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent. Put the layer in a folder and resize that to keep resolution unchanged.\n\
\n\
Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server.
".trim().to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Resolution".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::IconButton(IconButton {
size: 24,
icon: "Rescale".into(),
tooltip: "Set the layer scale to this resolution".into(),
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateScaleFromResolution.into()),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: {
let (width, height) = pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache);
format!("{} W x {} H", width, height)
},
tooltip,
bold: true,
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Number of iterations to improve the image generation quality, with diminishing returns around 40".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Sampling Steps".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.samples.into()),
min: Some(0.),
max: Some(150.),
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateSamples {
samples: number_input.value.unwrap().round() as u32,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "
Algorithm used to generate the image during each sampling step.\n\
\n\
'DPM Fast' and 'DPM Adaptive' do not support live refreshing updates.
"
.trim()
.to_string();
let sampling_methods = ImaginateSamplingMethod::list();
let mut entries = Vec::with_capacity(sampling_methods.len());
for method in sampling_methods {
entries.push(DropdownEntryData {
label: method.to_string(),
on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateSamplingMethod { method }.into()),
..DropdownEntryData::default()
});
}
let entries = vec![entries];
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Sampling Method".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
entries,
selected_index: Some(imaginate_layer.sampling_method as u32),
tooltip,
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Generate an image based upon the artwork beneath this frame in the containing folder".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Use Base Image".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateUseImg2Img { use_img2img: checkbox_input.checked }.into()),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Strength of the artistic liberties allowing changes from the base image. The image is unaltered at 0 and completely different at 1.".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Image Creativity".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.denoising_strength),
min: Some(0.),
max: Some(1.),
disabled: !imaginate_layer.use_img2img,
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateDenoisingStrength {
denoising_strength: number_input.value.unwrap(),
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip =
"Amplification of the text prompt's influence over the outcome. Lower values are more creative and exploratory. Higher values are more literal and uninspired.".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Text Rigidness".into(),
tooltip: tooltip.to_string(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(imaginate_layer.cfg_scale),
min: Some(0.),
max: Some(30.),
tooltip,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
PropertiesPanelMessage::SetImaginateCfgScale {
cfg_scale: number_input.value.unwrap(),
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Text Prompt".into(),
tooltip: "
Description of the desired image subject and style.\n\
\n\
Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
\n\
To boost the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
\"(colorless:0.7) green (ideas sleep:1.3) furiously\"
"
.trim()
.into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
value: imaginate_layer.prompt.clone(),
on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| {
PropertiesPanelMessage::SetImaginatePrompt {
prompt: text_area_input.value.clone(),
}
.into()
}),
..Default::default()
})),
],
},
LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Neg. Prompt".into(),
tooltip: "A negative text prompt can be used to list things like objects or colors to avoid".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
value: imaginate_layer.negative_prompt.clone(),
on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| {
PropertiesPanelMessage::SetImaginateNegativePrompt {
negative_prompt: text_area_input.value.clone(),
}
.into()
}),
..Default::default()
})),
],
},
LayoutGroup::Row {
widgets: {
let tooltip = "Postprocess human (or human-like) faces to look subtly less distorted".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Improve Faces".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: imaginate_layer.restore_faces,
tooltip,
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| {
PropertiesPanelMessage::SetImaginateRestoreFaces {
restore_faces: checkbox_input.checked,
}
.into()
}),
..Default::default()
})),
]
},
},
LayoutGroup::Row {
widgets: {
let tooltip = "Generate the image so its edges loop seamlessly to make repeatable patterns or textures".to_string();
vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Tiling".into(),
tooltip: tooltip.clone(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
checked: imaginate_layer.tiling,
tooltip,
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateTiling { tiling: checkbox_input.checked }.into()),
..Default::default()
})),
]
},
},
],
}
}
fn node_section_font(layer: &TextLayer) -> LayoutGroup { fn node_section_font(layer: &TextLayer) -> LayoutGroup {
let font = layer.font.clone(); let font = layer.font.clone();
let size = layer.size; let size = layer.size;
@@ -1,5 +1,4 @@
use graphene::document::Document as GrapheneDocument; use graphene::document::Document as GrapheneDocument;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId; use graphene::LayerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -8,7 +7,6 @@ pub struct PropertiesPanelMessageHandlerData<'a> {
pub artwork_document: &'a GrapheneDocument, pub artwork_document: &'a GrapheneDocument,
pub artboard_document: &'a GrapheneDocument, pub artboard_document: &'a GrapheneDocument,
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>, pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
pub font_cache: &'a FontCache,
} }
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
@@ -72,7 +72,7 @@ impl LayerPanelEntry {
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>(); let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new(); let mut thumbnail = String::new();
let mut svg_defs = String::new(); let mut svg_defs = String::new();
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false); let render_data = RenderData::new(ViewMode::Normal, font_cache, None);
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data); 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 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() { let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
@@ -44,12 +44,11 @@ pub enum DocumentMode {
impl fmt::Display for DocumentMode { impl fmt::Display for DocumentMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self { match self {
DocumentMode::DesignMode => "Design Mode".to_string(), DocumentMode::DesignMode => write!(f, "Design Mode"),
DocumentMode::SelectMode => "Select Mode".to_string(), DocumentMode::SelectMode => write!(f, "Select Mode"),
DocumentMode::GuideMode => "Guide Mode".to_string(), DocumentMode::GuideMode => write!(f, "Guide Mode"),
}; }
write!(f, "{}", text)
} }
} }
@@ -63,33 +62,7 @@ impl DocumentMode {
} }
} }
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)] pub enum DocumentRenderMode<'a> {
pub enum Platform { Root,
#[default] OnlyBelowLayerInFolder(&'a [LayerId]),
Unknown,
Windows,
Mac,
Linux,
}
impl Platform {
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
match self {
Platform::Mac => KeyboardPlatformLayout::Mac,
Platform::Unknown => {
warn!("The platform has not been set, remember to send `GlobalsMessage::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,
} }
@@ -84,6 +84,13 @@ impl PropertyHolder for MenuBarMessageHandler {
..MenuBarEntry::default() ..MenuBarEntry::default()
}, },
], ],
vec![MenuBarEntry {
label: "Preferences…".into(),
icon: Some("Settings".into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestPreferencesDialog),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestPreferencesDialog.into()),
..MenuBarEntry::default()
}],
]), ]),
), ),
MenuBarEntry::new_root( MenuBarEntry::new_root(
+1
View File
@@ -3,6 +3,7 @@ mod portfolio_message_handler;
pub mod document; pub mod document;
pub mod menu_bar; pub mod menu_bar;
pub mod utility_types;
#[doc(inline)] #[doc(inline)]
pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant}; pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
@@ -1,7 +1,8 @@
use super::utility_types::ImaginateServerStatus;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::layers::text_layer::Font; use graphene::layers::{imaginate_layer::ImaginateStatus, text_layer::Font};
use graphene::LayerId; use graphene::LayerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -13,12 +14,17 @@ pub enum PortfolioMessage {
// Sub-messages // Sub-messages
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
Document(DocumentMessage), MenuBar(MenuBarMessage),
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
MenuBar(MenuBarMessage), Document(DocumentMessage),
// Messages // Messages
#[remain::unsorted]
DocumentPassMessage {
document_id: u64,
message: DocumentMessage,
},
AutoSaveActiveDocument, AutoSaveActiveDocument,
AutoSaveDocument { AutoSaveDocument {
document_id: u64, document_id: u64,
@@ -45,8 +51,31 @@ pub enum PortfolioMessage {
data: Vec<u8>, data: Vec<u8>,
is_default: bool, is_default: bool,
}, },
ImaginateCheckServerStatus,
ImaginateSetBlobUrl {
document_id: u64,
layer_path: Vec<LayerId>,
blob_url: String,
resolution: (f64, f64),
},
ImaginateSetGeneratingStatus {
document_id: u64,
path: Vec<LayerId>,
percent: Option<f64>,
status: ImaginateStatus,
},
ImaginateSetImageData {
document_id: u64,
layer_path: Vec<LayerId>,
image_data: Vec<u8>,
},
ImaginateSetServerStatus {
status: ImaginateServerStatus,
},
Import, Import,
LoadDocumentResources, LoadDocumentResources {
document_id: u64,
},
LoadFont { LoadFont {
font: Font, font: Font,
is_default: bool, is_default: bool,
@@ -66,6 +95,7 @@ pub enum PortfolioMessage {
document_is_saved: bool, document_is_saved: bool,
document_serialized_content: String, document_serialized_content: String,
}, },
// TODO: Paste message is unused, delete it?
Paste { Paste {
clipboard: Clipboard, clipboard: Clipboard,
}, },
@@ -84,6 +114,12 @@ pub enum PortfolioMessage {
SetActiveDocument { SetActiveDocument {
document_id: u64, document_id: u64,
}, },
SetImageBlobUrl {
document_id: u64,
layer_path: Vec<LayerId>,
blob_url: String,
resolution: (f64, f64),
},
UpdateDocumentWidgets, UpdateDocumentWidgets,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
} }
@@ -1,3 +1,4 @@
use super::utility_types::PersistentData;
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::messages::dialog::simple_dialogs; use crate::messages::dialog::simple_dialogs;
@@ -5,10 +6,11 @@ use crate::messages::frontend::utility_types::FrontendDocumentDetails;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget; 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::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::utility_types::ImaginateServerStatus;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene::layers::layer_info::LayerDataTypeDiscriminant; use graphene::layers::layer_info::LayerDataTypeDiscriminant;
use graphene::layers::text_layer::{Font, FontCache}; use graphene::layers::text_layer::Font;
use graphene::Operation as DocumentOperation; use graphene::Operation as DocumentOperation;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -18,34 +20,39 @@ pub struct PortfolioMessageHandler {
document_ids: Vec<u64>, document_ids: Vec<u64>,
active_document_id: Option<u64>, active_document_id: Option<u64>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize], copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
font_cache: FontCache, pub persistent_data: PersistentData,
} }
impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for PortfolioMessageHandler { impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &PreferencesMessageHandler)> for PortfolioMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { fn process_message(&mut self, message: PortfolioMessage, (ipp, preferences): (&InputPreprocessorMessageHandler, &PreferencesMessageHandler), responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
use PortfolioMessage::*;
#[remain::sorted] #[remain::sorted]
match message { match message {
// Sub-messages // Sub-messages
#[remain::unsorted] #[remain::unsorted]
Document(message) => { PortfolioMessage::MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) { #[remain::unsorted]
document.process_message(message, (ipp, &self.font_cache), responses) PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id {
if let Some(document) = self.documents.get_mut(&document_id) {
document.process_message(message, (document_id, ipp, &self.persistent_data, preferences), responses)
}
} }
} }
#[remain::unsorted]
MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
// Messages // Messages
AutoSaveActiveDocument => { #[remain::unsorted]
PortfolioMessage::DocumentPassMessage { document_id, message } => {
if let Some(document) = self.documents.get_mut(&document_id) {
document.process_message(message, (document_id, ipp, &self.persistent_data, preferences), responses)
}
}
PortfolioMessage::AutoSaveActiveDocument => {
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into()); responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into());
} }
} }
AutoSaveDocument { document_id } => { PortfolioMessage::AutoSaveDocument { document_id } => {
let document = self.documents.get(&document_id).unwrap(); let document = self.documents.get(&document_id).unwrap();
responses.push_back( responses.push_back(
FrontendMessage::TriggerIndexedDbWriteDocument { FrontendMessage::TriggerIndexedDbWriteDocument {
@@ -60,12 +67,12 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
.into(), .into(),
) )
} }
CloseActiveDocumentWithConfirmation => { PortfolioMessage::CloseActiveDocumentWithConfirmation => {
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into()); responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into());
} }
} }
CloseAllDocuments => { PortfolioMessage::CloseAllDocuments => {
if self.active_document_id.is_some() { if self.active_document_id.is_some() {
responses.push_back(PropertiesPanelMessage::Deactivate.into()); responses.push_back(PropertiesPanelMessage::Deactivate.into());
responses.push_back(BroadcastEvent::ToolAbort.into()); responses.push_back(BroadcastEvent::ToolAbort.into());
@@ -79,7 +86,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(PortfolioMessage::DestroyAllDocuments.into()); responses.push_back(PortfolioMessage::DestroyAllDocuments.into());
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
} }
CloseDocument { document_id } => { PortfolioMessage::CloseDocument { document_id } => {
let document_index = self.document_index(document_id); let document_index = self.document_index(document_id);
self.documents.remove(&document_id); self.documents.remove(&document_id);
self.document_ids.remove(document_index); self.document_ids.remove(document_index);
@@ -107,9 +114,9 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
// Send the new list of document tab names // Send the new list of document tab names
responses.push_back(UpdateOpenDocumentsList.into()); responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }.into()); responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }.into());
responses.push_back(RenderDocument.into()); responses.push_back(DocumentMessage::RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
for layer in document.layer_metadata.keys() { for layer in document.layer_metadata.keys() {
@@ -117,7 +124,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
} }
} }
CloseDocumentWithConfirmation { document_id } => { PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
let target_document = self.documents.get(&document_id).unwrap(); let target_document = self.documents.get(&document_id).unwrap();
if target_document.is_saved() { if target_document.is_saved() {
responses.push_back(BroadcastEvent::ToolAbort.into()); responses.push_back(BroadcastEvent::ToolAbort.into());
@@ -134,7 +141,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
} }
} }
Copy { clipboard } => { PortfolioMessage::Copy { clipboard } => {
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self` // 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)) { if let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get(&id)) {
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| { let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
@@ -162,24 +169,24 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
} }
} }
Cut { clipboard } => { PortfolioMessage::Cut { clipboard } => {
responses.push_back(Copy { clipboard }.into()); responses.push_back(PortfolioMessage::Copy { clipboard }.into());
responses.push_back(DeleteSelectedLayers.into()); responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
} }
DestroyAllDocuments => { PortfolioMessage::DestroyAllDocuments => {
// Empty the list of internal document data // Empty the list of internal document data
self.documents.clear(); self.documents.clear();
self.document_ids.clear(); self.document_ids.clear();
self.active_document_id = None; self.active_document_id = None;
} }
FontLoaded { PortfolioMessage::FontLoaded {
font_family, font_family,
font_style, font_style,
preview_url, preview_url,
data, data,
is_default, is_default,
} => { } => {
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default); self.persistent_data.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
if let Some(document) = self.active_document_mut() { if let Some(document) = self.active_document_mut() {
document.graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text); document.graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text);
@@ -187,23 +194,64 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(BroadcastEvent::DocumentIsDirty.into()); responses.push_back(BroadcastEvent::DocumentIsDirty.into());
} }
} }
Import => { PortfolioMessage::ImaginateCheckServerStatus => {
self.persistent_data.imaginate_server_status = ImaginateServerStatus::Checking;
responses.push_back(
FrontendMessage::TriggerImaginateCheckServerStatus {
hostname: preferences.imaginate_server_hostname.clone(),
}
.into(),
);
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
}
PortfolioMessage::ImaginateSetBlobUrl {
document_id,
layer_path,
blob_url,
resolution,
} => {
if let Some(document) = self.documents.get_mut(&document_id) {
if let Ok(layer) = document.graphene_document.layer(&layer_path) {
let previous_blob_url = &layer.as_imaginate().unwrap().blob_url;
if let Some(url) = previous_blob_url {
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
}
let message = DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into();
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into());
}
}
}
PortfolioMessage::ImaginateSetGeneratingStatus { document_id, path, percent, status } => {
let message = DocumentOperation::ImaginateSetGeneratingStatus { path, percent, status }.into();
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into());
}
PortfolioMessage::ImaginateSetImageData { document_id, layer_path, image_data } => {
let message = DocumentOperation::ImaginateSetImageData { layer_path, image_data }.into();
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into());
}
PortfolioMessage::ImaginateSetServerStatus { status } => {
self.persistent_data.imaginate_server_status = status;
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
}
PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages // 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() { if self.active_document().is_some() {
responses.push_back(FrontendMessage::TriggerImport.into()); responses.push_back(FrontendMessage::TriggerImport.into());
} }
} }
LoadDocumentResources => { PortfolioMessage::LoadDocumentResources { document_id } => {
if let Some(document) = self.active_document_mut() { if let Some(document) = self.document_mut(document_id) {
document.load_layer_resources(responses, &document.graphene_document.root.data, Vec::new()); document.load_layer_resources(responses, &document.graphene_document.root.data, Vec::new(), document_id);
} }
} }
LoadFont { font, is_default } => { PortfolioMessage::LoadFont { font, is_default } => {
if !self.font_cache.loaded_font(&font) { if !self.persistent_data.font_cache.loaded_font(&font) {
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into()); responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into());
} }
} }
NewDocumentWithName { name } => { PortfolioMessage::NewDocumentWithName { name } => {
let new_document = DocumentMessageHandler::with_name(name, ipp); let new_document = DocumentMessageHandler::with_name(name, ipp);
let document_id = generate_uuid(); let document_id = generate_uuid();
if self.active_document().is_some() { if self.active_document().is_some() {
@@ -213,7 +261,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
self.load_document(new_document, document_id, responses); self.load_document(new_document, document_id, responses);
} }
NextDocument => { PortfolioMessage::NextDocument => {
if let Some(active_document_id) = self.active_document_id { if let Some(active_document_id) = self.active_document_id {
let current_index = self.document_index(active_document_id); let current_index = self.document_index(active_document_id);
let next_index = (current_index + 1) % self.document_ids.len(); let next_index = (current_index + 1) % self.document_ids.len();
@@ -222,11 +270,11 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into());
} }
} }
OpenDocument => { PortfolioMessage::OpenDocument => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages // 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()); responses.push_back(FrontendMessage::TriggerOpenDocument.into());
} }
OpenDocumentFile { PortfolioMessage::OpenDocumentFile {
document_name, document_name,
document_serialized_content, document_serialized_content,
} => { } => {
@@ -240,7 +288,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
.into(), .into(),
); );
} }
OpenDocumentFileWithId { PortfolioMessage::OpenDocumentFileWithId {
document_id, document_id,
document_name, document_name,
document_is_saved, document_is_saved,
@@ -261,7 +309,8 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
), ),
} }
} }
Paste { clipboard } => { // TODO: Paste message is unused, delete it?
PortfolioMessage::Paste { clipboard } => {
let shallowest_common_folder = self.active_document().map(|document| { let shallowest_common_folder = self.active_document().map(|document| {
document document
.graphene_document .graphene_document
@@ -270,20 +319,20 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
}); });
if let Some(folder) = shallowest_common_folder { if let Some(folder) = shallowest_common_folder {
responses.push_back(DeselectAllLayers.into()); responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(StartTransaction.into()); responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back( responses.push_back(
PasteIntoFolder { PortfolioMessage::PasteIntoFolder {
clipboard, clipboard,
folder_path: folder.to_vec(), folder_path: folder.to_vec(),
insert_index: -1, insert_index: -1,
} }
.into(), .into(),
); );
responses.push_back(CommitTransaction.into()); responses.push_back(DocumentMessage::CommitTransaction.into());
} }
} }
PasteIntoFolder { PortfolioMessage::PasteIntoFolder {
clipboard, clipboard,
folder_path: path, folder_path: path,
insert_index, insert_index,
@@ -300,10 +349,10 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
.into(), .into(),
); );
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone()); document.load_layer_resources(responses, &entry.layer.data, destination_path.clone(), self.active_document_id.unwrap());
responses.push_front( responses.push_front(
DocumentOperation::InsertLayer { DocumentOperation::InsertLayer {
layer: entry.layer.clone(), layer: Box::new(entry.layer.clone()),
destination_path, destination_path,
insert_index, insert_index,
} }
@@ -322,15 +371,15 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
} }
} }
PasteSerializedData { data } => { PortfolioMessage::PasteSerializedData { data } => {
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) { if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
let shallowest_common_folder = document let shallowest_common_folder = document
.graphene_document .graphene_document
.shallowest_common_folder(document.selected_layers()) .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"); .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(DocumentMessage::DeselectAllLayers.into());
responses.push_back(StartTransaction.into()); responses.push_back(DocumentMessage::StartTransaction.into());
for entry in data.iter().rev() { for entry in data.iter().rev() {
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat(); let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
@@ -342,10 +391,10 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
.into(), .into(),
); );
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone()); document.load_layer_resources(responses, &entry.layer.data, destination_path.clone(), self.active_document_id.unwrap());
responses.push_front( responses.push_front(
DocumentOperation::InsertLayer { DocumentOperation::InsertLayer {
layer: entry.layer.clone(), layer: Box::new(entry.layer.clone()),
destination_path, destination_path,
insert_index: -1, insert_index: -1,
} }
@@ -353,11 +402,11 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
); );
} }
responses.push_back(CommitTransaction.into()); responses.push_back(DocumentMessage::CommitTransaction.into());
} }
} }
} }
PrevDocument => { PortfolioMessage::PrevDocument => {
if let Some(active_document_id) = self.active_document_id { if let Some(active_document_id) = self.active_document_id {
let len = self.document_ids.len(); let len = self.document_ids.len();
let current_index = self.document_index(active_document_id); let current_index = self.document_index(active_document_id);
@@ -366,7 +415,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
} }
} }
SelectDocument { document_id } => { PortfolioMessage::SelectDocument { document_id } => {
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
if !document.is_saved() { if !document.is_saved() {
// Safe to unwrap since we know that there is an active document // Safe to unwrap since we know that there is an active document
@@ -384,10 +433,10 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
// TODO: Remove this message in favor of having tools have specific data per document instance // 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::SetActiveDocument { document_id }.into());
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into()); responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
responses.push_back(RenderDocument.into()); responses.push_back(DocumentMessage::RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.documents.get(&document_id).unwrap().layer_metadata.keys() { 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(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
@@ -397,13 +446,27 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
responses.push_back(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); responses.push_back(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
} }
SetActiveDocument { document_id } => self.active_document_id = Some(document_id), PortfolioMessage::SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
UpdateDocumentWidgets => { PortfolioMessage::SetImageBlobUrl {
document_id,
layer_path,
blob_url,
resolution,
} => {
let message = DocumentMessage::SetImageBlobUrl {
layer_path,
blob_url,
resolution,
document_id,
};
responses.push_back(PortfolioMessage::DocumentPassMessage { document_id, message }.into());
}
PortfolioMessage::UpdateDocumentWidgets => {
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
document.update_document_widgets(responses); document.update_document_widgets(responses);
} }
} }
UpdateOpenDocumentsList => { PortfolioMessage::UpdateOpenDocumentsList => {
// Send the list of document tab names // Send the list of document tab names
let open_documents = self let open_documents = self
.document_ids .document_ids
@@ -449,6 +512,14 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
impl PortfolioMessageHandler { impl PortfolioMessageHandler {
pub fn document(&self, document_id: u64) -> Option<&DocumentMessageHandler> {
self.documents.get(&document_id)
}
pub fn document_mut(&mut self, document_id: u64) -> Option<&mut DocumentMessageHandler> {
self.documents.get_mut(&document_id)
}
pub fn active_document(&self) -> Option<&DocumentMessageHandler> { pub fn active_document(&self) -> Option<&DocumentMessageHandler> {
self.active_document_id.and_then(|id| self.documents.get(&id)) self.active_document_id.and_then(|id| self.documents.get(&id))
} }
@@ -457,6 +528,10 @@ impl PortfolioMessageHandler {
self.active_document_id.and_then(|id| self.documents.get_mut(&id)) self.active_document_id.and_then(|id| self.documents.get_mut(&id))
} }
pub fn active_document_id(&self) -> Option<u64> {
self.active_document_id
}
pub fn generate_new_document_name(&self) -> String { pub fn generate_new_document_name(&self) -> String {
let mut doc_title_numbers = self let mut doc_title_numbers = self
.ordered_document_iterator() .ordered_document_iterator()
@@ -486,11 +561,11 @@ impl PortfolioMessageHandler {
new_document new_document
.layer_metadata .layer_metadata
.keys() .keys()
.filter_map(|path| new_document.layer_panel_entry_from_path(path, &self.font_cache)) .filter_map(|path| new_document.layer_panel_entry_from_path(path, &self.persistent_data.font_cache))
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into()) .map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache); new_document.update_layer_tree_options_bar_widgets(responses, &self.persistent_data.font_cache);
self.documents.insert(document_id, new_document); self.documents.insert(document_id, new_document);
@@ -502,12 +577,12 @@ impl PortfolioMessageHandler {
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
responses.push_back(PortfolioMessage::LoadDocumentResources.into()); responses.push_back(PortfolioMessage::LoadDocumentResources { document_id }.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
responses.push_back(ToolMessage::InitTools.into()); responses.push_back(ToolMessage::InitTools.into());
responses.push_back(PropertiesPanelMessage::Init.into()); responses.push_back(PropertiesPanelMessage::Init.into());
responses.push_back(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); responses.push_back(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()) responses.push_back(DocumentMessage::DocumentStructureChanged.into());
} }
/// Returns an iterator over the open documents in order. /// Returns an iterator over the open documents in order.
@@ -518,8 +593,4 @@ impl PortfolioMessageHandler {
fn document_index(&self, document_id: u64) -> usize { 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") 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,58 @@
use graphene::layers::text_layer::FontCache;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct PersistentData {
pub font_cache: FontCache,
pub imaginate_server_status: ImaginateServerStatus,
}
impl Default for PersistentData {
fn default() -> Self {
Self {
font_cache: Default::default(),
imaginate_server_status: ImaginateServerStatus::Unknown,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub enum ImaginateServerStatus {
#[default]
Unknown,
Checking,
Unavailable,
Connected,
}
#[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 => {
warn!("The platform has not been set, remember to send `GlobalsMessage::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,
}
+7
View File
@@ -0,0 +1,7 @@
mod preferences_message;
mod preferences_message_handler;
#[doc(inline)]
pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant};
#[doc(inline)]
pub use preferences_message_handler::PreferencesMessageHandler;
@@ -0,0 +1,13 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[impl_message(Message, Preferences)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PreferencesMessage {
Load { preferences: String },
ResetToDefaults,
ImaginateRefreshFrequency { seconds: f64 },
ImaginateServerHostname { hostname: String },
}
@@ -0,0 +1,72 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct PreferencesMessageHandler {
pub imaginate_server_hostname: String,
pub imaginate_refresh_frequency: f64,
}
impl Default for PreferencesMessageHandler {
fn default() -> Self {
Self {
imaginate_server_hostname: "http://localhost:7860/".into(),
imaginate_refresh_frequency: 1.,
}
}
}
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
#[remain::check]
fn process_message(&mut self, message: PreferencesMessage, _data: (), responses: &mut VecDeque<Message>) {
match message {
PreferencesMessage::Load { preferences } => {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences;
if self.imaginate_server_hostname != Self::default().imaginate_server_hostname {
responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into());
}
}
}
PreferencesMessage::ResetToDefaults => {
refresh_dialog(responses);
*self = Self::default()
}
PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
self.imaginate_refresh_frequency = seconds;
responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into());
}
PreferencesMessage::ImaginateServerHostname { hostname } => {
let initial = hostname.clone();
let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
if hostname != initial {
refresh_dialog(responses);
}
self.imaginate_server_hostname = hostname;
responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into());
}
}
responses.push_back(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }.into());
}
advertise_actions!(PreferencesMessageDiscriminant;
);
}
fn refresh_dialog(responses: &mut VecDeque<Message>) {
responses.push_back(
DialogMessage::CloseDialogAndThen {
followups: vec![DialogMessage::RequestPreferencesDialog.into()],
}
.into(),
);
}
+3
View File
@@ -6,6 +6,7 @@ pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscrimin
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler}; pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageDiscriminant, ExportDialogMessageHandler}; pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};
pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler}; pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler};
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
pub use crate::messages::dialog::{DialogMessage, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::dialog::{DialogMessage, DialogMessageDiscriminant, DialogMessageHandler};
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler}; pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler};
@@ -20,6 +21,7 @@ pub use crate::messages::portfolio::document::transform_layer::{TransformLayerMe
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageDiscriminant, ToolMessageHandler}; pub use crate::messages::tool::{ToolMessage, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler}; pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
@@ -32,6 +34,7 @@ pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMe
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant}; 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::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant}; 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::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
+6
View File
@@ -75,6 +75,9 @@ pub enum ToolMessage {
// #[remain::unsorted] // #[remain::unsorted]
// #[child] // #[child]
// Detail(DetailToolMessage), // Detail(DetailToolMessage),
#[remain::unsorted]
#[child]
Imaginate(ImaginateToolMessage),
// Messages // Messages
#[remain::unsorted] #[remain::unsorted]
@@ -109,6 +112,9 @@ pub enum ToolMessage {
#[remain::unsorted] #[remain::unsorted]
ActivateToolShape, ActivateToolShape,
#[remain::unsorted]
ActivateToolImaginate,
ActivateTool { ActivateTool {
tool_type: ToolType, tool_type: ToolType,
}, },
@@ -2,21 +2,25 @@ use super::utility_types::{tool_message_to_tool_type, ToolFsmState};
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder; use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use graphene::color::Color; use graphene::color::Color;
use graphene::layers::text_layer::FontCache;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ToolMessageHandler { pub struct ToolMessageHandler {
tool_state: ToolFsmState, tool_state: ToolFsmState,
} }
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache)> for ToolMessageHandler { impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocessorMessageHandler, &PersistentData)> for ToolMessageHandler {
#[remain::check] #[remain::check]
fn process_message(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) { fn process_message(
let (document, input, font_cache) = data; &mut self,
message: ToolMessage,
(document, document_id, input, persistent_data): (&DocumentMessageHandler, u64, &InputPreprocessorMessageHandler, &PersistentData),
responses: &mut VecDeque<Message>,
) {
#[remain::sorted] #[remain::sorted]
match message { match message {
// Messages // Messages
@@ -52,6 +56,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
#[remain::unsorted] #[remain::unsorted]
ToolMessage::ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()), ToolMessage::ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolImaginate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }.into()),
ToolMessage::ActivateTool { tool_type } => { ToolMessage::ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data; let document_data = &self.tool_state.document_tool_data;
@@ -66,12 +73,12 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| { 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) = tool_data.tools.get_mut(&tool_type) {
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort { 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); tool.process_message(tool_abort_message, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
} }
if update_hints_and_cursor { if update_hints_and_cursor {
tool.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses); tool.process_message(ToolMessage::UpdateHints, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
tool.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses); tool.process_message(ToolMessage::UpdateCursor, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
} }
} }
}; };
@@ -126,10 +133,10 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
// Set initial hints and cursor // Set initial hints and cursor
tool_data tool_data
.active_tool_mut() .active_tool_mut()
.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses); .process_message(ToolMessage::UpdateHints, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
tool_data tool_data
.active_tool_mut() .active_tool_mut()
.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses); .process_message(ToolMessage::UpdateCursor, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
} }
ToolMessage::ResetColors => { ToolMessage::ResetColors => {
let document_data = &mut self.tool_state.document_tool_data; let document_data = &mut self.tool_state.document_tool_data;
@@ -184,7 +191,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
if let Some(tool) = tool_data.tools.get_mut(&tool_type) { if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
if tool_type == tool_data.active_tool_type { if tool_type == tool_data.active_tool_type {
tool.process_message(tool_message, (document, document_data, input, font_cache), responses); tool.process_message(tool_message, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
} }
} }
} }
@@ -200,6 +207,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
ActivateToolText, ActivateToolText,
ActivateToolFill, ActivateToolFill,
ActivateToolGradient, ActivateToolGradient,
ActivateToolPath, ActivateToolPath,
ActivateToolPen, ActivateToolPen,
ActivateToolFreehand, ActivateToolFreehand,
@@ -208,6 +216,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
ActivateToolRectangle, ActivateToolRectangle,
ActivateToolEllipse, ActivateToolEllipse,
ActivateToolShape, ActivateToolShape,
ActivateToolImaginate,
SelectRandomPrimaryColor, SelectRandomPrimaryColor,
ResetColors, ResetColors,
SwapColors, SwapColors,
@@ -132,7 +132,7 @@ impl Fsm for ArtboardToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -122,7 +122,7 @@ impl Fsm for EllipseToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -104,7 +104,7 @@ impl Fsm for EyedropperToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
_tool_data: &mut Self::ToolData, _tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -105,7 +105,7 @@ impl Fsm for FillToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
_tool_data: &mut Self::ToolData, _tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -161,7 +161,7 @@ impl Fsm for FreehandToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, _font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, _font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -335,7 +335,7 @@ impl Fsm for GradientToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -0,0 +1,231 @@
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::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct ImaginateTool {
fsm_state: ImaginateToolFsmState,
tool_data: ImaginateToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Imaginate)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ImaginateToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize {
center: Key,
lock_ratio: Key,
},
}
impl PropertyHolder for ImaginateTool {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ImaginateTool {
fn process_message(&mut self, message: ToolMessage, tool_data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if message == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if message == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
fn actions(&self) -> ActionList {
use ImaginateToolFsmState::*;
match self.fsm_state {
Ready => actions!(ImaginateToolMessageDiscriminant;
DragStart,
),
Drawing => actions!(ImaginateToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolMetadata for ImaginateTool {
fn icon_name(&self) -> String {
"RasterImaginateTool".into()
}
fn tooltip(&self) -> String {
"Imaginate Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Imaginate
}
}
impl ToolTransition for ImaginateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(ImaginateToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ImaginateToolFsmState {
Ready,
Drawing,
}
impl Default for ImaginateToolFsmState {
fn default() -> Self {
ImaginateToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct ImaginateToolData {
data: Resize,
}
impl Fsm for ImaginateToolFsmState {
type ToolData = ImaginateToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
(document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use ImaginateToolFsmState::*;
use ImaginateToolMessage::*;
let mut shape_data = &mut tool_data.data;
if let ToolMessage::Imaginate(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::AddImaginateFrame {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
}
.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 {
ImaginateToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Repaint Frame"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Shift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Alt])],
key_groups_mac: None,
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
ImaginateToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Shift])],
key_groups_mac: None,
mouse: None,
label: String::from("Constrain Square"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::Alt])],
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());
}
}
@@ -170,7 +170,7 @@ impl Fsm for LineToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -4,6 +4,7 @@ pub mod eyedropper_tool;
pub mod fill_tool; pub mod fill_tool;
pub mod freehand_tool; pub mod freehand_tool;
pub mod gradient_tool; pub mod gradient_tool;
pub mod imaginate_tool;
pub mod line_tool; pub mod line_tool;
pub mod navigate_tool; pub mod navigate_tool;
pub mod path_tool; pub mod path_tool;
@@ -126,7 +126,7 @@ impl Fsm for NavigateToolFsmState {
self, self,
message: ToolMessage, message: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(_document, _global_tool_data, input, _font_cache): ToolActionHandlerData, (_document, _document_id, _global_tool_data, input, _font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
messages: &mut VecDeque<Message>, messages: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -142,7 +142,7 @@ impl Fsm for PathToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -182,7 +182,7 @@ impl Fsm for PenToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -121,7 +121,7 @@ impl Fsm for RectangleToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -1,6 +1,6 @@
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::frontend::utility_types::{FrontendImageData, MouseCursorIcon};
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion}; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
@@ -374,7 +374,7 @@ impl Fsm for SelectToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, _global_tool_data, input, font_cache): ToolActionHandlerData, (document, document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -550,12 +550,6 @@ impl Fsm for SelectToolFsmState {
.flat_map(snapping::expand_bounds) .flat_map(snapping::expand_bounds)
.collect(); .collect();
if input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_none() {
tool_data.start_duplicates(document, responses);
} else if !input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_some() {
tool_data.stop_duplicates(responses);
}
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta); let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for path in Document::shallowest_unique_layers(tool_data.layers_dragging.iter()) { for path in Document::shallowest_unique_layers(tool_data.layers_dragging.iter()) {
@@ -568,6 +562,13 @@ impl Fsm for SelectToolFsmState {
); );
} }
tool_data.drag_current = mouse_position + closest_move; tool_data.drag_current = mouse_position + closest_move;
if input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_none() {
tool_data.start_duplicates(document, document_id, responses);
} else if !input.keyboard.get(duplicate as usize) && tool_data.not_duplicated_layers.is_some() {
tool_data.stop_duplicates(responses);
}
Dragging Dragging
} }
(ResizingBounds, PointerMove { axis_align, center, .. }) => { (ResizingBounds, PointerMove { axis_align, center, .. }) => {
@@ -919,8 +920,8 @@ impl Fsm for SelectToolFsmState {
} }
impl SelectToolData { impl SelectToolData {
/// Duplicates the currently dragging layers. Called when alt is pressed and the layers have not yet been duplicated. /// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated.
fn start_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { fn start_duplicates(&mut self, document: &DocumentMessageHandler, document_id: u64, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeselectAllLayers.into()); responses.push_back(DocumentMessage::DeselectAllLayers.into());
self.not_duplicated_layers = Some(self.layers_dragging.clone()); self.not_duplicated_layers = Some(self.layers_dragging.clone());
@@ -938,19 +939,34 @@ impl SelectToolData {
// Copy the layers. // Copy the layers.
// Not using the Copy message allows us to retrieve the ids of the new layers to initialize the drag. // Not using the Copy message allows us to retrieve the ids of the new layers to initialize the drag.
let layer = match document.graphene_document.layer(layer_path) { let mut layer = match document.graphene_document.layer(layer_path) {
Ok(layer) => layer.clone(), Ok(layer) => layer.clone(),
Err(e) => { Err(e) => {
warn!("Could not access selected layer {:?}: {:?}", layer_path, e); warn!("Could not access selected layer {:?}: {:?}", layer_path, e);
continue; continue;
} }
}; };
let layer_metadata = *document.layer_metadata(layer_path); let layer_metadata = *document.layer_metadata(layer_path);
*layer_path.last_mut().unwrap() = generate_uuid(); *layer_path.last_mut().unwrap() = generate_uuid();
let image_data = if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.blob_url = None;
imaginate.image_data.as_ref().map(|data| {
vec![FrontendImageData {
path: layer_path.clone(),
image_data: data.image_data.clone(),
mime: imaginate.mime.clone(),
}]
})
} else {
None
};
responses.push_back( responses.push_back(
Operation::InsertLayer { Operation::InsertLayer {
layer, layer: Box::new(layer),
destination_path: layer_path.clone(), destination_path: layer_path.clone(),
insert_index: -1, insert_index: -1,
} }
@@ -964,10 +980,14 @@ impl SelectToolData {
} }
.into(), .into(),
); );
if let Some(image_data) = image_data {
responses.push_back(FrontendMessage::UpdateImageData { image_data, document_id }.into());
}
} }
} }
/// Removes the duplicated layers. Called when alt is released and the layers have been duplicated. /// Removes the duplicated layers. Called when Alt is released and the layers have been duplicated.
fn stop_duplicates(&mut self, responses: &mut VecDeque<Message>) { fn stop_duplicates(&mut self, responses: &mut VecDeque<Message>) {
let originals = match self.not_duplicated_layers.take() { let originals = match self.not_duplicated_layers.take() {
Some(x) => x, Some(x) => x,
@@ -162,7 +162,7 @@ impl Fsm for ShapeToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -170,7 +170,7 @@ impl Fsm for SplineToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -274,7 +274,7 @@ impl Fsm for TextToolFsmState {
self, self,
event: ToolMessage, event: ToolMessage,
tool_data: &mut Self::ToolData, tool_data: &mut Self::ToolData,
(document, global_tool_data, input, font_cache): ToolActionHandlerData, (document, _document_id, global_tool_data, input, font_cache): ToolActionHandlerData,
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
@@ -302,7 +302,7 @@ impl Fsm for TextToolFsmState {
{ {
if state == TextToolFsmState::Editing { if state == TextToolFsmState::Editing {
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: false, editable: false,
} }
@@ -313,7 +313,7 @@ impl Fsm for TextToolFsmState {
tool_data.path = l.clone(); tool_data.path = l.clone();
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: true, editable: true,
} }
@@ -358,7 +358,7 @@ impl Fsm for TextToolFsmState {
); );
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: true, editable: true,
} }
@@ -376,7 +376,7 @@ impl Fsm for TextToolFsmState {
} else { } else {
// Removing old text as editable // Removing old text as editable
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: false, editable: false,
} }
@@ -393,7 +393,7 @@ impl Fsm for TextToolFsmState {
(state, Abort) => { (state, Abort) => {
if state == TextToolFsmState::Editing { if state == TextToolFsmState::Editing {
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: false, editable: false,
} }
@@ -420,7 +420,7 @@ impl Fsm for TextToolFsmState {
); );
responses.push_back( responses.push_back(
DocumentMessage::SetTexboxEditability { DocumentMessage::SetTextboxEditability {
path: tool_data.path.clone(), path: tool_data.path.clone(),
editable: false, editable: false,
} }
+85 -66
View File
@@ -16,7 +16,7 @@ use graphene::layers::text_layer::FontCache;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentToolData, &'a InputPreprocessorMessageHandler, &'a FontCache); pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, u64, &'a DocumentToolData, &'a InputPreprocessorMessageHandler, &'a FontCache);
pub trait ToolCommon: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {} pub trait ToolCommon: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {}
impl<T> ToolCommon for T where T: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {} impl<T> ToolCommon for T where T: for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> + PropertyHolder + ToolTransition + ToolMetadata {}
@@ -161,13 +161,17 @@ impl PropertyHolder for ToolData {
fn properties(&self) -> Layout { fn properties(&self) -> Layout {
let tool_groups_layout = list_tools_in_groups() let tool_groups_layout = list_tools_in_groups()
.iter() .iter()
.map(|tool_group| tool_group.iter().map(|tool| ToolEntry { .map(|tool_group| tool_group.iter().map(|tool_availability| {
tooltip: tool.tooltip(), match tool_availability {
tooltip_shortcut: action_keys!(tool_type_to_activate_tool_message(tool.tool_type())), ToolAvailability::Available(tool) => ToolEntry {
icon_name: tool.icon_name(), tooltip: tool.tooltip(),
tool_type: tool.tool_type(), tooltip_shortcut: action_keys!(tool_type_to_activate_tool_message(tool.tool_type())),
icon_name: tool.icon_name(),
tool_type: tool.tool_type(),
},
ToolAvailability::ComingSoon(tool) => tool.clone(),
}
}).collect::<Vec<_>>()) }).collect::<Vec<_>>())
.chain(coming_soon_tools())
.flat_map(|group| { .flat_map(|group| {
let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator { let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Vertical, direction: SeparatorDirection::Vertical,
@@ -189,6 +193,7 @@ impl PropertyHolder for ToolData {
}), }),
})) }))
}); });
separator.chain(buttons) separator.chain(buttons)
}) })
// Skip the initial separator // Skip the initial separator
@@ -201,7 +206,7 @@ impl PropertyHolder for ToolData {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct ToolEntry { pub struct ToolEntry {
pub tooltip: String, pub tooltip: String,
pub tooltip_shortcut: Option<ActionKeys>, pub tooltip_shortcut: Option<ActionKeys>,
@@ -220,7 +225,14 @@ impl Default for ToolFsmState {
ToolFsmState { ToolFsmState {
tool_data: ToolData { tool_data: ToolData {
active_tool_type: ToolType::Select, active_tool_type: ToolType::Select,
tools: list_tools_in_groups().into_iter().flatten().map(|tool| (tool.tool_type(), tool)).collect(), tools: list_tools_in_groups()
.into_iter()
.flatten()
.filter_map(|tool| match tool {
ToolAvailability::Available(tool) => Some((tool.tool_type(), tool)),
ToolAvailability::ComingSoon(_) => None,
})
.collect(),
}, },
document_tool_data: DocumentToolData { document_tool_data: DocumentToolData {
primary_color: Color::BLACK, primary_color: Color::BLACK,
@@ -265,76 +277,81 @@ pub enum ToolType {
Patch, Patch,
Detail, Detail,
Relight, Relight,
Imaginate,
}
enum ToolAvailability {
Available(Box<Tool>),
ComingSoon(ToolEntry),
} }
/// List of all the tools in their conventional ordering and grouping. /// List of all the tools in their conventional ordering and grouping.
pub fn list_tools_in_groups() -> Vec<Vec<Box<Tool>>> { fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
vec![ vec![
vec![ vec![
// General tool group // General tool group
Box::new(select_tool::SelectTool::default()), ToolAvailability::Available(Box::new(select_tool::SelectTool::default())),
Box::new(artboard_tool::ArtboardTool::default()), ToolAvailability::Available(Box::new(artboard_tool::ArtboardTool::default())),
Box::new(navigate_tool::NavigateTool::default()), ToolAvailability::Available(Box::new(navigate_tool::NavigateTool::default())),
Box::new(eyedropper_tool::EyedropperTool::default()), ToolAvailability::Available(Box::new(eyedropper_tool::EyedropperTool::default())),
Box::new(fill_tool::FillTool::default()), ToolAvailability::Available(Box::new(fill_tool::FillTool::default())),
Box::new(gradient_tool::GradientTool::default()), ToolAvailability::Available(Box::new(gradient_tool::GradientTool::default())),
], ],
vec![ vec![
// Vector tool group // Vector tool group
Box::new(path_tool::PathTool::default()), ToolAvailability::Available(Box::new(path_tool::PathTool::default())),
Box::new(pen_tool::PenTool::default()), ToolAvailability::Available(Box::new(pen_tool::PenTool::default())),
Box::new(freehand_tool::FreehandTool::default()), ToolAvailability::Available(Box::new(freehand_tool::FreehandTool::default())),
Box::new(spline_tool::SplineTool::default()), ToolAvailability::Available(Box::new(spline_tool::SplineTool::default())),
Box::new(line_tool::LineTool::default()), ToolAvailability::Available(Box::new(line_tool::LineTool::default())),
Box::new(rectangle_tool::RectangleTool::default()), ToolAvailability::Available(Box::new(rectangle_tool::RectangleTool::default())),
Box::new(ellipse_tool::EllipseTool::default()), ToolAvailability::Available(Box::new(ellipse_tool::EllipseTool::default())),
Box::new(shape_tool::ShapeTool::default()), ToolAvailability::Available(Box::new(shape_tool::ShapeTool::default())),
Box::new(text_tool::TextTool::default()), ToolAvailability::Available(Box::new(text_tool::TextTool::default())),
],
vec![
// Raster tool group
ToolAvailability::Available(Box::new(imaginate_tool::ImaginateTool::default())),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(),
tooltip: "Coming Soon: Brush Tool (B)".into(),
tooltip_shortcut: None,
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Heal,
icon_name: "RasterHealTool".into(),
tooltip: "Coming Soon: Heal Tool (J)".into(),
tooltip_shortcut: None,
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Clone,
icon_name: "RasterCloneTool".into(),
tooltip: "Coming Soon: Clone Tool (C)".into(),
tooltip_shortcut: None,
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Patch,
icon_name: "RasterPatchTool".into(),
tooltip: "Coming Soon: Patch Tool".into(),
tooltip_shortcut: None,
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Detail,
icon_name: "RasterDetailTool".into(),
tooltip: "Coming Soon: Detail Tool (D)".into(),
tooltip_shortcut: None,
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Relight,
icon_name: "RasterRelightTool".into(),
tooltip: "Coming Soon: Relight Tool (O)".into(),
tooltip_shortcut: None,
}),
], ],
] ]
} }
pub fn coming_soon_tools() -> Vec<Vec<ToolEntry>> {
vec![vec![
ToolEntry {
tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(),
tooltip: "Coming Soon: Brush Tool (B)".into(),
tooltip_shortcut: None,
},
ToolEntry {
tool_type: ToolType::Heal,
icon_name: "RasterHealTool".into(),
tooltip: "Coming Soon: Heal Tool (J)".into(),
tooltip_shortcut: None,
},
ToolEntry {
tool_type: ToolType::Clone,
icon_name: "RasterCloneTool".into(),
tooltip: "Coming Soon: Clone Tool (C)".into(),
tooltip_shortcut: None,
},
ToolEntry {
tool_type: ToolType::Patch,
icon_name: "RasterPatchTool".into(),
tooltip: "Coming Soon: Patch Tool".into(),
tooltip_shortcut: None,
},
ToolEntry {
tool_type: ToolType::Detail,
icon_name: "RasterDetailTool".into(),
tooltip: "Coming Soon: Detail Tool (D)".into(),
tooltip_shortcut: None,
},
ToolEntry {
tool_type: ToolType::Relight,
icon_name: "RasterRelightTool".into(),
tooltip: "Coming Soon: Relight Tool (O)".into(),
tooltip_shortcut: None,
},
]]
}
pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
match tool_message { match tool_message {
// General tool group // General tool group
@@ -363,6 +380,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
// ToolMessage::Patch(_) => ToolType::Patch, // ToolMessage::Patch(_) => ToolType::Patch,
// ToolMessage::Detail(_) => ToolType::Detail, // ToolMessage::Detail(_) => ToolType::Detail,
// ToolMessage::Relight(_) => ToolType::Relight, // ToolMessage::Relight(_) => ToolType::Relight,
ToolMessage::Imaginate(_) => ToolType::Imaginate,
_ => panic!( _ => panic!(
"Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}", "Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
tool_message tool_message
@@ -398,6 +416,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch, // ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail, // ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight, // ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
_ => panic!( _ => panic!(
"Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}", "Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}",
tool_type tool_type
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::application::set_uuid_seed;
use crate::application::Editor; use crate::application::Editor;
use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition}; use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition};
use crate::messages::portfolio::document::utility_types::misc::Platform; use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
@@ -1,11 +1,11 @@
<svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> <svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<path fill="#ffffff" d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" /> <path d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
<path fill="#ffffff" d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" /> <path d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
<path fill="#ffffff" d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" /> <path d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
<path fill="#ffffff" d="M732.15,154.68c-0.64-2.97-0.91-6-0.81-9.03v-39.22c0-7.05-1.57-12.18-4.72-15.38c-3.15-3.2-8.08-4.8-14.81-4.8c-4.06,0.01-8.07,0.84-11.8,2.44c-3.08,1.21-6.03,2.75-8.79,4.57c-3.07,2.01-5.99,4.25-8.71,6.72V61.55c0-5.21,0.08-9.52,0.24-12.94c0.16-3.42,0.3-5.94,0.41-7.57L682.11,40l-35.45,16.42l1.66,3.82c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.32,2.85-0.33c0.96-0.07,1.92,0.22,2.68,0.81c0.6,0.55,0.9,1.63,0.9,3.26v82.63c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99c-0.64-2.97-0.91-6-0.82-9.03v-37.92c2.72-1.87,5.71-3.29,8.87-4.23c2.26-0.61,4.58-0.94,6.92-0.98c3.79,0,6.18,1,7.16,3.01c1.06,2.43,1.56,5.08,1.46,7.73v32.39c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99" /> <path d="M732.15,154.68c-0.64-2.97-0.91-6-0.81-9.03v-39.22c0-7.05-1.57-12.18-4.72-15.38c-3.15-3.2-8.08-4.8-14.81-4.8c-4.06,0.01-8.07,0.84-11.8,2.44c-3.08,1.21-6.03,2.75-8.79,4.57c-3.07,2.01-5.99,4.25-8.71,6.72V61.55c0-5.21,0.08-9.52,0.24-12.94c0.16-3.42,0.3-5.94,0.41-7.57L682.11,40l-35.45,16.42l1.66,3.82c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.32,2.85-0.33c0.96-0.07,1.92,0.22,2.68,0.81c0.6,0.55,0.9,1.63,0.9,3.26v82.63c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99c-0.64-2.97-0.91-6-0.82-9.03v-37.92c2.72-1.87,5.71-3.29,8.87-4.23c2.26-0.61,4.58-0.94,6.92-0.98c3.79,0,6.18,1,7.16,3.01c1.06,2.43,1.56,5.08,1.46,7.73v32.39c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99" />
<path fill="#ffffff" d="M624.97,90.71c-4.3-2.92-9.37-4.48-14.57-4.48c-5.74-0.16-11.38,1.43-16.19,4.56c-4.26,2.76-7.67,6.65-9.85,11.23h-0.32c0-3.26,0.12-6.35,0.39-9.49c0.14-2.07,0.38-4.14,0.73-6.18l-0.98-0.98l-33.84,15.68l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.81c0.6,0.54,0.9,1.63,0.9,3.25v73.9c0.08,3.02-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.72-2.86,1.11-4.39,1.14V200h43.12v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.96-2.18-3.33-3.99c-0.64-2.97-0.91-6-0.81-9.03v-16.76c1.52,0.22,3.17,0.38,4.96,0.49s3.77,0.16,5.94,0.16c5.18-0.03,10.33-0.8,15.3-2.28c5.21-1.52,10.1-4.01,14.4-7.32c4.5-3.5,8.15-7.98,10.66-13.1c2.71-5.37,4.07-11.96,4.07-19.78c0-7.81-1.36-14.49-4.07-20.02C633.4,98.33,629.66,93.92,624.97,90.71 M608.94,150.61c-3.26,5.04-7.27,7.57-12.04,7.57c-5.21,0-9.33-2.39-12.37-7.16v-43.3c1.7-1.75,3.75-3.11,6.02-3.99c2.03-0.79,4.18-1.2,6.35-1.22c4.77,0,8.79,2.31,12.04,6.92c3.26,4.61,4.88,11.64,4.88,21.08C613.82,138.86,612.19,145.57,608.94,150.61" /> <path d="M624.97,90.71c-4.3-2.92-9.37-4.48-14.57-4.48c-5.74-0.16-11.38,1.43-16.19,4.56c-4.26,2.76-7.67,6.65-9.85,11.23h-0.32c0-3.26,0.12-6.35,0.39-9.49c0.14-2.07,0.38-4.14,0.73-6.18l-0.98-0.98l-33.84,15.68l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.81c0.6,0.54,0.9,1.63,0.9,3.25v73.9c0.08,3.02-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.72-2.86,1.11-4.39,1.14V200h43.12v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.96-2.18-3.33-3.99c-0.64-2.97-0.91-6-0.81-9.03v-16.76c1.52,0.22,3.17,0.38,4.96,0.49s3.77,0.16,5.94,0.16c5.18-0.03,10.33-0.8,15.3-2.28c5.21-1.52,10.1-4.01,14.4-7.32c4.5-3.5,8.15-7.98,10.66-13.1c2.71-5.37,4.07-11.96,4.07-19.78c0-7.81-1.36-14.49-4.07-20.02C633.4,98.33,629.66,93.92,624.97,90.71 M608.94,150.61c-3.26,5.04-7.27,7.57-12.04,7.57c-5.21,0-9.33-2.39-12.37-7.16v-43.3c1.7-1.75,3.75-3.11,6.02-3.99c2.03-0.79,4.18-1.2,6.35-1.22c4.77,0,8.79,2.31,12.04,6.92c3.26,4.61,4.88,11.64,4.88,21.08C613.82,138.86,612.19,145.57,608.94,150.61" />
<path fill="#ffffff" d="M541.31,150.61c-1.17,0.45-2.41,0.7-3.66,0.73c-1.95,0-3.25-0.68-3.91-2.03c-0.74-1.83-1.07-3.81-0.98-5.78v-35.48c0-12.25-7.16-19.5-19.95-21.8c-8.97-1.62-19.39-1.04-28.28,0.57c-5.06,0.92-10.37,2.79-13.57,5.49v23.95h3.71c0.91-5.48,3.36-10.58,7.07-14.72c3.2-3.81,7.96-5.97,12.94-5.86c3.8,0,6.75,1.11,8.87,3.34c2.12,2.23,3.17,5.89,3.17,10.99v8.63c-13.78,3.69-23.95,7.76-30.52,12.21s-9.85,10.25-9.85,17.42c-0.06,4.5,1.47,8.88,4.31,12.36c2.87,3.58,7.29,5.37,13.27,5.37c4.5-0.01,8.92-1.16,12.86-3.34c4.18-2.27,7.62-5.69,9.93-9.85h0.33c0.95,3.66,3.1,6.9,6.1,9.2c2.87,2.12,6.97,3.17,12.29,3.17c4.71,0.08,9.34-1.19,13.35-3.66c4.15-2.73,7.43-6.6,9.44-11.15l-2.6-2.6C544.39,148.99,542.93,149.96,541.31,150.61 M506.73,146.3c-1.27,1.36-2.72,2.54-4.31,3.5c-1.74,1.05-3.75,1.58-5.78,1.54c-2.11,0.12-4.16-0.75-5.53-2.36c-1.32-1.63-2.02-3.68-1.95-5.78c0.09-1.95,0.5-3.88,1.22-5.7c1.09-2.66,2.82-5.01,5.05-6.84c2.55-2.28,6.32-4.12,11.31-5.53L506.73,146.3z" /> <path d="M541.31,150.61c-1.17,0.45-2.41,0.7-3.66,0.73c-1.95,0-3.25-0.68-3.91-2.03c-0.74-1.83-1.07-3.81-0.98-5.78v-35.48c0-12.25-7.16-19.5-19.95-21.8c-8.97-1.62-19.39-1.04-28.28,0.57c-5.06,0.92-10.37,2.79-13.57,5.49v23.95h3.71c0.91-5.48,3.36-10.58,7.07-14.72c3.2-3.81,7.96-5.97,12.94-5.86c3.8,0,6.75,1.11,8.87,3.34c2.12,2.23,3.17,5.89,3.17,10.99v8.63c-13.78,3.69-23.95,7.76-30.52,12.21s-9.85,10.25-9.85,17.42c-0.06,4.5,1.47,8.88,4.31,12.36c2.87,3.58,7.29,5.37,13.27,5.37c4.5-0.01,8.92-1.16,12.86-3.34c4.18-2.27,7.62-5.69,9.93-9.85h0.33c0.95,3.66,3.1,6.9,6.1,9.2c2.87,2.12,6.97,3.17,12.29,3.17c4.71,0.08,9.34-1.19,13.35-3.66c4.15-2.73,7.43-6.6,9.44-11.15l-2.6-2.6C544.39,148.99,542.93,149.96,541.31,150.61 M506.73,146.3c-1.27,1.36-2.72,2.54-4.31,3.5c-1.74,1.05-3.75,1.58-5.78,1.54c-2.11,0.12-4.16-0.75-5.53-2.36c-1.32-1.63-2.02-3.68-1.95-5.78c0.09-1.95,0.5-3.88,1.22-5.7c1.09-2.66,2.82-5.01,5.05-6.84c2.55-2.28,6.32-4.12,11.31-5.53L506.73,146.3z" />
<path fill="#ffffff" d="M440.68,91.63c-4.8,1.93-9.07,4.75-11.91,9.87h-0.33c-0.02-2.98,0.11-5.96,0.41-8.92c0.13-2.13,0.37-4.25,0.73-6.35l-0.98-0.98l-33.85,15.79l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.82c0.59,0.54,0.89,1.63,0.9,3.25v37.44c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h43.13v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.97-2.18-3.34-3.99c-0.64-2.97-0.91-6-0.82-9.03v-36.29c2.1-1.79,4.53-3.15,7.16-3.99c2.49-0.72,5.06-1.08,7.65-1.06c2.42,0.01,4.78,0.68,6.84,1.95c2.17,1.3,3.71,5.12,4.48,10h4.1V92.3C455.3,89.03,446.61,89.25,440.68,91.63" /> <path d="M440.68,91.63c-4.8,1.93-9.07,4.75-11.91,9.87h-0.33c-0.02-2.98,0.11-5.96,0.41-8.92c0.13-2.13,0.37-4.25,0.73-6.35l-0.98-0.98l-33.85,15.79l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.82c0.59,0.54,0.89,1.63,0.9,3.25v37.44c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h43.13v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.97-2.18-3.34-3.99c-0.64-2.97-0.91-6-0.82-9.03v-36.29c2.1-1.79,4.53-3.15,7.16-3.99c2.49-0.72,5.06-1.08,7.65-1.06c2.42,0.01,4.78,0.68,6.84,1.95c2.17,1.3,3.71,5.12,4.48,10h4.1V92.3C455.3,89.03,446.61,89.25,440.68,91.63" />
<path fill="#ffffff" d="M344.13,115.53c2.68,0.05,5.32,0.57,7.81,1.55c1.73,0.81,2.9,2.6,3.5,5.37c0.72,4.38,1.02,8.82,0.9,13.26c0,3.8-0.04,6.29-0.2,9.22c-0.16,2.93-0.39,4.51-1.58,6.47c-1.63,2.71-4.43,4-7.41,4.59c-2.7,0.57-5.46,0.87-8.22,0.9c-6.29,0-12.7-1.98-16.81-5.14c-5.27-4.05-9.38-11.35-12.04-19.92c-2.4-8.27-3.58-16.84-3.51-25.45c0-14.54,4.01-24.17,9.38-31.43c5.46-7.37,14.61-11.25,25-11.89c4.13-0.21,8.27,0.21,12.28,1.25c3.63,1.12,7.4,2.65,10.43,6.07c3.03,3.42,4.67,7.11,6.85,13.4h3.74v-24.9c-4.86-1.84-9.87-3.25-14.97-4.23c-5.73-1.18-11.56-1.78-17.41-1.79c-8.11-0.06-16.17,1.23-23.85,3.82c-7.23,2.44-13.91,6.25-19.69,11.23c-5.77,5.04-10.36,11.29-13.43,18.31c-3.38,7.91-5.05,16.46-4.88,25.07c0,10.96,2.39,20.57,7.16,28.81c4.6,8.07,11.36,14.7,19.53,19.12c8.5,4.57,18.02,6.89,27.67,6.76c7.53,0.11,15.02-0.97,22.22-3.18c5.71-1.74,11.2-4.14,16.36-7.16c3.26-1.87,6.32-4.08,9.11-6.59c-0.63-2.67-1.01-5.4-1.14-8.14c-0.11-2.61-0.16-5.37-0.16-8.3v-9.44c0-2.82,0.3-4.77,0.9-5.86c0.66-1.12,1.87-1.81,3.17-1.79v-3.74h-40.7V115.53z" /> <path d="M344.13,115.53c2.68,0.05,5.32,0.57,7.81,1.55c1.73,0.81,2.9,2.6,3.5,5.37c0.72,4.38,1.02,8.82,0.9,13.26c0,3.8-0.04,6.29-0.2,9.22c-0.16,2.93-0.39,4.51-1.58,6.47c-1.63,2.71-4.43,4-7.41,4.59c-2.7,0.57-5.46,0.87-8.22,0.9c-6.29,0-12.7-1.98-16.81-5.14c-5.27-4.05-9.38-11.35-12.04-19.92c-2.4-8.27-3.58-16.84-3.51-25.45c0-14.54,4.01-24.17,9.38-31.43c5.46-7.37,14.61-11.25,25-11.89c4.13-0.21,8.27,0.21,12.28,1.25c3.63,1.12,7.4,2.65,10.43,6.07c3.03,3.42,4.67,7.11,6.85,13.4h3.74v-24.9c-4.86-1.84-9.87-3.25-14.97-4.23c-5.73-1.18-11.56-1.78-17.41-1.79c-8.11-0.06-16.17,1.23-23.85,3.82c-7.23,2.44-13.91,6.25-19.69,11.23c-5.77,5.04-10.36,11.29-13.43,18.31c-3.38,7.91-5.05,16.46-4.88,25.07c0,10.96,2.39,20.57,7.16,28.81c4.6,8.07,11.36,14.7,19.53,19.12c8.5,4.57,18.02,6.89,27.67,6.76c7.53,0.11,15.02-0.97,22.22-3.18c5.71-1.74,11.2-4.14,16.36-7.16c3.26-1.87,6.32-4.08,9.11-6.59c-0.63-2.67-1.01-5.4-1.14-8.14c-0.11-2.61-0.16-5.37-0.16-8.3v-9.44c0-2.82,0.3-4.77,0.9-5.86c0.66-1.12,1.87-1.81,3.17-1.79v-3.74h-40.7V115.53z" />
<path fill="#ffffff" d="M231.18,218.98l-0.07-0.69c-0.86-9.39-11.15-121.38-11.18-121.86c-0.23-2.84-1.07-5.6-2.45-8.09c-0.03-0.09-0.07-0.17-0.11-0.25l-0.06-0.15l-0.03,0.03l-0.02-0.01l0.04-0.02L205.5,67.5L172.31,10c-3.58-6.19-10.18-10-17.33-10H64.99c-7.14,0-13.74,3.81-17.32,10l-45,77.93c-3.57,6.19-3.57,13.81,0,20l45,77.93c3.57,6.19,10.17,10,17.32,10h89.99c3.86-0.03,7.63-1.19,10.85-3.32l38.59,27.68c-6.97-2.18-14.18-3.47-21.47-3.83c-18.11-0.87-71.2-0.28-131.42,4.63c-24.71,2.01-36.39,7.88-35.03,9.03c3.49,2.98,7.62,4.16,28.2,4.08c18.32-0.06,71.65,1.91,87.76,2.9c11.41,0.71,23.41,2.88,32.04,2.97c9.2-0.12,18.37-0.82,27.48-2.1c13.74-1.89,31.96-5.7,36.15-10.77C230.34,225.03,231.47,222.02,231.18,218.98z M62.49,24.32c1.67-2.55,4.45-4.16,7.5-4.33h79.99c3.04,0.17,5.81,1.77,7.49,4.31l33.26,57.61c-4.99,5.2-9.32,11-12.89,17.26l-24.77,2.75L138.3,122c-7.21-0.04-14.4,0.82-21.4,2.54L60.77,27.31L62.49,24.32z M69.99,175.86c-3.05-0.17-5.83-1.78-7.5-4.33l-40-69.27c-1.37-2.72-1.37-5.94,0-8.66l26.73-46.28l59.6,103.24l0.04-0.02c0.69,1.24,1.64,2.31,2.79,3.15l30.93,22.18L69.99,175.86z M186.75,182.93l-57.9-41.53c6.39-1.39,12.91-2.09,19.45-2.09l14.77-20.07l24.77-2.75c3.26-5.66,7.13-10.95,11.52-15.79l7.03,70.9C198.07,170.71,190.13,175.29,186.75,182.93z M81.64,154.71c1.49,2.33,0.8,5.42-1.52,6.91c-2.33,1.49-5.42,0.8-6.91-1.52c-0.08-0.12-0.15-0.25-0.22-0.38l-35-60.61c-1.49-2.33-0.8-5.42,1.52-6.91c2.33-1.49,5.42-0.8,6.91,1.52c0.08,0.12,0.15,0.25,0.22,0.38L81.64,154.71z" /> <path d="M231.18,218.98l-0.07-0.69c-0.86-9.39-11.15-121.38-11.18-121.86c-0.23-2.84-1.07-5.6-2.45-8.09c-0.03-0.09-0.07-0.17-0.11-0.25l-0.06-0.15l-0.03,0.03l-0.02-0.01l0.04-0.02L205.5,67.5L172.31,10c-3.58-6.19-10.18-10-17.33-10H64.99c-7.14,0-13.74,3.81-17.32,10l-45,77.93c-3.57,6.19-3.57,13.81,0,20l45,77.93c3.57,6.19,10.17,10,17.32,10h89.99c3.86-0.03,7.63-1.19,10.85-3.32l38.59,27.68c-6.97-2.18-14.18-3.47-21.47-3.83c-18.11-0.87-71.2-0.28-131.42,4.63c-24.71,2.01-36.39,7.88-35.03,9.03c3.49,2.98,7.62,4.16,28.2,4.08c18.32-0.06,71.65,1.91,87.76,2.9c11.41,0.71,23.41,2.88,32.04,2.97c9.2-0.12,18.37-0.82,27.48-2.1c13.74-1.89,31.96-5.7,36.15-10.77C230.34,225.03,231.47,222.02,231.18,218.98z M62.49,24.32c1.67-2.55,4.45-4.16,7.5-4.33h79.99c3.04,0.17,5.81,1.77,7.49,4.31l33.26,57.61c-4.99,5.2-9.32,11-12.89,17.26l-24.77,2.75L138.3,122c-7.21-0.04-14.4,0.82-21.4,2.54L60.77,27.31L62.49,24.32z M69.99,175.86c-3.05-0.17-5.83-1.78-7.5-4.33l-40-69.27c-1.37-2.72-1.37-5.94,0-8.66l26.73-46.28l59.6,103.24l0.04-0.02c0.69,1.24,1.64,2.31,2.79,3.15l30.93,22.18L69.99,175.86z M186.75,182.93l-57.9-41.53c6.39-1.39,12.91-2.09,19.45-2.09l14.77-20.07l24.77-2.75c3.26-5.66,7.13-10.95,11.52-15.79l7.03,70.9C198.07,170.71,190.13,175.29,186.75,182.93z M81.64,154.71c1.49,2.33,0.8,5.42-1.52,6.91c-2.33,1.49-5.42,0.8-6.91-1.52c-0.08-0.12-0.15-0.25-0.22-0.38l-35-60.61c-1.49-2.33-0.8-5.42,1.52-6.91c2.33-1.49,5.42-0.8,6.91,1.52c0.08,0.12,0.15,0.25,0.22,0.38L81.64,154.71z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,7 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M9.6,13.6C9.5,14.5,8.7,15,8,15c-0.5,0-1-0.2-1.3-0.6L9.6,13.6z M6.3,12.5c0,0.3,0,0.6,0.1,1c0,0,0,0,0,0.1l3.3-0.8c0-0.5,0.1-0.8,0.1-1.1L6.3,12.5z M8,2c2.3,0,4.1,1.9,4.1,4.2c0,2.5-1.5,3.1-2.1,4.5c0,0,0,0.1,0,0.1l-3.8,0.9c-0.1-0.4-0.2-0.7-0.3-1C5.4,9.3,3.9,8.7,3.9,6.2c0,0,0,0,0,0C3.9,3.9,5.7,2,8,2C8,2,8,2,8,2z M8.2,8.9C8,8.7,7.3,9.2,7.1,9.5c-0.2,0.3-0.2,0.7,0.1,1c0.5,0.1,0.9,0,1.2-0.4C8.6,9.7,8.6,9.2,8.2,8.9z M8.2,8c0.3-0.7,1.2-4.7,1.2-4.7C8.2,2.7,7.1,4.2,7.1,4.2C7,5.5,7.1,6.8,7.2,8.1C7.6,8.2,7.9,8.1,8.2,8L8.2,8z" />
<polygon points="3,1 1,1 0,1 0,2 0,4 1,4 1,2 3,2" />
<polygon points="15,1 13,1 13,2 15,2 15,4 16,4 16,2 16,1" />
<polygon points="1,14 1,12 0,12 0,14 0,15 1,15 3,15 3,14" />
<polygon points="15,12 15,14 13,14 13,15 15,15 16,15 16,14 16,12" />
</svg>

After

Width:  |  Height:  |  Size: 845 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M15.9,9.6L14,2.7c-0.5-1.9-2.5-3.1-4.4-2.5L2.7,2C0.7,2.5-0.4,4.5,0.1,6.4L2,13.3c0.5,1.9,2.5,3.1,4.4,2.5l6.9-1.9C15.3,13.5,16.4,11.5,15.9,9.6z M2.3,5.9C2.1,5.1,2.5,4.3,3.3,4.1c0.8-0.2,1.6,0.3,1.8,1.1C5.4,5.9,4.9,6.8,4.1,7C3.3,7.2,2.5,6.7,2.3,5.9z M5.9,13.7c-0.8,0.2-1.6-0.3-1.8-1.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1C7.2,12.7,6.7,13.5,5.9,13.7z M8.4,9.4C7.6,9.7,6.8,9.2,6.6,8.4C6.3,7.6,6.8,6.8,7.6,6.6c0.8-0.2,1.6,0.3,1.8,1.1C9.7,8.4,9.2,9.2,8.4,9.4z M9,4.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1c0.2,0.8-0.3,1.6-1.1,1.8C10.1,5.4,9.2,4.9,9,4.1z M12.7,11.9c-0.8,0.2-1.6-0.3-1.8-1.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1C13.9,10.9,13.5,11.7,12.7,11.9z" />
</svg>

After

Width:  |  Height:  |  Size: 765 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<polygon points="12.6,0 15.5,5 9.7,5" />
<path d="M2.2,8H0.8C0.8,4,4,0.8,8,0.8c2,0,3.9,0.8,5.3,2.3l-1,1C11.2,2.9,9.6,2.2,8,2.2C4.8,2.2,2.2,4.8,2.2,8z" />
<path d="M8,15.2c-2,0-3.9-0.8-5.3-2.3l1-1c1.1,1.2,2.6,1.9,4.3,1.9c3.2,0,5.8-2.6,5.8-5.8h1.4C15.2,12,12,15.2,8,15.2z" />
<polygon points="3.4,16 0.5,11 6.3,11" />
</svg>

After

Width:  |  Height:  |  Size: 388 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8,15.2C4,15.2,0.8,12,0.8,8C0.8,4,4,0.8,8,0.8c2,0,3.9,0.8,5.3,2.3l-1,1C11.2,2.9,9.6,2.2,8,2.2C4.8,2.2,2.2,4.8,2.2,8s2.6,5.8,5.8,5.8s5.8-2.6,5.8-5.8h1.4C15.2,12,12,15.2,8,15.2z" />
<polygon points="12.6,0 15.5,5 9.7,5" />
</svg>

After

Width:  |  Height:  |  Size: 300 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<polygon points="3,1 1,1 0,1 0,2 0,4 1,4 1,2 3,2" />
<polygon points="15,1 13,1 13,2 15,2 15,4 16,4 16,2 16,1" />
<polygon points="1,14 1,12 0,12 0,14 0,15 1,15 3,15 3,14" />
<polygon points="15,12 15,14 13,14 13,15 15,15 16,15 16,14 16,12" />
<path d="M12,5v6H4V5H12 M13,4H3v8h10V4L13,4z" />
</svg>

After

Width:  |  Height:  |  Size: 366 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<polygon points="0,8 2.4,12.2 4.9,8" />
<path d="M8.8,15.2c-1.2,0-2.5-0.3-3.6-1L5.9,13c2.8,1.6,6.3,0.6,7.9-2.1c0.8-1.3,1-2.9,0.6-4.4S13,3.8,11.7,3C8.9,1.4,5.4,2.3,3.8,5.1C3,6.5,2.8,8.2,3.3,9.7l-1.3,0.4c-0.6-1.9-0.4-4,0.6-5.8C4.5,1,9-0.2,12.4,1.8c1.7,1,2.9,2.5,3.4,4.4C16.2,8,16,9.9,15,11.6C13.7,13.9,11.3,15.2,8.8,15.2z" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M14,8c0-0.3,0-0.7-0.1-1l1.8-1.7l-1.5-2.6l-2.4,0.7c-0.5-0.4-1.1-0.8-1.7-1L9.5,0h-3L5.9,2.4C5.3,2.6,4.7,3,4.2,3.4L1.8,2.7L0.3,5.3L2.1,7C2,7.3,2,7.7,2,8s0,0.7,0.1,1l-1.8,1.7l1.5,2.6l2.4-0.7c0.5,0.4,1.1,0.8,1.7,1L6.5,16h3l0.6-2.4c0.6-0.2,1.2-0.6,1.7-1l2.4,0.7l1.5-2.6L13.9,9C14,8.7,14,8.3,14,8z M8,11c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S9.7,11,8,11z" />
</svg>

After

Width:  |  Height:  |  Size: 429 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path class="color-solid" d="M10.1,22.1c0.5,0.6,1.3,0.9,1.9,0.9c1,0,2.3-0.7,2.4-2L10.1,22.1z" />
<path class="color-solid" d="M9.5,19.4c0,0.4,0,0.9,0.1,1.5c0,0,0,0.1,0,0.1l4.8-1.2c0-0.7,0.1-1.2,0.2-1.6L9.5,19.4z" />
<path class="color-raster" d="M12,4c-3.3,0-6,2.8-6,6.1c0,0,0,0,0,0c0,3.6,2.2,4.5,3,6.6c0.2,0.5,0.3,1,0.4,1.5l5.5-1.4c0,0,0-0.1,0-0.1c0.8-2.1,3-3,3-6.6C18,6.8,15.3,4,12,4C12,4,12,4,12,4z M12.5,15.8c-0.4,0.6-1.1,0.9-1.7,0.7c-0.4-0.4-0.4-1-0.1-1.5c0.3-0.5,1.3-1.2,1.7-0.9C12.8,14.5,12.9,15.3,12.5,15.8z M12.3,12.7c-0.5,0.2-1,0.3-1.5,0.2c-0.2-1.9-0.3-3.8-0.2-5.7c0,0,1.7-2.2,3.5-1.4C14.2,5.8,12.8,11.8,12.3,12.7L12.3,12.7z" />
<path class="color-solid" d="M3.8,12.7l-1.4,0.6c-0.4,0.2-0.6,0.6-0.4,1c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.2,0,0.3-0.1l1.4-0.6c0.4-0.2,0.6-0.6,0.4-1S4.2,12.5,3.8,12.7z" />
<path class="color-solid" d="M4.3,6.1L3,5.5c-0.4-0.2-0.8,0-1,0.4c-0.2,0.4,0,0.8,0.4,1l1.4,0.6c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5C4.9,6.7,4.7,6.3,4.3,6.1z" />
<path class="color-solid" d="M19.9,7.6c0.1,0,0.2,0,0.3-0.1l1.4-0.6c0.4-0.2,0.6-0.6,0.4-1c-0.2-0.4-0.6-0.6-1-0.4l-1.4,0.6c-0.4,0.2-0.6,0.6-0.4,1C19.4,7.4,19.6,7.6,19.9,7.6z" />
<path class="color-solid" d="M21.6,13.3l-1.4-0.6c-0.4-0.2-0.8,0-1,0.4c-0.2,0.4,0,0.8,0.4,1l1.4,0.6c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5C22.2,13.9,22,13.4,21.6,13.3z" />
<path class="color-solid" d="M8.8,0.5c-0.2-0.4-0.6-0.6-1-0.4c-0.4,0.2-0.6,0.6-0.4,1L8,2.4c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.2,0,0.3-0.1c0.4-0.2,0.6-0.6,0.4-1L8.8,0.5z" />
<path class="color-solid" d="M16.2,0.1c-0.4-0.2-0.8,0-1,0.4l-0.6,1.4c-0.2,0.4,0,0.8,0.4,1c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5l0.6-1.4C16.7,0.7,16.5,0.2,16.2,0.1z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

-3
View File
@@ -217,7 +217,6 @@ img {
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { createBlobManager } from "@/io-managers/blob";
import { createClipboardManager } from "@/io-managers/clipboard"; import { createClipboardManager } from "@/io-managers/clipboard";
import { createHyperlinkManager } from "@/io-managers/hyperlinks"; import { createHyperlinkManager } from "@/io-managers/hyperlinks";
import { createInputManager } from "@/io-managers/input"; import { createInputManager } from "@/io-managers/input";
@@ -236,7 +235,6 @@ import { createEditor, type Editor } from "@/wasm-communication/editor";
import MainWindow from "@/components/window/MainWindow.vue"; import MainWindow from "@/components/window/MainWindow.vue";
const managerDestructors: { const managerDestructors: {
createBlobManager?: () => void;
createClipboardManager?: () => void; createClipboardManager?: () => void;
createHyperlinkManager?: () => void; createHyperlinkManager?: () => void;
createInputManager?: () => void; createInputManager?: () => void;
@@ -285,7 +283,6 @@ export default defineComponent({
async mounted() { async mounted() {
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.) // Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
Object.assign(managerDestructors, { Object.assign(managerDestructors, {
createBlobManager: createBlobManager(this.editor),
createClipboardManager: createClipboardManager(this.editor), createClipboardManager: createClipboardManager(this.editor),
createHyperlinkManager: createHyperlinkManager(this.editor), createHyperlinkManager: createHyperlinkManager(this.editor),
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen), createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
@@ -22,6 +22,7 @@
class="row" class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }" :class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
:style="{ height: virtualScrollingEntryHeight || '20px' }" :style="{ height: virtualScrollingEntryHeight || '20px' }"
:title="tooltip"
@click="() => !entry.disabled && onEntryClick(entry)" @click="() => !entry.disabled && onEntryClick(entry)"
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)" @pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)" @pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
@@ -184,6 +185,7 @@ const MenuList = defineComponent({
interactive: { type: Boolean as PropType<boolean>, default: false }, interactive: { type: Boolean as PropType<boolean>, default: false },
scrollableY: { type: Boolean as PropType<boolean>, default: false }, scrollableY: { type: Boolean as PropType<boolean>, default: false },
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 }, virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
+15 -6
View File
@@ -45,17 +45,14 @@
@click.alt="(e: MouseEvent) => e.stopPropagation()" @click.alt="(e: MouseEvent) => e.stopPropagation()"
> >
<LayoutRow class="layer-type-icon"> <LayoutRow class="layer-type-icon">
<IconLabel v-if="listing.entry.layerType === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" /> <IconLabel :icon="layerTypeData(listing.entry.layerType).icon" :title="layerTypeData(listing.entry.layerType).name" />
<IconLabel v-else-if="listing.entry.layerType === 'Image'" :icon="'NodeImage'" :iconStyle="'Node'" title="Image" />
<IconLabel v-else-if="listing.entry.layerType === 'Shape'" :icon="'NodeShape'" :iconStyle="'Node'" title="Shape" />
<IconLabel v-else-if="listing.entry.layerType === 'Text'" :icon="'NodeText'" :iconStyle="'Node'" title="Path" />
</LayoutRow> </LayoutRow>
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)"> <LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
<input <input
data-text-input data-text-input
type="text" type="text"
:value="listing.entry.name" :value="listing.entry.name"
:placeholder="listing.entry.layerType" :placeholder="layerTypeData(listing.entry.layerType).name"
:disabled="!listing.editingName" :disabled="!listing.editingName"
@blur="() => onEditLayerNameDeselect(listing)" @blur="() => onEditLayerNameDeselect(listing)"
@keydown.esc="onEditLayerNameDeselect(listing)" @keydown.esc="onEditLayerNameDeselect(listing)"
@@ -268,7 +265,16 @@
import { defineComponent, nextTick } from "vue"; import { defineComponent, nextTick } from "vue";
import { platformIsMac } from "@/utility-functions/platform"; import { platformIsMac } from "@/utility-functions/platform";
import { type LayerPanelEntry, defaultWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructure, UpdateLayerTreeOptionsLayout } from "@/wasm-communication/messages"; import {
type LayerType,
type LayerTypeData,
type LayerPanelEntry,
defaultWidgetLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructure,
UpdateLayerTreeOptionsLayout,
layerTypeData,
} from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
@@ -484,6 +490,9 @@ export default defineComponent({
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache); recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
}, },
layerTypeData(layerType: LayerType): LayerTypeData {
return layerTypeData(layerType) || { name: "Error", icon: "NodeText" };
},
}, },
mounted() { mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => { this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
+8 -8
View File
@@ -28,7 +28,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeImage'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeImage'" />
<TextLabel>Image</TextLabel> <TextLabel>Image</TextLabel>
</div> </div>
</div> </div>
@@ -42,7 +42,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeMask'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeMask'" />
<TextLabel>Mask</TextLabel> <TextLabel>Mask</TextLabel>
</div> </div>
<div class="arguments"> <div class="arguments">
@@ -69,7 +69,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeTransform'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeTransform'" />
<TextLabel>Transform</TextLabel> <TextLabel>Transform</TextLabel>
</div> </div>
</div> </div>
@@ -83,7 +83,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeMotionBlur'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeMotionBlur'" />
<TextLabel>Motion Blur</TextLabel> <TextLabel>Motion Blur</TextLabel>
</div> </div>
<div class="arguments"> <div class="arguments">
@@ -110,7 +110,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeShape'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeShape'" />
<TextLabel>Shape</TextLabel> <TextLabel>Shape</TextLabel>
</div> </div>
</div> </div>
@@ -124,7 +124,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeBrushwork'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeBrushwork'" />
<TextLabel>Brushwork</TextLabel> <TextLabel>Brushwork</TextLabel>
</div> </div>
</div> </div>
@@ -138,7 +138,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeBlur'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeBlur'" />
<TextLabel>Blur</TextLabel> <TextLabel>Blur</TextLabel>
</div> </div>
</div> </div>
@@ -152,7 +152,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<IconLabel :icon="'NodeGradient'" :iconStyle="'Node'" /> <IconLabel :icon="'NodeGradient'" />
<TextLabel>Gradient</TextLabel> <TextLabel>Gradient</TextLabel>
</div> </div>
</div> </div>
@@ -21,12 +21,6 @@
.options-bar { .options-bar {
height: 32px; height: 32px;
flex: 0 0 auto; flex: 0 0 auto;
.widget-row > .icon-label:first-of-type {
border-radius: 2px;
background: var(--color-node-background);
fill: var(--color-node-icon);
}
} }
.sections { .sections {
@@ -1,6 +1,6 @@
<template> <template>
<LayoutRow class="popover-button"> <LayoutRow class="popover-button">
<IconButton :action="() => onClick()" :icon="icon" :size="16" data-hover-menu-spawner /> <IconButton :action="() => onClick()" :icon="icon" :size="16" data-hover-menu-spawner :tooltip="tooltip" />
<FloatingMenu v-model:open="open" :type="'Popover'" :direction="'Bottom'"> <FloatingMenu v-model:open="open" :type="'Popover'" :direction="'Bottom'">
<slot></slot> <slot></slot>
</FloatingMenu> </FloatingMenu>
@@ -58,6 +58,7 @@ import IconButton from "@/components/widgets/buttons/IconButton.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
icon: { type: String as PropType<IconName>, default: "DropdownArrow" }, icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks // Callbacks
action: { type: Function as PropType<() => void>, required: false }, action: { type: Function as PropType<() => void>, required: false },
@@ -5,6 +5,7 @@
:data-emphasized="emphasized || undefined" :data-emphasized="emphasized || undefined"
:data-disabled="disabled || undefined" :data-disabled="disabled || undefined"
data-text-button data-text-button
:title="tooltip"
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
@click="(e: MouseEvent) => action(e)" @click="(e: MouseEvent) => action(e)"
> >
@@ -71,23 +72,6 @@ import { type IconName } from "@/utility-functions/icons";
import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue"; import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export type TextButtonWidget = {
tooltip?: string;
message?: string | object;
callback?: () => void;
props: {
kind: "TextButton";
label: string;
icon?: string;
emphasized?: boolean;
minWidth?: number;
disabled?: boolean;
// Callbacks
// `action` is used via `IconButtonWidget.callback`
};
};
export default defineComponent({ export default defineComponent({
props: { props: {
label: { type: String as PropType<string>, required: true }, label: { type: String as PropType<string>, required: true },
@@ -95,6 +79,7 @@ export default defineComponent({
emphasized: { type: Boolean as PropType<boolean>, default: false }, emphasized: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 }, minWidth: { type: Number as PropType<number>, default: 0 },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks // Callbacks
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true }, action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
@@ -61,10 +61,14 @@
.body { .body {
margin: 0 4px; margin: 0 4px;
.text-label { .text-label:first-of-type {
flex: 0 0 30%; flex: 0 0 30%;
text-align: right; text-align: right;
} }
.text-button {
flex-grow: 1;
}
} }
} }
</style> </style>
@@ -4,6 +4,7 @@
class="dropdown-box" class="dropdown-box"
:class="{ disabled, open }" :class="{ disabled, open }"
:style="{ minWidth: `${minWidth}px` }" :style="{ minWidth: `${minWidth}px` }"
:title="tooltip"
@click="() => !disabled && (open = true)" @click="() => !disabled && (open = true)"
@blur="(e: FocusEvent) => blur(e)" @blur="(e: FocusEvent) => blur(e)"
@keydown="(e: KeyboardEvent) => keydown(e)" @keydown="(e: KeyboardEvent) => keydown(e)"
@@ -115,6 +116,7 @@ export default defineComponent({
drawIcon: { type: Boolean as PropType<boolean>, default: false }, drawIcon: { type: Boolean as PropType<boolean>, default: false },
interactive: { type: Boolean as PropType<boolean>, default: true }, interactive: { type: Boolean as PropType<boolean>, default: true },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
@@ -10,6 +10,7 @@
v-model="inputValue" v-model="inputValue"
:spellcheck="spellcheck" :spellcheck="spellcheck"
:disabled="disabled" :disabled="disabled"
:title="tooltip"
@focus="() => $emit('textFocused')" @focus="() => $emit('textFocused')"
@blur="() => $emit('textChanged')" @blur="() => $emit('textChanged')"
@change="() => $emit('textChanged')" @change="() => $emit('textChanged')"
@@ -26,6 +27,7 @@
v-model="inputValue" v-model="inputValue"
:spellcheck="spellcheck" :spellcheck="spellcheck"
:disabled="disabled" :disabled="disabled"
:title="tooltip"
@focus="() => $emit('textFocused')" @focus="() => $emit('textFocused')"
@blur="() => $emit('textChanged')" @blur="() => $emit('textChanged')"
@change="() => $emit('textChanged')" @change="() => $emit('textChanged')"
@@ -55,6 +57,7 @@
padding: 3px 0; padding: 3px 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
&:not(.disabled) label { &:not(.disabled) label {
@@ -129,6 +132,7 @@ export default defineComponent({
spellcheck: { type: Boolean as PropType<boolean>, default: false }, spellcheck: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
textarea: { type: Boolean as PropType<boolean>, default: false }, textarea: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
@@ -1,6 +1,6 @@
<template> <template>
<LayoutRow class="font-input"> <LayoutRow class="font-input">
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner> <LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" :title="tooltip" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner>
<span>{{ activeEntry?.value || "" }}</span> <span>{{ activeEntry?.value || "" }}</span>
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" /> <IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</LayoutRow> </LayoutRow>
@@ -87,6 +87,7 @@ export default defineComponent({
fontStyle: { type: String as PropType<string>, required: true }, fontStyle: { type: String as PropType<string>, required: true },
isStyle: { type: Boolean as PropType<boolean>, default: false }, isStyle: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
@@ -5,6 +5,8 @@
:label="label" :label="label"
:spellcheck="false" :spellcheck="false"
:disabled="disabled" :disabled="disabled"
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
:tooltip="tooltip"
@textFocused="() => onTextFocused()" @textFocused="() => onTextFocused()"
@textChanged="() => onTextChanged()" @textChanged="() => onTextChanged()"
@cancelTextChange="() => onCancelTextChange()" @cancelTextChange="() => onCancelTextChange()"
@@ -107,6 +109,8 @@ export default defineComponent({
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" }, incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
incrementFactor: { type: Number as PropType<number>, default: 1 }, incrementFactor: { type: Number as PropType<number>, default: 1 },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks // Callbacks
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false }, incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
@@ -122,7 +126,7 @@ export default defineComponent({
onTextFocused() { onTextFocused() {
if (this.value === undefined) this.text = ""; if (this.value === undefined) this.text = "";
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`; else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
else this.text = `${this.value}${this.unit}`; else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
this.editing = true; this.editing = true;
@@ -201,7 +205,7 @@ export default defineComponent({
const displayValue = Math.round(value * roundingPower) / roundingPower; const displayValue = Math.round(value * roundingPower) / roundingPower;
return `${displayValue}${this.unit}`; return `${displayValue}${unPluralize(this.unit, value)}`;
}, },
}, },
watch: { watch: {
@@ -222,4 +226,9 @@ export default defineComponent({
}, },
components: { FieldInput }, components: { FieldInput },
}); });
function unPluralize(unit: string, value: number): string {
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
return unit;
}
</script> </script>
@@ -3,10 +3,11 @@
:textarea="true" :textarea="true"
class="text-area-input" class="text-area-input"
:class="{ 'has-label': label }" :class="{ 'has-label': label }"
v-model:value="inputValue"
:label="label" :label="label"
:spellcheck="true" :spellcheck="true"
:disabled="disabled" :disabled="disabled"
:tooltip="tooltip"
v-model:value="inputValue"
@textFocused="() => onTextFocused()" @textFocused="() => onTextFocused()"
@textChanged="() => onTextChanged()" @textChanged="() => onTextChanged()"
@cancelTextChange="() => onCancelTextChange()" @cancelTextChange="() => onCancelTextChange()"
@@ -27,6 +28,7 @@ export default defineComponent({
value: { type: String as PropType<string>, required: true }, value: { type: String as PropType<string>, required: true },
label: { type: String as PropType<string>, required: false }, label: { type: String as PropType<string>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
@@ -5,6 +5,8 @@
:label="label" :label="label"
:spellcheck="true" :spellcheck="true"
:disabled="disabled" :disabled="disabled"
:tooltip="tooltip"
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
@textFocused="() => onTextFocused()" @textFocused="() => onTextFocused()"
@textChanged="() => onTextChanged()" @textChanged="() => onTextChanged()"
@cancelTextChange="() => onCancelTextChange()" @cancelTextChange="() => onCancelTextChange()"
@@ -31,6 +33,8 @@ export default defineComponent({
value: { type: String as PropType<string>, required: true }, value: { type: String as PropType<string>, required: true },
label: { type: String as PropType<string>, required: false }, label: { type: String as PropType<string>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false }, disabled: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
data() { data() {
return { return {
@@ -1,5 +1,5 @@
<template> <template>
<LayoutRow :class="['icon-label', iconSizeClass, iconStyleClass]"> <LayoutRow :class="['icon-label', iconSizeClass]" :title="tooltip">
<component :is="icon" /> <component :is="icon" />
</LayoutRow> </LayoutRow>
</template> </template>
@@ -23,35 +23,25 @@
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
&.node-style {
border-radius: 2px;
background: var(--color-node-background);
fill: var(--color-node-icon);
}
} }
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import { type IconName, type IconStyle, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons"; import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
icon: { type: String as PropType<IconName>, required: true }, icon: { type: String as PropType<IconName>, required: true },
iconStyle: { type: String as PropType<IconStyle | undefined>, required: false }, tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
computed: { computed: {
iconSizeClass(): string { iconSizeClass(): string {
return `size-${ICONS[this.icon].size}`; return `size-${ICONS[this.icon].size}`;
}, },
iconStyleClass(): string {
if (!this.iconStyle || this.iconStyle === "Normal") return "";
return `${this.iconStyle.toLowerCase()}-style`;
},
}, },
components: { components: {
LayoutRow, LayoutRow,
@@ -1,5 +1,5 @@
<template> <template>
<span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }"> <span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" :title="tooltip">
<slot></slot> <slot></slot>
</span> </span>
</template> </template>
@@ -37,7 +37,9 @@ export default defineComponent({
bold: { type: Boolean as PropType<boolean>, default: false }, bold: { type: Boolean as PropType<boolean>, default: false },
italic: { type: Boolean as PropType<boolean>, default: false }, italic: { type: Boolean as PropType<boolean>, default: false },
tableAlign: { type: Boolean as PropType<boolean>, default: false }, tableAlign: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
multiline: { type: Boolean as PropType<boolean>, default: false }, multiline: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
}, },
}); });
</script> </script>
@@ -3,15 +3,16 @@
<LayoutRow class="tab-bar" data-tab-bar :class="{ 'min-widths': tabMinWidths }"> <LayoutRow class="tab-bar" data-tab-bar :class="{ 'min-widths': tabMinWidths }">
<LayoutRow class="tab-group" :scrollableX="true"> <LayoutRow class="tab-group" :scrollableX="true">
<LayoutRow <LayoutRow
class="tab"
:class="{ active: tabIndex === tabActiveIndex }"
data-tab
v-for="(tabLabel, tabIndex) in tabLabels" v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex" :key="tabIndex"
class="tab"
:class="{ active: tabIndex === tabActiveIndex }"
:title="tabLabel.tooltip || null"
@click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(tabIndex))" @click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" @click.middle="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))"
data-tab
> >
<span>{{ tabLabel }}</span> <span>{{ tabLabel.name }}</span>
<IconButton :action="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" /> <IconButton :action="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</LayoutRow> </LayoutRow>
</LayoutRow> </LayoutRow>
@@ -31,7 +32,7 @@
<table> <table>
<tr> <tr>
<td> <td>
<TextButton :label="'New Document:'" :icon="'File'" :action="() => newDocument()" /> <TextButton :label="'New Document'" :icon="'File'" :action="() => newDocument()" />
</td> </td>
<td> <td>
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(true), { key: 'KeyN', label: 'N' }]]" /> <UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(true), { key: 'KeyN', label: 'N' }]]" />
@@ -39,7 +40,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<TextButton :label="'Open Document:'" :icon="'Folder'" :action="() => openDocument()" /> <TextButton :label="'Open Document'" :icon="'Folder'" :action="() => openDocument()" />
</td> </td>
<td> <td>
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(false), { key: 'KeyO', label: 'O' }]]" /> <UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(false), { key: 'KeyO', label: 'O' }]]" />
@@ -244,7 +245,7 @@ export default defineComponent({
props: { props: {
tabMinWidths: { type: Boolean as PropType<boolean>, default: false }, tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false }, tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
tabLabels: { type: Array as PropType<string[]>, required: true }, tabLabels: { type: Array as PropType<{ name: string; tooltip?: string }[]>, required: true },
tabActiveIndex: { type: Number as PropType<number>, required: true }, tabActiveIndex: { type: Number as PropType<number>, required: true },
panelType: { type: String as PropType<PanelTypes>, required: false }, panelType: { type: String as PropType<PanelTypes>, required: false },
clickAction: { type: Function as PropType<(index: number) => void>, required: false }, clickAction: { type: Function as PropType<(index: number) => void>, required: false },
@@ -7,7 +7,7 @@
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined" :panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
:tabCloseButtons="true" :tabCloseButtons="true"
:tabMinWidths="true" :tabMinWidths="true"
:tabLabels="portfolio.state.documents.map((doc) => doc.displayName)" :tabLabels="portfolio.state.documents.map((doc) => ({ name: doc.displayName, tooltip: doc.id }))"
:clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)" :clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)" :closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
:tabActiveIndex="portfolio.state.activeDocumentIndex" :tabActiveIndex="portfolio.state.activeDocumentIndex"
@@ -16,17 +16,17 @@
</LayoutRow> </LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow> <LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible"> <LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
<Panel :panelType="'NodeGraph'" :tabLabels="['Node Graph']" :tabActiveIndex="0" /> <Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol> <LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17"> <LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.2">
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402"> <LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" /> <Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
</LayoutRow> </LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow> <LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590"> <LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" /> <Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
-20
View File
@@ -1,20 +0,0 @@
import { type Editor } from "@/wasm-communication/editor";
import { UpdateImageData } from "@/wasm-communication/messages";
export function createBlobManager(editor: Editor): void {
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
// Using updateImageData.imageData.buffer returns undefined for some reason?
const buffer = new Uint8Array(element.imageData.values()).buffer;
const blob = new Blob([buffer], { type: element.mime });
// TODO: Call `URL.revokeObjectURL` at the appropriate time to avoid a memory leak
const blobURL = URL.createObjectURL(blob);
const image = await createImageBitmap(blob);
editor.instance.setImageBlobUrl(element.path, blobURL, image.width, image.height);
});
});
}
+2 -2
View File
@@ -25,8 +25,8 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] { function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const widgets: WidgetLayout = { const widgets: WidgetLayout = {
layout: [ layout: [
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, multiline: false }, 0n)] }, { rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" }, 0n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, multiline: true }, 1n)] }, { rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
], ],
layoutTarget: undefined, layoutTarget: undefined,
}; };
+131 -75
View File
@@ -1,99 +1,155 @@
import { type PortfolioState } from "@/state-providers/portfolio"; import { type PortfolioState } from "@/state-providers/portfolio";
import { stripIndents } from "@/utility-functions/strip-indents"; import { stripIndents } from "@/utility-functions/strip-indents";
import { type Editor } from "@/wasm-communication/editor"; import { type Editor } from "@/wasm-communication/editor";
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument } from "@/wasm-communication/messages"; import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument, TriggerSavePreferences, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences } from "@/wasm-communication/messages";
const GRAPHITE_INDEXED_DB_VERSION = 2; const GRAPHITE_INDEXED_DB_VERSION = 2;
const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db"; const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db";
const GRAPHITE_AUTO_SAVE_STORE = "auto-save-documents";
const GRAPHITE_AUTO_SAVE_STORE = { name: "auto-save-documents", keyPath: "details.id" };
const GRAPHITE_EDITOR_PREFERENCES_STORE = { name: "editor-preferences", keyPath: "key" };
const GRAPHITE_INDEXEDDB_STORES = [GRAPHITE_AUTO_SAVE_STORE, GRAPHITE_EDITOR_PREFERENCES_STORE];
const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order"; const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): () => void {
export async function createPersistenceManager(editor: Editor, portfolio: PortfolioState): Promise<() => void> { async function initialize(): Promise<IDBDatabase> {
function storeDocumentOrder(): void { // Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
// Make sure to store as string since JSON does not play nice with BigInt return new Promise<IDBDatabase>((resolve) => {
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString()); const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
}
async function removeDocument(id: string): Promise<void> { // Handle a version mismatch if `GRAPHITE_INDEXED_DB_VERSION` is now higher than what was saved in the database
const db = await databaseConnection; dbOpenRequest.onupgradeneeded = (): void => {
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite"); const db = dbOpenRequest.result;
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).delete(id);
storeDocumentOrder();
}
async function closeDatabaseConnection(): Promise<void> { // Wipe out all stores when a request is made to upgrade the database version to a newer one
const db = await databaseConnection; GRAPHITE_INDEXEDDB_STORES.forEach((store) => {
db.close(); if (db.objectStoreNames.contains(store.name)) db.deleteObjectStore(store.name);
}
// Subscribe to process backend events db.createObjectStore(store.name, { keyPath: store.keyPath });
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => { });
const db = await databaseConnection; };
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).put(autoSaveDocument);
storeDocumentOrder();
});
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
removeDocument(removeAutoSaveDocument.documentId);
});
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open // Handle some other error by presenting it to the user
const databaseConnection = new Promise<IDBDatabase>((resolve) => { dbOpenRequest.onerror = (): void => {
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION); const errorText = stripIndents`
dbOpenRequest.onupgradeneeded = (): void => {
const db = dbOpenRequest.result;
// Wipes out all auto-save data on upgrade
if (db.objectStoreNames.contains(GRAPHITE_AUTO_SAVE_STORE)) {
db.deleteObjectStore(GRAPHITE_AUTO_SAVE_STORE);
}
db.createObjectStore(GRAPHITE_AUTO_SAVE_STORE, { keyPath: "details.id" });
};
dbOpenRequest.onerror = (): void => {
const errorText = stripIndents`
Documents won't be saved across reloads and later visits. Documents won't be saved across reloads and later visits.
This may be caused by Firefox's private browsing mode. This may be caused by Firefox's private browsing mode.
Error on opening IndexDB: Error on opening IndexDB:
${dbOpenRequest.error} ${dbOpenRequest.error}
`; `;
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText); editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
}; };
dbOpenRequest.onsuccess = (): void => { // Resolve the promise on a successful opening of the database connection
resolve(dbOpenRequest.result); dbOpenRequest.onsuccess = (): void => {
}; resolve(dbOpenRequest.result);
});
databaseConnection.then(async (db) => {
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readonly");
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).getAll();
await new Promise((resolve): void => {
request.onsuccess = (): void => {
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
const orderedSavedDocuments = documentOrder
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
orderedSavedDocuments.forEach((doc: TriggerIndexedDbWriteDocument) => {
if (doc.version === currentDocumentVersion) {
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
} else {
removeDocument(doc.details.id);
}
});
resolve(undefined);
}; };
}); });
}
function storeDocumentOrder(): void {
// Make sure to store as string since JSON does not play nice with BigInt
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
}
async function removeDocument(id: string, db: IDBDatabase): Promise<void> {
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).delete(id);
storeDocumentOrder();
}
async function loadAutoSaveDocuments(db: IDBDatabase): Promise<void> {
let promiseResolve: (value: void | PromiseLike<void>) => void;
const promise = new Promise<void>((resolve): void => {
promiseResolve = resolve;
});
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readonly");
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).getAll();
request.onsuccess = (): void => {
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
const orderedSavedDocuments = documentOrder
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
orderedSavedDocuments.forEach(async (doc: TriggerIndexedDbWriteDocument) => {
if (doc.version === currentDocumentVersion) {
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
} else {
await removeDocument(doc.details.id, db);
}
});
promiseResolve();
};
await promise;
}
async function loadPreferences(db: IDBDatabase): Promise<void> {
let promiseResolve: (value: void | PromiseLike<void>) => void;
const promise = new Promise<void>((resolve): void => {
promiseResolve = resolve;
});
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readonly");
const request = transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).getAll();
request.onsuccess = (): void => {
const preferenceEntries: { key: string; value: unknown }[] = request.result;
const preferences: Record<string, unknown> = {};
preferenceEntries.forEach(({ key, value }) => {
preferences[key] = value;
});
editor.instance.loadPreferences(JSON.stringify(preferences));
promiseResolve();
};
await promise;
}
// Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => {
const transaction = (await databaseConnection).transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).put(autoSaveDocument);
storeDocumentOrder();
});
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
await removeDocument(removeAutoSaveDocument.documentId, await databaseConnection);
});
editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => {
await loadAutoSaveDocuments(await databaseConnection);
});
editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (preferences) => {
Object.entries(preferences.preferences).forEach(async ([key, value]) => {
const storedObject = { key, value };
const transaction = (await databaseConnection).transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).put(storedObject);
});
});
editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => {
await loadPreferences(await databaseConnection);
}); });
return closeDatabaseConnection; const databaseConnection = initialize();
// Destructor
return () => {
databaseConnection.then((connection) => connection.close());
};
} }
+16 -7
View File
@@ -9,7 +9,19 @@ import { initWasm } from "@/wasm-communication/editor";
import App from "@/App.vue"; import App from "@/App.vue";
// Browser app entry point
(async (): Promise<void> => { (async (): Promise<void> => {
// Confirm the browser is compatible before initializing the application
if (!checkBrowserCompatibility()) return;
// Initialize the WASM module for the editor backend
await initWasm();
// Initialize the Vue application
createApp(App).mount("#app");
})();
function checkBrowserCompatibility(): boolean {
if (!("BigUint64Array" in window)) { if (!("BigUint64Array" in window)) {
const body = document.body; const body = document.body;
const message = stripIndents` const message = stripIndents`
@@ -23,12 +35,9 @@ import App from "@/App.vue";
JavaScript API must be supported by the browser for Graphite to function.)</p> JavaScript API must be supported by the browser for Graphite to function.)</p>
`; `;
body.innerHTML = message + body.innerHTML; body.innerHTML = message + body.innerHTML;
return;
return false;
} }
// Initialize the WASM module for the editor backend return true;
await initWasm(); }
// Initialize the Vue application
createApp(App).mount("#app");
})();
+47
View File
@@ -2,6 +2,7 @@
import { reactive, readonly } from "vue"; import { reactive, readonly } from "vue";
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files"; import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate } from "@/utility-functions/imaginate";
import { rasterizeSVG } from "@/utility-functions/rasterization"; import { rasterizeSVG } from "@/utility-functions/rasterization";
import { type Editor } from "@/wasm-communication/editor"; import { type Editor } from "@/wasm-communication/editor";
import { import {
@@ -10,8 +11,13 @@ import {
TriggerImport, TriggerImport,
TriggerOpenDocument, TriggerOpenDocument,
TriggerRasterDownload, TriggerRasterDownload,
TriggerImaginateGenerate,
TriggerImaginateTerminate,
TriggerImaginateCheckServerStatus,
UpdateActiveDocument, UpdateActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdateImageData,
TriggerRevokeBlobUrl,
} from "@/wasm-communication/messages"; } from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -55,6 +61,47 @@ export function createPortfolioState(editor: Editor) {
// Have the browser download the file to the user's disk // Have the browser download the file to the user's disk
downloadFileBlob(name, blob); downloadFileBlob(name, blob);
}); });
editor.subscriptions.subscribeJsMessage(TriggerImaginateCheckServerStatus, async (triggerImaginateCheckServerStatus) => {
const { hostname } = triggerImaginateCheckServerStatus;
imaginateCheckConnection(hostname, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateGenerate, async (triggerImaginateGenerate) => {
const { documentId, layerPath, hostname, refreshFrequency, baseImage, parameters } = triggerImaginateGenerate;
// Handle img2img mode
let image: Blob | undefined;
if (parameters.denoisingStrength !== undefined && baseImage !== undefined) {
// Rasterize the SVG to an image file
image = await rasterizeSVG(baseImage.svg, baseImage.size[0], baseImage.size[1], "image/png");
const blobURL = URL.createObjectURL(image);
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, baseImage.size[0], baseImage.size[1]);
}
imaginateGenerate(parameters, image, hostname, refreshFrequency, documentId, layerPath, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateTerminate, async (triggerImaginateTerminate) => {
const { documentId, layerPath, hostname } = triggerImaginateTerminate;
imaginateTerminate(hostname, documentId, layerPath, editor);
});
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
const buffer = new Uint8Array(element.imageData.values()).buffer;
const blob = new Blob([buffer], { type: element.mime });
const blobURL = URL.createObjectURL(blob);
const image = await createImageBitmap(blob);
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.width, image.height);
});
});
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url);
});
return { return {
state: readonly(state) as typeof state, state: readonly(state) as typeof state,
+14
View File
@@ -0,0 +1,14 @@
/* eslint-disable no-useless-escape */
/* eslint-disable quotes */
export function escapeJSON(str: string): string {
return str
.replace(/[\\]/g, "\\\\")
.replace(/[\"]/g, '\\"')
.replace(/[\/]/g, "\\/")
.replace(/[\b]/g, "\\b")
.replace(/[\f]/g, "\\f")
.replace(/[\n]/g, "\\n")
.replace(/[\r]/g, "\\r")
.replace(/[\t]/g, "\\t");
}
+28
View File
@@ -52,3 +52,31 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
} }
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> }; export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never; type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never;
export function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = (): void => resolve(typeof reader.result === "string" ? reader.result : "");
reader.readAsDataURL(blob);
});
}
export async function replaceBlobURLsWithBase64(svg: string): Promise<string> {
const splitByBlobs = svg.split(/(?<=")(blob:.*?)(?=")/);
const onlyBlobs = splitByBlobs.filter((_, i) => i % 2 === 1);
const onlyBlobsConverted = onlyBlobs.map(async (blobURL) => {
const data = await fetch(blobURL);
const dataBlob = await data.blob();
return blobToBase64(dataBlob);
});
const base64Images = await Promise.all(onlyBlobsConverted);
const substituted = splitByBlobs.map((segment, i) => {
if (i % 2 === 0) return segment;
const blobsIndex = Math.floor(i / 2);
return base64Images[blobsIndex];
});
return substituted.join("");
}

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