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>
|
@ -7,7 +7,7 @@ fn main() {
|
|||
let try_git_command = |args: &[&str]| -> Option<String> {
|
||||
let git_output = Command::new("git").args(args).output().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)
|
||||
};
|
||||
// Execute a Git command for its output. Return "unknown" if it fails for any of the possible reasons.
|
||||
|
|
|
@ -22,6 +22,7 @@ struct DispatcherMessageHandlers {
|
|||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||
layout_message_handler: LayoutMessageHandler,
|
||||
portfolio_message_handler: PortfolioMessageHandler,
|
||||
preferences_message_handler: PreferencesMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
workspace_message_handler: WorkspaceMessageHandler,
|
||||
}
|
||||
|
@ -92,14 +93,16 @@ impl Dispatcher {
|
|||
NoOp => {}
|
||||
#[remain::unsorted]
|
||||
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
|
||||
let message = MenuBarMessage::SendLayout.into();
|
||||
queue.push_back(message);
|
||||
queue.push_back(MenuBarMessage::SendLayout.into());
|
||||
|
||||
// Load the default font
|
||||
let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into());
|
||||
let message = FrontendMessage::TriggerFontLoad { font, is_default: true }.into();
|
||||
queue.push_back(message);
|
||||
queue.push_back(FrontendMessage::TriggerFontLoad { font, is_default: true }.into());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Dialog(message) => {
|
||||
self.message_handlers
|
||||
.dialog_message_handler
|
||||
.process_message(message, &self.message_handlers.portfolio_message_handler, &mut queue);
|
||||
self.message_handlers.dialog_message_handler.process_message(
|
||||
message,
|
||||
(&self.message_handlers.portfolio_message_handler, &self.message_handlers.preferences_message_handler),
|
||||
&mut queue,
|
||||
);
|
||||
}
|
||||
Frontend(message) => {
|
||||
// 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);
|
||||
}
|
||||
Portfolio(message) => {
|
||||
self.message_handlers
|
||||
.portfolio_message_handler
|
||||
.process_message(message, &self.message_handlers.input_preprocessor_message_handler, &mut queue);
|
||||
self.message_handlers.portfolio_message_handler.process_message(
|
||||
message,
|
||||
(&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) => {
|
||||
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() {
|
||||
|
@ -155,8 +165,9 @@ impl Dispatcher {
|
|||
message,
|
||||
(
|
||||
document,
|
||||
self.message_handlers.portfolio_message_handler.active_document_id().unwrap(),
|
||||
&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,
|
||||
);
|
||||
|
|
|
@ -3,8 +3,6 @@ extern crate graphite_proc_macros;
|
|||
// `macro_use` puts these macros into scope for all descendant code files
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ pub enum DialogMessage {
|
|||
#[remain::unsorted]
|
||||
#[child]
|
||||
NewDocumentDialog(NewDocumentDialogMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
PreferencesDialog(PreferencesDialogMessage),
|
||||
|
||||
// Messages
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
|
@ -32,4 +35,5 @@ pub enum DialogMessage {
|
|||
},
|
||||
RequestExportDialog,
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
}
|
||||
|
|
|
@ -7,17 +7,20 @@ use crate::messages::prelude::*;
|
|||
pub struct DialogMessageHandler {
|
||||
export_dialog: ExportDialogMessageHandler,
|
||||
new_document_dialog: NewDocumentDialogMessageHandler,
|
||||
preferences_dialog: PreferencesDialogMessageHandler,
|
||||
}
|
||||
|
||||
impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHandler {
|
||||
impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessageHandler)> for DialogMessageHandler {
|
||||
#[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]
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, (), responses),
|
||||
#[remain::unsorted]
|
||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, (), responses),
|
||||
#[remain::unsorted]
|
||||
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, preferences, responses),
|
||||
|
||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||
let dialog = simple_dialogs::CloseAllDocumentsDialog;
|
||||
|
@ -97,12 +100,18 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
|
|||
self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
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;
|
||||
RequestNewDocumentDialog,
|
||||
RequestExportDialog,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
RequestExportDialog,
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ mod dialog_message_handler;
|
|||
|
||||
pub mod export_dialog;
|
||||
pub mod new_document_dialog;
|
||||
pub mod preferences_dialog;
|
||||
pub mod simple_dialogs;
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
|
@ -83,7 +83,6 @@ impl PropertyHolder for NewDocumentDialogMessageHandler {
|
|||
})),
|
||||
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||
checked: self.infinite,
|
||||
icon: "Checkmark".to_string(),
|
||||
on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()),
|
||||
..Default::default()
|
||||
})),
|
||||
|
|
7
editor/src/messages/dialog/preferences_dialog/mod.rs
Normal file
|
@ -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 graphene::color::Color;
|
||||
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters};
|
||||
use graphene::layers::text_layer::Font;
|
||||
use graphene::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -50,6 +52,28 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "isDefault")]
|
||||
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,
|
||||
TriggerIndexedDbRemoveDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
|
@ -60,6 +84,8 @@ pub enum FrontendMessage {
|
|||
details: FrontendDocumentDetails,
|
||||
version: String,
|
||||
},
|
||||
TriggerLoadAutoSaveDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerOpenDocument,
|
||||
TriggerPaste,
|
||||
TriggerRasterDownload {
|
||||
|
@ -69,6 +95,12 @@ pub enum FrontendMessage {
|
|||
size: (f64, f64),
|
||||
},
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerRevokeBlobUrl {
|
||||
url: String,
|
||||
},
|
||||
TriggerSavePreferences {
|
||||
preferences: PreferencesMessageHandler,
|
||||
},
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy {
|
||||
#[serde(rename = "copyText")]
|
||||
|
@ -126,6 +158,8 @@ pub enum FrontendMessage {
|
|||
multiplier: (f64, f64),
|
||||
},
|
||||
UpdateImageData {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: u64,
|
||||
#[serde(rename = "imageData")]
|
||||
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;
|
||||
|
||||
|
|
|
@ -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 serde::{Deserialize, Serialize};
|
||||
|
|
|
@ -8,7 +8,9 @@ impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
|
|||
fn process_message(&mut self, message: GlobalsMessage, _data: (), _responses: &mut VecDeque<Message>) {
|
||||
match message {
|
||||
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!(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
|
||||
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
|
||||
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),
|
||||
//
|
||||
// DialogMessage
|
||||
entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog),
|
||||
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
|
||||
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
|
||||
entry!(KeyDown(KeyT); modifiers=[Alt], action_dispatch=DebugMessage::ToggleTraceLogs),
|
||||
|
|
|
@ -11,9 +11,7 @@ pub struct InputMapperMessageHandler {
|
|||
}
|
||||
|
||||
impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, ActionList)> for InputMapperMessageHandler {
|
||||
fn process_message(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque<Message>) {
|
||||
let (input, actions) = data;
|
||||
|
||||
fn process_message(&mut self, message: InputMapperMessage, (input, actions): (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque<Message>) {
|
||||
if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions) {
|
||||
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::*;
|
||||
|
||||
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_mouse::{MouseKeys, MouseState, ViewportBounds};
|
||||
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
|
||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub use graphene::DocumentResponse;
|
||||
|
@ -183,7 +183,7 @@ impl InputPreprocessorMessageHandler {
|
|||
mod test {
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
||||
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
|
||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -63,7 +63,20 @@ impl Layout {
|
|||
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
_ => None,
|
||||
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 {
|
||||
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
|
||||
|
||||
use derivative::*;
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct PivotAssist {
|
||||
|
|
|
@ -33,6 +33,11 @@ pub struct PopoverButton {
|
|||
pub header: String,
|
||||
|
||||
pub text: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
|
@ -50,6 +55,11 @@ pub struct TextButton {
|
|||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
@ -6,7 +6,7 @@ use graphene::color::Color;
|
|||
use derivative::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derive(Clone, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct CheckboxInput {
|
||||
pub checked: bool,
|
||||
|
@ -24,6 +24,18 @@ pub struct 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)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorInput {
|
||||
|
@ -64,6 +76,11 @@ pub struct DropdownInput {
|
|||
pub interactive: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
//
|
||||
// Callbacks
|
||||
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
|
||||
|
@ -109,6 +126,11 @@ pub struct FontInput {
|
|||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
@ -159,11 +181,15 @@ pub struct NumberInput {
|
|||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<NumberInput>,
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_increase: WidgetCallback<NumberInput>,
|
||||
|
@ -171,6 +197,10 @@ pub struct NumberInput {
|
|||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
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)]
|
||||
|
@ -246,6 +276,8 @@ pub struct TextAreaInput {
|
|||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
@ -261,6 +293,11 @@ pub struct TextInput {
|
|||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
@ -5,15 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct IconLabel {
|
||||
pub icon: String,
|
||||
|
||||
#[serde(rename = "iconStyle")]
|
||||
pub icon_style: IconStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
|
||||
pub enum IconStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
Node,
|
||||
pub tooltip: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
@ -49,6 +41,11 @@ pub struct TextLabel {
|
|||
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Body
|
||||
pub value: String,
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ pub enum Message {
|
|||
#[child]
|
||||
Portfolio(PortfolioMessage),
|
||||
#[child]
|
||||
Preferences(PreferencesMessage),
|
||||
#[child]
|
||||
Tool(ToolMessage),
|
||||
#[child]
|
||||
Workspace(WorkspaceMessage),
|
||||
|
|
|
@ -10,6 +10,7 @@ pub mod input_preprocessor;
|
|||
pub mod layout;
|
||||
pub mod message;
|
||||
pub mod portfolio;
|
||||
pub mod preferences;
|
||||
pub mod prelude;
|
||||
pub mod tool;
|
||||
pub mod workspace;
|
||||
|
|
|
@ -85,7 +85,7 @@ impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
|
|||
.into(),
|
||||
)
|
||||
} 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(
|
||||
FrontendMessage::UpdateDocumentArtboards {
|
||||
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::LayerId;
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
|
@ -75,6 +74,9 @@ pub enum DocumentMessage {
|
|||
affected_folder_path: Vec<LayerId>,
|
||||
},
|
||||
GroupSelectedLayers,
|
||||
ImaginateClear,
|
||||
ImaginateGenerate,
|
||||
ImaginateTerminate,
|
||||
LayerChanged {
|
||||
affected_layer_path: Vec<LayerId>,
|
||||
},
|
||||
|
@ -120,6 +122,12 @@ pub enum DocumentMessage {
|
|||
SetBlendModeForSelectedLayers {
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
layer_path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
document_id: u64,
|
||||
},
|
||||
SetLayerExpansion {
|
||||
layer_path: Vec<LayerId>,
|
||||
set_expanded: bool,
|
||||
|
@ -140,7 +148,7 @@ pub enum DocumentMessage {
|
|||
SetSnapping {
|
||||
snap: bool,
|
||||
},
|
||||
SetTexboxEditability {
|
||||
SetTextboxEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use super::utility_types::error::EditorError;
|
||||
use super::utility_types::misc::DocumentRenderMode;
|
||||
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::messages::frontend::utility_types::ExportBounds;
|
||||
use crate::messages::frontend::utility_types::{FileType, FrontendImageData};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
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::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
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::folder_layer::FolderLayer;
|
||||
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateStatus};
|
||||
use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
|
||||
use graphene::layers::style::{Fill, RenderData, ViewMode};
|
||||
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]
|
||||
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::*;
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Sub-messages
|
||||
#[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)) => {
|
||||
for response in document_responses {
|
||||
match &response {
|
||||
|
@ -130,7 +139,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
},
|
||||
#[remain::unsorted]
|
||||
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]
|
||||
Navigation(message) => {
|
||||
|
@ -138,25 +147,23 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
}
|
||||
#[remain::unsorted]
|
||||
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]
|
||||
TransformLayer(message) => {
|
||||
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]
|
||||
PropertiesPanel(message) => {
|
||||
self.properties_panel_message_handler.process_message(
|
||||
message,
|
||||
PropertiesPanelMessageHandlerData {
|
||||
artwork_document: &self.graphene_document,
|
||||
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())),
|
||||
font_cache,
|
||||
},
|
||||
responses,
|
||||
);
|
||||
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
|
||||
artwork_document: &self.graphene_document,
|
||||
artboard_document: &self.artboard_message_handler.artboards_graphene_document,
|
||||
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())),
|
||||
};
|
||||
self.properties_panel_message_handler
|
||||
.process_message(message, (persistent_data, properties_panel_message_handler_data), responses);
|
||||
}
|
||||
|
||||
// Messages
|
||||
|
@ -166,20 +173,20 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
}
|
||||
AddSelectedLayers { 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
|
||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.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 } => {
|
||||
self.backup(responses);
|
||||
let (paths, boxes): (Vec<_>, Vec<_>) = self
|
||||
.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();
|
||||
|
||||
let axis = match axis {
|
||||
|
@ -187,7 +194,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
AlignAxis::Y => DVec2::Y,
|
||||
};
|
||||
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 {
|
||||
AlignAggregate::Min => combined_box[0],
|
||||
AlignAggregate::Max => combined_box[1],
|
||||
|
@ -308,21 +315,21 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
scale_factor,
|
||||
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
|
||||
use crate::messages::frontend::utility_types::ExportBounds;
|
||||
let bbox = match bounds {
|
||||
ExportBounds::AllArtwork => self.all_layer_bounds(font_cache),
|
||||
ExportBounds::Selection => self.selected_visible_layers_bounding_box(font_cache),
|
||||
ExportBounds::Artboard(id) => self.artboard_message_handler.artboards_graphene_document.layer(&[id]).ok().and_then(|layer| layer.aabb(font_cache)),
|
||||
let bounds = match bounds {
|
||||
ExportBounds::AllArtwork => self.all_layer_bounds(&persistent_data.font_cache),
|
||||
ExportBounds::Selection => self.selected_visible_layers_bounding_box(&persistent_data.font_cache),
|
||||
ExportBounds::Artboard(id) => self
|
||||
.artboard_message_handler
|
||||
.artboards_graphene_document
|
||||
.layer(&[id])
|
||||
.ok()
|
||||
.and_then(|layer| layer.aabb(&persistent_data.font_cache)),
|
||||
}
|
||||
.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 name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
|
@ -330,16 +337,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
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 {
|
||||
responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into());
|
||||
} else {
|
||||
|
@ -354,7 +351,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
FlipAxis::X => 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 bbox_trans = DAffine2::from_translation(-center);
|
||||
for path in self.selected_layers() {
|
||||
|
@ -403,12 +400,73 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
.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 } => {
|
||||
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(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 {
|
||||
folder_path,
|
||||
|
@ -466,6 +524,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
);
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateImageData {
|
||||
document_id,
|
||||
image_data: vec![FrontendImageData { path: path.clone(), image_data, mime }],
|
||||
}
|
||||
.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()),
|
||||
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(
|
||||
FrontendMessage::UpdateDocumentArtwork {
|
||||
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 viewport_size = ipp.viewport_bounds.size();
|
||||
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 bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
|
||||
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
|
||||
|
@ -621,17 +680,44 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
}
|
||||
SetBlendModeForSelectedLayers { blend_mode } => {
|
||||
self.backup(responses);
|
||||
for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
for path in self.selected_layers() {
|
||||
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 } => {
|
||||
self.layer_metadata_mut(&layer_path).expanded = set_expanded;
|
||||
responses.push_back(DocumentStructureChanged.into());
|
||||
responses.push_back(LayerChanged { affected_layer_path: layer_path }.into())
|
||||
}
|
||||
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
|
||||
if layer.name != name {
|
||||
self.backup(responses);
|
||||
|
@ -664,7 +750,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
SetSnapping { snap } => {
|
||||
self.snapping_enabled = snap;
|
||||
}
|
||||
SetTexboxEditability { path, editable } => {
|
||||
SetTextboxEditability { path, editable } => {
|
||||
let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap();
|
||||
responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into());
|
||||
if editable {
|
||||
|
@ -760,7 +846,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
responses.push_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. }.into());
|
||||
}
|
||||
ZoomCanvasToFitAll => {
|
||||
if let Some(bounds) = self.document_bounds(font_cache) {
|
||||
if let Some(bounds) = self.document_bounds(&persistent_data.font_cache) {
|
||||
responses.push_back(
|
||||
NavigationMessage::FitViewportToBounds {
|
||||
bounds,
|
||||
|
@ -812,6 +898,99 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
}
|
||||
|
||||
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 {
|
||||
let val = serde_json::to_string(self);
|
||||
// We fully expect the serialization to succeed
|
||||
|
@ -881,11 +1060,20 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
|
||||
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]> {
|
||||
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]> {
|
||||
|
@ -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
|
||||
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
|
||||
|
@ -1022,7 +1210,7 @@ impl DocumentMessageHandler {
|
|||
// Compute the indices for each layer to be able to sort them
|
||||
let mut layers_with_indices: Vec<(&[LayerId], Vec<usize>)> = paths
|
||||
// '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| {
|
||||
// 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) {
|
||||
|
@ -1093,7 +1281,7 @@ impl DocumentMessageHandler {
|
|||
Some((document, layer_metadata)) => {
|
||||
// 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
|
||||
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 {
|
||||
responses.push_back(BroadcastEvent::SelectionChanged.into());
|
||||
|
@ -1123,7 +1311,7 @@ impl DocumentMessageHandler {
|
|||
Some((document, layer_metadata)) => {
|
||||
// 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
|
||||
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 {
|
||||
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
|
||||
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>) {
|
||||
match data {
|
||||
LayerDataType::Folder(f) => {
|
||||
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) {
|
||||
LayerDataType::Folder(folder) => {
|
||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers().iter()) {
|
||||
path.push(*id);
|
||||
walk_layers(&layer.data, path, image_data, fonts);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(txt) => {
|
||||
fonts.insert(txt.font.clone());
|
||||
LayerDataType::Text(text) => {
|
||||
fonts.insert(text.font.clone());
|
||||
}
|
||||
LayerDataType::Image(img) => image_data.push(FrontendImageData {
|
||||
LayerDataType::Image(image) => image_data.push(FrontendImageData {
|
||||
path: path.clone(),
|
||||
image_data: img.image_data.clone(),
|
||||
mime: img.mime.clone(),
|
||||
image_data: image.image_data.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();
|
||||
walk_layers(root, &mut path, &mut image_data, &mut fonts);
|
||||
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 {
|
||||
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 {
|
||||
#[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::*;
|
||||
|
||||
let (document, ipp) = data;
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
DecreaseCanvasZoom { center_on_mouse } => {
|
||||
|
|
|
@ -31,7 +31,7 @@ impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
|
|||
responses.push_back(
|
||||
FrontendMessage::UpdateDocumentOverlays {
|
||||
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)
|
||||
} else {
|
||||
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::prelude::*;
|
||||
|
||||
use graphene::layers::imaginate_layer::ImaginateSamplingMethod;
|
||||
use graphene::layers::style::{Fill, Stroke};
|
||||
use graphene::LayerId;
|
||||
|
||||
|
@ -26,6 +27,19 @@ pub enum PropertiesPanelMessage {
|
|||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
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 },
|
||||
UpdateSelectedDocumentProperties,
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties};
|
||||
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::misc::LayoutTarget;
|
||||
use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation;
|
||||
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::{LayerId, Operation};
|
||||
|
@ -15,14 +17,13 @@ pub struct PropertiesPanelMessageHandler {
|
|||
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]
|
||||
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 {
|
||||
artwork_document,
|
||||
artboard_document,
|
||||
selected_layers,
|
||||
font_cache,
|
||||
} = data;
|
||||
let get_document = |document_selector: TargetDocument| match document_selector {
|
||||
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 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 }));
|
||||
}
|
||||
|
@ -136,8 +137,8 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
if let Some((path, target_document)) = self.active_selection.clone() {
|
||||
let layer = get_document(target_document).layer(&path).unwrap();
|
||||
match target_document {
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, font_cache),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, font_cache),
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, persistent_data),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, persistent_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +149,62 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
}
|
||||
.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::misc::LayoutTarget;
|
||||
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::input_widgets::{ColorInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, IconStyle, Separator, SeparatorDirection, SeparatorType, TextLabel};
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{
|
||||
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 glam::{DAffine2, DVec2};
|
||||
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::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||
use graphene::layers::text_layer::{FontCache, TextLayer};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeArtboard".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Artboard".into(),
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
|
@ -81,7 +86,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||
} else {
|
||||
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 {
|
||||
name: "Artboard".into(),
|
||||
|
@ -139,7 +144,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
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(),
|
||||
unit: " px".into(),
|
||||
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,
|
||||
})),
|
||||
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(),
|
||||
unit: " px".into(),
|
||||
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 {
|
||||
widgets: vec![
|
||||
match &layer.data {
|
||||
LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeFolder".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Folder".into(),
|
||||
})),
|
||||
LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeShape".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Shape".into(),
|
||||
})),
|
||||
LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeText".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Text".into(),
|
||||
})),
|
||||
LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
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 {
|
||||
|
@ -272,24 +281,31 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
|
|||
let properties_body = match &layer.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
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 {
|
||||
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) => {
|
||||
vec![
|
||||
node_section_transform(layer, font_cache),
|
||||
node_section_transform(layer, persistent_data),
|
||||
node_section_font(text),
|
||||
node_section_fill(text.path_style.fill()).expect("Text should have fill"),
|
||||
node_section_stroke(&text.path_style.stroke().unwrap_or_default()),
|
||||
]
|
||||
}
|
||||
LayerDataType::Image(_) => {
|
||||
vec![node_section_transform(layer, font_cache)]
|
||||
vec![node_section_transform(layer, persistent_data)]
|
||||
}
|
||||
_ => {
|
||||
vec![]
|
||||
LayerDataType::Imaginate(imaginate) => {
|
||||
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 {
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache));
|
||||
fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> LayoutGroup {
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&persistent_data.font_cache));
|
||||
LayoutGroup::Section {
|
||||
name: "Transform".into(),
|
||||
layout: vec![
|
||||
|
@ -442,7 +458,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
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(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
|
@ -459,7 +475,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
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(),
|
||||
unit: " px".into(),
|
||||
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 {
|
||||
let font = layer.font.clone();
|
||||
let size = layer.size;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use graphene::document::Document as GrapheneDocument;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -8,7 +7,6 @@ pub struct PropertiesPanelMessageHandlerData<'a> {
|
|||
pub artwork_document: &'a GrapheneDocument,
|
||||
pub artboard_document: &'a GrapheneDocument,
|
||||
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
|
||||
pub font_cache: &'a FontCache,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -72,7 +72,7 @@ impl LayerPanelEntry {
|
|||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
let mut thumbnail = String::new();
|
||||
let mut svg_defs = String::new();
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None);
|
||||
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
|
||||
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
|
||||
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
|
||||
|
|
|
@ -44,12 +44,11 @@ pub enum DocumentMode {
|
|||
|
||||
impl fmt::Display for DocumentMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
DocumentMode::DesignMode => "Design Mode".to_string(),
|
||||
DocumentMode::SelectMode => "Select Mode".to_string(),
|
||||
DocumentMode::GuideMode => "Guide Mode".to_string(),
|
||||
};
|
||||
write!(f, "{}", text)
|
||||
match self {
|
||||
DocumentMode::DesignMode => write!(f, "Design Mode"),
|
||||
DocumentMode::SelectMode => write!(f, "Select Mode"),
|
||||
DocumentMode::GuideMode => write!(f, "Guide Mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,33 +62,7 @@ impl DocumentMode {
|
|||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub enum DocumentRenderMode<'a> {
|
||||
Root,
|
||||
OnlyBelowLayerInFolder(&'a [LayerId]),
|
||||
}
|
||||
|
|
|
@ -84,6 +84,13 @@ impl PropertyHolder for MenuBarMessageHandler {
|
|||
..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(
|
||||
|
|
|
@ -3,6 +3,7 @@ mod portfolio_message_handler;
|
|||
|
||||
pub mod document;
|
||||
pub mod menu_bar;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
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::prelude::*;
|
||||
|
||||
use graphene::layers::text_layer::Font;
|
||||
use graphene::layers::{imaginate_layer::ImaginateStatus, text_layer::Font};
|
||||
use graphene::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -13,12 +14,17 @@ pub enum PortfolioMessage {
|
|||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
MenuBar(MenuBarMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
MenuBar(MenuBarMessage),
|
||||
Document(DocumentMessage),
|
||||
|
||||
// Messages
|
||||
#[remain::unsorted]
|
||||
DocumentPassMessage {
|
||||
document_id: u64,
|
||||
message: DocumentMessage,
|
||||
},
|
||||
AutoSaveActiveDocument,
|
||||
AutoSaveDocument {
|
||||
document_id: u64,
|
||||
|
@ -45,8 +51,31 @@ pub enum PortfolioMessage {
|
|||
data: Vec<u8>,
|
||||
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,
|
||||
LoadDocumentResources,
|
||||
LoadDocumentResources {
|
||||
document_id: u64,
|
||||
},
|
||||
LoadFont {
|
||||
font: Font,
|
||||
is_default: bool,
|
||||
|
@ -66,6 +95,7 @@ pub enum PortfolioMessage {
|
|||
document_is_saved: bool,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
// TODO: Paste message is unused, delete it?
|
||||
Paste {
|
||||
clipboard: Clipboard,
|
||||
},
|
||||
|
@ -84,6 +114,12 @@ pub enum PortfolioMessage {
|
|||
SetActiveDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
document_id: u64,
|
||||
layer_path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
},
|
||||
UpdateDocumentWidgets,
|
||||
UpdateOpenDocumentsList,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use super::utility_types::PersistentData;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
|
||||
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::misc::LayoutTarget;
|
||||
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 graphene::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use graphene::layers::text_layer::{Font, FontCache};
|
||||
use graphene::layers::text_layer::Font;
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
@ -18,34 +20,39 @@ pub struct PortfolioMessageHandler {
|
|||
document_ids: Vec<u64>,
|
||||
active_document_id: Option<u64>,
|
||||
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]
|
||||
fn process_message(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
use DocumentMessage::*;
|
||||
use PortfolioMessage::*;
|
||||
|
||||
fn process_message(&mut self, message: PortfolioMessage, (ipp, preferences): (&InputPreprocessorMessageHandler, &PreferencesMessageHandler), responses: &mut VecDeque<Message>) {
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
Document(message) => {
|
||||
if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) {
|
||||
document.process_message(message, (ipp, &self.font_cache), responses)
|
||||
PortfolioMessage::MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
|
||||
#[remain::unsorted]
|
||||
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
|
||||
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 {
|
||||
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into());
|
||||
}
|
||||
}
|
||||
AutoSaveDocument { document_id } => {
|
||||
PortfolioMessage::AutoSaveDocument { document_id } => {
|
||||
let document = self.documents.get(&document_id).unwrap();
|
||||
responses.push_back(
|
||||
FrontendMessage::TriggerIndexedDbWriteDocument {
|
||||
|
@ -60,12 +67,12 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
.into(),
|
||||
)
|
||||
}
|
||||
CloseActiveDocumentWithConfirmation => {
|
||||
PortfolioMessage::CloseActiveDocumentWithConfirmation => {
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into());
|
||||
}
|
||||
}
|
||||
CloseAllDocuments => {
|
||||
PortfolioMessage::CloseAllDocuments => {
|
||||
if self.active_document_id.is_some() {
|
||||
responses.push_back(PropertiesPanelMessage::Deactivate.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::UpdateOpenDocumentsList.into());
|
||||
}
|
||||
CloseDocument { document_id } => {
|
||||
PortfolioMessage::CloseDocument { document_id } => {
|
||||
let document_index = self.document_index(document_id);
|
||||
self.documents.remove(&document_id);
|
||||
self.document_ids.remove(document_index);
|
||||
|
@ -107,9 +114,9 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
}
|
||||
|
||||
// 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(RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||
if let Some(document) = self.active_document() {
|
||||
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();
|
||||
if target_document.is_saved() {
|
||||
responses.push_back(BroadcastEvent::ToolAbort.into());
|
||||
|
@ -134,7 +141,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
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`
|
||||
if let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get(&id)) {
|
||||
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
|
||||
|
@ -162,24 +169,24 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
}
|
||||
}
|
||||
}
|
||||
Cut { clipboard } => {
|
||||
responses.push_back(Copy { clipboard }.into());
|
||||
responses.push_back(DeleteSelectedLayers.into());
|
||||
PortfolioMessage::Cut { clipboard } => {
|
||||
responses.push_back(PortfolioMessage::Copy { clipboard }.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
}
|
||||
DestroyAllDocuments => {
|
||||
PortfolioMessage::DestroyAllDocuments => {
|
||||
// Empty the list of internal document data
|
||||
self.documents.clear();
|
||||
self.document_ids.clear();
|
||||
self.active_document_id = None;
|
||||
}
|
||||
FontLoaded {
|
||||
PortfolioMessage::FontLoaded {
|
||||
font_family,
|
||||
font_style,
|
||||
preview_url,
|
||||
data,
|
||||
is_default,
|
||||
} => {
|
||||
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
|
||||
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() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
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
|
||||
if self.active_document().is_some() {
|
||||
responses.push_back(FrontendMessage::TriggerImport.into());
|
||||
}
|
||||
}
|
||||
LoadDocumentResources => {
|
||||
if let Some(document) = self.active_document_mut() {
|
||||
document.load_layer_resources(responses, &document.graphene_document.root.data, Vec::new());
|
||||
PortfolioMessage::LoadDocumentResources { document_id } => {
|
||||
if let Some(document) = self.document_mut(document_id) {
|
||||
document.load_layer_resources(responses, &document.graphene_document.root.data, Vec::new(), document_id);
|
||||
}
|
||||
}
|
||||
LoadFont { font, is_default } => {
|
||||
if !self.font_cache.loaded_font(&font) {
|
||||
PortfolioMessage::LoadFont { font, is_default } => {
|
||||
if !self.persistent_data.font_cache.loaded_font(&font) {
|
||||
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into());
|
||||
}
|
||||
}
|
||||
NewDocumentWithName { name } => {
|
||||
PortfolioMessage::NewDocumentWithName { name } => {
|
||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||
let document_id = generate_uuid();
|
||||
if self.active_document().is_some() {
|
||||
|
@ -213,7 +261,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
|
||||
self.load_document(new_document, document_id, responses);
|
||||
}
|
||||
NextDocument => {
|
||||
PortfolioMessage::NextDocument => {
|
||||
if let Some(active_document_id) = self.active_document_id {
|
||||
let current_index = self.document_index(active_document_id);
|
||||
let next_index = (current_index + 1) % self.document_ids.len();
|
||||
|
@ -222,11 +270,11 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
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
|
||||
responses.push_back(FrontendMessage::TriggerOpenDocument.into());
|
||||
}
|
||||
OpenDocumentFile {
|
||||
PortfolioMessage::OpenDocumentFile {
|
||||
document_name,
|
||||
document_serialized_content,
|
||||
} => {
|
||||
|
@ -240,7 +288,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
.into(),
|
||||
);
|
||||
}
|
||||
OpenDocumentFileWithId {
|
||||
PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
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| {
|
||||
document
|
||||
.graphene_document
|
||||
|
@ -270,20 +319,20 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
});
|
||||
|
||||
if let Some(folder) = shallowest_common_folder {
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(
|
||||
PasteIntoFolder {
|
||||
PortfolioMessage::PasteIntoFolder {
|
||||
clipboard,
|
||||
folder_path: folder.to_vec(),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(CommitTransaction.into());
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
}
|
||||
}
|
||||
PasteIntoFolder {
|
||||
PortfolioMessage::PasteIntoFolder {
|
||||
clipboard,
|
||||
folder_path: path,
|
||||
insert_index,
|
||||
|
@ -300,10 +349,10 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
}
|
||||
.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(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
layer: Box::new(entry.layer.clone()),
|
||||
destination_path,
|
||||
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 Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
|
||||
let shallowest_common_folder = document
|
||||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
|
||||
for entry in data.iter().rev() {
|
||||
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
|
||||
|
@ -342,10 +391,10 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
}
|
||||
.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(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
layer: Box::new(entry.layer.clone()),
|
||||
destination_path,
|
||||
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 {
|
||||
let len = self.document_ids.len();
|
||||
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());
|
||||
}
|
||||
}
|
||||
SelectDocument { document_id } => {
|
||||
PortfolioMessage::SelectDocument { document_id } => {
|
||||
if let Some(document) = self.active_document() {
|
||||
if !document.is_saved() {
|
||||
// Safe to unwrap since we know that there is an active document
|
||||
|
@ -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
|
||||
responses.push_back(SetActiveDocument { document_id }.into());
|
||||
responses.push_back(PortfolioMessage::SetActiveDocument { document_id }.into());
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||
for layer in self.documents.get(&document_id).unwrap().layer_metadata.keys() {
|
||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
||||
|
@ -397,13 +446,27 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
responses.push_back(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
|
||||
}
|
||||
SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
|
||||
UpdateDocumentWidgets => {
|
||||
PortfolioMessage::SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
|
||||
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() {
|
||||
document.update_document_widgets(responses);
|
||||
}
|
||||
}
|
||||
UpdateOpenDocumentsList => {
|
||||
PortfolioMessage::UpdateOpenDocumentsList => {
|
||||
// Send the list of document tab names
|
||||
let open_documents = self
|
||||
.document_ids
|
||||
|
@ -449,6 +512,14 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||
}
|
||||
|
||||
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> {
|
||||
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))
|
||||
}
|
||||
|
||||
pub fn active_document_id(&self) -> Option<u64> {
|
||||
self.active_document_id
|
||||
}
|
||||
|
||||
pub fn generate_new_document_name(&self) -> String {
|
||||
let mut doc_title_numbers = self
|
||||
.ordered_document_iterator()
|
||||
|
@ -486,11 +561,11 @@ impl PortfolioMessageHandler {
|
|||
new_document
|
||||
.layer_metadata
|
||||
.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())
|
||||
.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);
|
||||
|
||||
|
@ -502,12 +577,12 @@ impl PortfolioMessageHandler {
|
|||
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.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(ToolMessage::InitTools.into());
|
||||
responses.push_back(PropertiesPanelMessage::Init.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.
|
||||
|
@ -518,8 +593,4 @@ impl PortfolioMessageHandler {
|
|||
fn document_index(&self, document_id: u64) -> usize {
|
||||
self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids")
|
||||
}
|
||||
|
||||
pub fn font_cache(&self) -> &FontCache {
|
||||
&self.font_cache
|
||||
}
|
||||
}
|
||||
|
|
58
editor/src/messages/portfolio/utility_types.rs
Normal file
|
@ -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
editor/src/messages/preferences/mod.rs
Normal 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;
|
13
editor/src/messages/preferences/preferences_message.rs
Normal file
|
@ -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(),
|
||||
);
|
||||
}
|
|
@ -6,6 +6,7 @@ pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscrimin
|
|||
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
|
||||
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::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
|
||||
pub use crate::messages::dialog::{DialogMessage, DialogMessageDiscriminant, DialogMessageHandler};
|
||||
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant};
|
||||
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::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
|
||||
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::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::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
|
||||
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::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
|
||||
|
|
|
@ -75,6 +75,9 @@ pub enum ToolMessage {
|
|||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
// Detail(DetailToolMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Imaginate(ImaginateToolMessage),
|
||||
|
||||
// Messages
|
||||
#[remain::unsorted]
|
||||
|
@ -109,6 +112,9 @@ pub enum ToolMessage {
|
|||
#[remain::unsorted]
|
||||
ActivateToolShape,
|
||||
|
||||
#[remain::unsorted]
|
||||
ActivateToolImaginate,
|
||||
|
||||
ActivateTool {
|
||||
tool_type: ToolType,
|
||||
},
|
||||
|
|
|
@ -2,21 +2,25 @@ use super::utility_types::{tool_message_to_tool_type, ToolFsmState};
|
|||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToolMessageHandler {
|
||||
tool_state: ToolFsmState,
|
||||
}
|
||||
|
||||
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache)> for ToolMessageHandler {
|
||||
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocessorMessageHandler, &PersistentData)> for ToolMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) {
|
||||
let (document, input, font_cache) = data;
|
||||
fn process_message(
|
||||
&mut self,
|
||||
message: ToolMessage,
|
||||
(document, document_id, input, persistent_data): (&DocumentMessageHandler, u64, &InputPreprocessorMessageHandler, &PersistentData),
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Messages
|
||||
|
@ -52,6 +56,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
|||
#[remain::unsorted]
|
||||
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 } => {
|
||||
let tool_data = &mut self.tool_state.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| {
|
||||
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
|
||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||
tool.process_message(tool_abort_message, (document, document_data, input, font_cache), responses);
|
||||
tool.process_message(tool_abort_message, (document, document_id, document_data, input, &persistent_data.font_cache), responses);
|
||||
}
|
||||
|
||||
if update_hints_and_cursor {
|
||||
tool.process_message(ToolMessage::UpdateHints, (document, document_data, input, font_cache), responses);
|
||||
tool.process_message(ToolMessage::UpdateCursor, (document, document_data, input, font_cache), responses);
|
||||
tool.process_message(ToolMessage::UpdateHints, (document, document_id, document_data, input, &persistent_data.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
|
||||
tool_data
|
||||
.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
|
||||
.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 => {
|
||||
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 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,
|
||||
ActivateToolFill,
|
||||
ActivateToolGradient,
|
||||
|
||||
ActivateToolPath,
|
||||
ActivateToolPen,
|
||||
ActivateToolFreehand,
|
||||
|
@ -208,6 +216,9 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
|||
ActivateToolRectangle,
|
||||
ActivateToolEllipse,
|
||||
ActivateToolShape,
|
||||
|
||||
ActivateToolImaginate,
|
||||
|
||||
SelectRandomPrimaryColor,
|
||||
ResetColors,
|
||||
SwapColors,
|
||||
|
|
|
@ -132,7 +132,7 @@ impl Fsm for ArtboardToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -122,7 +122,7 @@ impl Fsm for EllipseToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -104,7 +104,7 @@ impl Fsm for EyedropperToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
_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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -105,7 +105,7 @@ impl Fsm for FillToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
_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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -161,7 +161,7 @@ impl Fsm for FreehandToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -335,7 +335,7 @@ impl Fsm for GradientToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
231
editor/src/messages/tool/tool_messages/imaginate_tool.rs
Normal file
|
@ -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,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod eyedropper_tool;
|
|||
pub mod fill_tool;
|
||||
pub mod freehand_tool;
|
||||
pub mod gradient_tool;
|
||||
pub mod imaginate_tool;
|
||||
pub mod line_tool;
|
||||
pub mod navigate_tool;
|
||||
pub mod path_tool;
|
||||
|
|
|
@ -126,7 +126,7 @@ impl Fsm for NavigateToolFsmState {
|
|||
self,
|
||||
message: ToolMessage,
|
||||
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,
|
||||
messages: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -142,7 +142,7 @@ impl Fsm for PathToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -182,7 +182,7 @@ impl Fsm for PenToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -121,7 +121,7 @@ impl Fsm for RectangleToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::application::generate_uuid;
|
||||
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_mouse::ViewportPosition;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
|
@ -374,7 +374,7 @@ impl Fsm for SelectToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
@ -550,12 +550,6 @@ impl Fsm for SelectToolFsmState {
|
|||
.flat_map(snapping::expand_bounds)
|
||||
.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);
|
||||
// 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()) {
|
||||
|
@ -568,6 +562,13 @@ impl Fsm for SelectToolFsmState {
|
|||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
(ResizingBounds, PointerMove { axis_align, center, .. }) => {
|
||||
|
@ -919,8 +920,8 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
|
||||
impl SelectToolData {
|
||||
/// 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>) {
|
||||
/// 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, document_id: u64, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
self.not_duplicated_layers = Some(self.layers_dragging.clone());
|
||||
|
@ -938,19 +939,34 @@ impl SelectToolData {
|
|||
|
||||
// Copy the layers.
|
||||
// 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(),
|
||||
Err(e) => {
|
||||
warn!("Could not access selected layer {:?}: {:?}", layer_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_metadata = *document.layer_metadata(layer_path);
|
||||
*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(
|
||||
Operation::InsertLayer {
|
||||
layer,
|
||||
layer: Box::new(layer),
|
||||
destination_path: layer_path.clone(),
|
||||
insert_index: -1,
|
||||
}
|
||||
|
@ -964,10 +980,14 @@ impl SelectToolData {
|
|||
}
|
||||
.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>) {
|
||||
let originals = match self.not_duplicated_layers.take() {
|
||||
Some(x) => x,
|
||||
|
|
|
@ -162,7 +162,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -170,7 +170,7 @@ impl Fsm for SplineToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
|
@ -274,7 +274,7 @@ impl Fsm for TextToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
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,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
@ -302,7 +302,7 @@ impl Fsm for TextToolFsmState {
|
|||
{
|
||||
if state == TextToolFsmState::Editing {
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
|
@ -313,7 +313,7 @@ impl Fsm for TextToolFsmState {
|
|||
tool_data.path = l.clone();
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: true,
|
||||
}
|
||||
|
@ -358,7 +358,7 @@ impl Fsm for TextToolFsmState {
|
|||
);
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: true,
|
||||
}
|
||||
|
@ -376,7 +376,7 @@ impl Fsm for TextToolFsmState {
|
|||
} else {
|
||||
// Removing old text as editable
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ impl Fsm for TextToolFsmState {
|
|||
(state, Abort) => {
|
||||
if state == TextToolFsmState::Editing {
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
|
@ -420,7 +420,7 @@ impl Fsm for TextToolFsmState {
|
|||
);
|
||||
|
||||
responses.push_back(
|
||||
DocumentMessage::SetTexboxEditability {
|
||||
DocumentMessage::SetTextboxEditability {
|
||||
path: tool_data.path.clone(),
|
||||
editable: false,
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use graphene::layers::text_layer::FontCache;
|
|||
use serde::{Deserialize, Serialize};
|
||||
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 {}
|
||||
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 {
|
||||
let tool_groups_layout = list_tools_in_groups()
|
||||
.iter()
|
||||
.map(|tool_group| tool_group.iter().map(|tool| ToolEntry {
|
||||
tooltip: tool.tooltip(),
|
||||
tooltip_shortcut: action_keys!(tool_type_to_activate_tool_message(tool.tool_type())),
|
||||
icon_name: tool.icon_name(),
|
||||
tool_type: tool.tool_type(),
|
||||
.map(|tool_group| tool_group.iter().map(|tool_availability| {
|
||||
match tool_availability {
|
||||
ToolAvailability::Available(tool) => ToolEntry {
|
||||
tooltip: tool.tooltip(),
|
||||
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<_>>())
|
||||
.chain(coming_soon_tools())
|
||||
.flat_map(|group| {
|
||||
let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Vertical,
|
||||
|
@ -189,6 +193,7 @@ impl PropertyHolder for ToolData {
|
|||
}),
|
||||
}))
|
||||
});
|
||||
|
||||
separator.chain(buttons)
|
||||
})
|
||||
// Skip the initial separator
|
||||
|
@ -201,7 +206,7 @@ impl PropertyHolder for ToolData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToolEntry {
|
||||
pub tooltip: String,
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
@ -220,7 +225,14 @@ impl Default for ToolFsmState {
|
|||
ToolFsmState {
|
||||
tool_data: ToolData {
|
||||
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 {
|
||||
primary_color: Color::BLACK,
|
||||
|
@ -265,76 +277,81 @@ pub enum ToolType {
|
|||
Patch,
|
||||
Detail,
|
||||
Relight,
|
||||
Imaginate,
|
||||
}
|
||||
|
||||
enum ToolAvailability {
|
||||
Available(Box<Tool>),
|
||||
ComingSoon(ToolEntry),
|
||||
}
|
||||
|
||||
/// 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![
|
||||
// General tool group
|
||||
Box::new(select_tool::SelectTool::default()),
|
||||
Box::new(artboard_tool::ArtboardTool::default()),
|
||||
Box::new(navigate_tool::NavigateTool::default()),
|
||||
Box::new(eyedropper_tool::EyedropperTool::default()),
|
||||
Box::new(fill_tool::FillTool::default()),
|
||||
Box::new(gradient_tool::GradientTool::default()),
|
||||
ToolAvailability::Available(Box::new(select_tool::SelectTool::default())),
|
||||
ToolAvailability::Available(Box::new(artboard_tool::ArtboardTool::default())),
|
||||
ToolAvailability::Available(Box::new(navigate_tool::NavigateTool::default())),
|
||||
ToolAvailability::Available(Box::new(eyedropper_tool::EyedropperTool::default())),
|
||||
ToolAvailability::Available(Box::new(fill_tool::FillTool::default())),
|
||||
ToolAvailability::Available(Box::new(gradient_tool::GradientTool::default())),
|
||||
],
|
||||
vec![
|
||||
// Vector tool group
|
||||
Box::new(path_tool::PathTool::default()),
|
||||
Box::new(pen_tool::PenTool::default()),
|
||||
Box::new(freehand_tool::FreehandTool::default()),
|
||||
Box::new(spline_tool::SplineTool::default()),
|
||||
Box::new(line_tool::LineTool::default()),
|
||||
Box::new(rectangle_tool::RectangleTool::default()),
|
||||
Box::new(ellipse_tool::EllipseTool::default()),
|
||||
Box::new(shape_tool::ShapeTool::default()),
|
||||
Box::new(text_tool::TextTool::default()),
|
||||
ToolAvailability::Available(Box::new(path_tool::PathTool::default())),
|
||||
ToolAvailability::Available(Box::new(pen_tool::PenTool::default())),
|
||||
ToolAvailability::Available(Box::new(freehand_tool::FreehandTool::default())),
|
||||
ToolAvailability::Available(Box::new(spline_tool::SplineTool::default())),
|
||||
ToolAvailability::Available(Box::new(line_tool::LineTool::default())),
|
||||
ToolAvailability::Available(Box::new(rectangle_tool::RectangleTool::default())),
|
||||
ToolAvailability::Available(Box::new(ellipse_tool::EllipseTool::default())),
|
||||
ToolAvailability::Available(Box::new(shape_tool::ShapeTool::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 {
|
||||
match tool_message {
|
||||
// General tool group
|
||||
|
@ -363,6 +380,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
|
|||
// ToolMessage::Patch(_) => ToolType::Patch,
|
||||
// ToolMessage::Detail(_) => ToolType::Detail,
|
||||
// ToolMessage::Relight(_) => ToolType::Relight,
|
||||
ToolMessage::Imaginate(_) => ToolType::Imaginate,
|
||||
_ => panic!(
|
||||
"Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
|
||||
tool_message
|
||||
|
@ -398,6 +416,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
|
|||
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,
|
||||
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
|
||||
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
|
||||
ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
|
||||
_ => panic!(
|
||||
"Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}",
|
||||
tool_type
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::application::set_uuid_seed;
|
|||
use crate::application::Editor;
|
||||
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::portfolio::document::utility_types::misc::Platform;
|
||||
use crate::messages::portfolio::utility_types::Platform;
|
||||
use crate::messages::prelude::*;
|
||||
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">
|
||||
<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 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 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 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 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 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 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 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 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="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="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="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="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="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="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="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="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="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>
|
||||
|
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.7 KiB |
7
frontend/assets/icon-16px-solid/node-imaginate.svg
Normal file
|
@ -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 |
3
frontend/assets/icon-16px-solid/random.svg
Normal file
|
@ -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 |
6
frontend/assets/icon-16px-solid/regenerate.svg
Normal file
|
@ -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 |
4
frontend/assets/icon-16px-solid/reload.svg
Normal file
|
@ -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 |
7
frontend/assets/icon-16px-solid/rescale.svg
Normal file
|
@ -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 |
4
frontend/assets/icon-16px-solid/reset.svg
Normal file
|
@ -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 |
3
frontend/assets/icon-16px-solid/settings.svg
Normal file
|
@ -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 |
11
frontend/assets/icon-24px-two-tone/raster-imaginate-tool.svg
Normal file
|
@ -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 |
|
@ -217,7 +217,6 @@ img {
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { createBlobManager } from "@/io-managers/blob";
|
||||
import { createClipboardManager } from "@/io-managers/clipboard";
|
||||
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
|
||||
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";
|
||||
|
||||
const managerDestructors: {
|
||||
createBlobManager?: () => void;
|
||||
createClipboardManager?: () => void;
|
||||
createHyperlinkManager?: () => void;
|
||||
createInputManager?: () => void;
|
||||
|
@ -285,7 +283,6 @@ export default defineComponent({
|
|||
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.)
|
||||
Object.assign(managerDestructors, {
|
||||
createBlobManager: createBlobManager(this.editor),
|
||||
createClipboardManager: createClipboardManager(this.editor),
|
||||
createHyperlinkManager: createHyperlinkManager(this.editor),
|
||||
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
class="row"
|
||||
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
|
||||
:style="{ height: virtualScrollingEntryHeight || '20px' }"
|
||||
:title="tooltip"
|
||||
@click="() => !entry.disabled && onEntryClick(entry)"
|
||||
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
|
||||
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
|
||||
|
@ -184,6 +185,7 @@ const MenuList = defineComponent({
|
|||
interactive: { type: Boolean as PropType<boolean>, default: false },
|
||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -45,17 +45,14 @@
|
|||
@click.alt="(e: MouseEvent) => e.stopPropagation()"
|
||||
>
|
||||
<LayoutRow class="layer-type-icon">
|
||||
<IconLabel v-if="listing.entry.layerType === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />
|
||||
<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" />
|
||||
<IconLabel :icon="layerTypeData(listing.entry.layerType).icon" :title="layerTypeData(listing.entry.layerType).name" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
||||
<input
|
||||
data-text-input
|
||||
type="text"
|
||||
:value="listing.entry.name"
|
||||
:placeholder="listing.entry.layerType"
|
||||
:placeholder="layerTypeData(listing.entry.layerType).name"
|
||||
:disabled="!listing.editingName"
|
||||
@blur="() => onEditLayerNameDeselect(listing)"
|
||||
@keydown.esc="onEditLayerNameDeselect(listing)"
|
||||
|
@ -268,7 +265,16 @@
|
|||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
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 LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
@ -484,6 +490,9 @@ export default defineComponent({
|
|||
|
||||
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
|
||||
},
|
||||
layerTypeData(layerType: LayerType): LayerTypeData {
|
||||
return layerTypeData(layerType) || { name: "Error", icon: "NodeText" };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeImage'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeImage'" />
|
||||
<TextLabel>Image</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,7 +42,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMask'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeMask'" />
|
||||
<TextLabel>Mask</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
|
@ -69,7 +69,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeTransform'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeTransform'" />
|
||||
<TextLabel>Transform</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -83,7 +83,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMotionBlur'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeMotionBlur'" />
|
||||
<TextLabel>Motion Blur</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
|
@ -110,7 +110,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeShape'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeShape'" />
|
||||
<TextLabel>Shape</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -124,7 +124,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBrushwork'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeBrushwork'" />
|
||||
<TextLabel>Brushwork</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -138,7 +138,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBlur'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeBlur'" />
|
||||
<TextLabel>Blur</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,7 +152,7 @@
|
|||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeGradient'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeGradient'" />
|
||||
<TextLabel>Gradient</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -21,12 +21,6 @@
|
|||
.options-bar {
|
||||
height: 32px;
|
||||
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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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'">
|
||||
<slot></slot>
|
||||
</FloatingMenu>
|
||||
|
@ -58,6 +58,7 @@ import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<() => void>, required: false },
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
:data-emphasized="emphasized || undefined"
|
||||
:data-disabled="disabled || undefined"
|
||||
data-text-button
|
||||
:title="tooltip"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
@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 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({
|
||||
props: {
|
||||
label: { type: String as PropType<string>, required: true },
|
||||
|
@ -95,6 +79,7 @@ export default defineComponent({
|
|||
emphasized: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
||||
|
|
|
@ -61,10 +61,14 @@
|
|||
.body {
|
||||
margin: 0 4px;
|
||||
|
||||
.text-label {
|
||||
.text-label:first-of-type {
|
||||
flex: 0 0 30%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
class="dropdown-box"
|
||||
:class="{ disabled, open }"
|
||||
:style="{ minWidth: `${minWidth}px` }"
|
||||
:title="tooltip"
|
||||
@click="() => !disabled && (open = true)"
|
||||
@blur="(e: FocusEvent) => blur(e)"
|
||||
@keydown="(e: KeyboardEvent) => keydown(e)"
|
||||
|
@ -115,6 +116,7 @@ export default defineComponent({
|
|||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||
interactive: { type: Boolean as PropType<boolean>, default: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
:title="tooltip"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
|
@ -26,6 +27,7 @@
|
|||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
:title="tooltip"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
|
@ -55,6 +57,7 @@
|
|||
padding: 3px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:not(.disabled) label {
|
||||
|
@ -129,6 +132,7 @@ export default defineComponent({
|
|||
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { 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() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
|
@ -87,6 +87,7 @@ export default defineComponent({
|
|||
fontStyle: { type: String as PropType<string>, required: true },
|
||||
isStyle: { 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() {
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
:label="label"
|
||||
:spellcheck="false"
|
||||
:disabled="disabled"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
:tooltip="tooltip"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
|
@ -107,6 +109,8 @@ export default defineComponent({
|
|||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||
incrementFactor: { type: Number as PropType<number>, default: 1 },
|
||||
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
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
|
@ -122,7 +126,7 @@ export default defineComponent({
|
|||
onTextFocused() {
|
||||
if (this.value === undefined) this.text = "";
|
||||
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;
|
||||
|
||||
|
@ -201,7 +205,7 @@ export default defineComponent({
|
|||
|
||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||
|
||||
return `${displayValue}${this.unit}`;
|
||||
return `${displayValue}${unPluralize(this.unit, value)}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -222,4 +226,9 @@ export default defineComponent({
|
|||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
|
||||
function unPluralize(unit: string, value: number): string {
|
||||
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
||||
return unit;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
v-model:value="inputValue"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
v-model:value="inputValue"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
|
@ -27,6 +28,7 @@ export default defineComponent({
|
|||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
|
@ -31,6 +33,8 @@ export default defineComponent({
|
|||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: 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() {
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<LayoutRow :class="['icon-label', iconSizeClass, iconStyleClass]">
|
||||
<LayoutRow :class="['icon-label', iconSizeClass]" :title="tooltip">
|
||||
<component :is="icon" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
@ -23,35 +23,25 @@
|
|||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.node-style {
|
||||
border-radius: 2px;
|
||||
background: var(--color-node-background);
|
||||
fill: var(--color-node-icon);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
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";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
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: {
|
||||
iconSizeClass(): string {
|
||||
return `size-${ICONS[this.icon].size}`;
|
||||
},
|
||||
iconStyleClass(): string {
|
||||
if (!this.iconStyle || this.iconStyle === "Normal") return "";
|
||||
return `${this.iconStyle.toLowerCase()}-style`;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
LayoutRow,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -37,7 +37,9 @@ export default defineComponent({
|
|||
bold: { type: Boolean as PropType<boolean>, default: false },
|
||||
italic: { 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 },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -3,15 +3,16 @@
|
|||
<LayoutRow class="tab-bar" data-tab-bar :class="{ 'min-widths': tabMinWidths }">
|
||||
<LayoutRow class="tab-group" :scrollableX="true">
|
||||
<LayoutRow
|
||||
class="tab"
|
||||
:class="{ active: tabIndex === tabActiveIndex }"
|
||||
data-tab
|
||||
v-for="(tabLabel, tabIndex) in tabLabels"
|
||||
:key="tabIndex"
|
||||
class="tab"
|
||||
:class="{ active: tabIndex === tabActiveIndex }"
|
||||
:title="tabLabel.tooltip || null"
|
||||
@click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(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" />
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
|
@ -31,7 +32,7 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton :label="'New Document:'" :icon="'File'" :action="() => newDocument()" />
|
||||
<TextButton :label="'New Document'" :icon="'File'" :action="() => newDocument()" />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(true), { key: 'KeyN', label: 'N' }]]" />
|
||||
|
@ -39,7 +40,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton :label="'Open Document:'" :icon="'Folder'" :action="() => openDocument()" />
|
||||
<TextButton :label="'Open Document'" :icon="'Folder'" :action="() => openDocument()" />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(false), { key: 'KeyO', label: 'O' }]]" />
|
||||
|
@ -244,7 +245,7 @@ export default defineComponent({
|
|||
props: {
|
||||
tabMinWidths: { 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 },
|
||||
panelType: { type: String as PropType<PanelTypes>, required: false },
|
||||
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
|
||||
:tabCloseButtons="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)"
|
||||
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
|
||||
:tabActiveIndex="portfolio.state.activeDocumentIndex"
|
||||
|
@ -16,17 +16,17 @@
|
|||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
||||
<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>
|
||||
</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">
|
||||
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
|
||||
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
||||
<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>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -25,8 +25,8 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
|
|||
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
||||
const widgets: WidgetLayout = {
|
||||
layout: [
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, multiline: false }, 0n)] },
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, multiline: true }, 1n)] },
|
||||
{ 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, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
|
||||
],
|
||||
layoutTarget: undefined,
|
||||
};
|
||||
|
|
|
@ -1,99 +1,155 @@
|
|||
import { type PortfolioState } from "@/state-providers/portfolio";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
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_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";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export async function createPersistenceManager(editor: Editor, portfolio: PortfolioState): Promise<() => void> {
|
||||
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));
|
||||
}
|
||||
export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): () => void {
|
||||
async function initialize(): Promise<IDBDatabase> {
|
||||
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
|
||||
return new Promise<IDBDatabase>((resolve) => {
|
||||
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
|
||||
|
||||
async function removeDocument(id: string): Promise<void> {
|
||||
const db = await databaseConnection;
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).delete(id);
|
||||
storeDocumentOrder();
|
||||
}
|
||||
// Handle a version mismatch if `GRAPHITE_INDEXED_DB_VERSION` is now higher than what was saved in the database
|
||||
dbOpenRequest.onupgradeneeded = (): void => {
|
||||
const db = dbOpenRequest.result;
|
||||
|
||||
async function closeDatabaseConnection(): Promise<void> {
|
||||
const db = await databaseConnection;
|
||||
db.close();
|
||||
}
|
||||
// Wipe out all stores when a request is made to upgrade the database version to a newer one
|
||||
GRAPHITE_INDEXEDDB_STORES.forEach((store) => {
|
||||
if (db.objectStoreNames.contains(store.name)) db.deleteObjectStore(store.name);
|
||||
|
||||
// Subscribe to process backend events
|
||||
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);
|
||||
});
|
||||
db.createObjectStore(store.name, { keyPath: store.keyPath });
|
||||
});
|
||||
};
|
||||
|
||||
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
|
||||
const databaseConnection = new Promise<IDBDatabase>((resolve) => {
|
||||
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
|
||||
|
||||
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`
|
||||
// Handle some other error by presenting it to the user
|
||||
dbOpenRequest.onerror = (): void => {
|
||||
const errorText = stripIndents`
|
||||
Documents won't be saved across reloads and later visits.
|
||||
This may be caused by Firefox's private browsing mode.
|
||||
|
||||
Error on opening IndexDB:
|
||||
${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(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);
|
||||
// Resolve the promise on a successful opening of the database connection
|
||||
dbOpenRequest.onsuccess = (): void => {
|
||||
resolve(dbOpenRequest.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,19 @@ import { initWasm } from "@/wasm-communication/editor";
|
|||
|
||||
import App from "@/App.vue";
|
||||
|
||||
// Browser app entry point
|
||||
(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)) {
|
||||
const body = document.body;
|
||||
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>
|
||||
`;
|
||||
body.innerHTML = message + body.innerHTML;
|
||||
return;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize the WASM module for the editor backend
|
||||
await initWasm();
|
||||
|
||||
// Initialize the Vue application
|
||||
createApp(App).mount("#app");
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate } from "@/utility-functions/imaginate";
|
||||
import { rasterizeSVG } from "@/utility-functions/rasterization";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
|
@ -10,8 +11,13 @@ import {
|
|||
TriggerImport,
|
||||
TriggerOpenDocument,
|
||||
TriggerRasterDownload,
|
||||
TriggerImaginateGenerate,
|
||||
TriggerImaginateTerminate,
|
||||
TriggerImaginateCheckServerStatus,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateImageData,
|
||||
TriggerRevokeBlobUrl,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
// 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
|
||||
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 {
|
||||
state: readonly(state) as typeof state,
|
||||
|
|
14
frontend/src/utility-functions/escape.ts
Normal 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");
|
||||
}
|
|
@ -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> };
|
||||
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("");
|
||||
}
|
||||
|
|