Integrate Stable Diffusion with the Imaginate layer (#784)

* Add AI Artist layer

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

* Successfully download the correct image

* Break out image downloading JS into helper function

* Change file download from using data URLs to blob URLs

* WIP rasterize to blob

* Remove dimensions from AI Artist layer

* Successfully draw rasterized image on layer after calculation

* Working txt2img generation based on user prompt

* Add img2img and the main parameters

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

* Fix test

* Rasterize with artboard background color

* Allow aspect ratio stretch of AI Artist images

* Add automatic resolution choosing

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

* Add negative prompt

* Add range bounds for parameter inputs

* Add seed

* Add tiling and restore faces

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

* Fix background color of infinite canvas rasterization

* Escape prompt text sent in the JSON

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

* Fix welcome screen logo color

* Add PreferencesMessageHandler

* Add persistent storage of preferences

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

* Add tooltips to the AI Artist layer properties

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

* Add a refresh button to the connection status

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

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

* Add a generate with random seed button

* Simplify and standardize message names

* Majorly improve robustness of networking code

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

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

* Update API to more recent server commit

* Add support for picking the sampling method

* Add machinery for filtering selected layers with type

* Replace placeholder button icons

* Improve the random icon by tilting the dice

* Use selected_layers() instead of repeating that code

* Fix borrow error

* Change message flow in progress towards fixing #797

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

* Reduce code duplication with rasterization

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

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

Co-authored-by: otdavies <oliver@psyfer.io>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers 2022-10-18 22:33:27 -07:00
parent 562217015d
commit fe1a03fac7
118 changed files with 3767 additions and 678 deletions

View file

@ -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.

View file

@ -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,
);

View file

@ -3,8 +3,6 @@ extern crate graphite_proc_macros;
// `macro_use` puts these macros into scope for all descendant code files
#[macro_use]
mod macros;
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
#[macro_use]
extern crate log;

View file

@ -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,
}

View file

@ -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,
);
}

View file

@ -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)]

View file

@ -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()
})),

View 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;

View file

@ -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,
}

View file

@ -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 },
]))
}
}

View file

@ -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>,
},

View file

@ -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;

View file

@ -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};

View file

@ -8,10 +8,12 @@ impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
fn process_message(&mut self, message: GlobalsMessage, _data: (), _responses: &mut VecDeque<Message>) {
match message {
GlobalsMessage::SetPlatform { platform } => {
if GLOBAL_PLATFORM.get() != Some(&platform) {
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
}
}
}
}
advertise_actions!(GlobalsMessageDiscriminant;
);

View file

@ -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),

View file

@ -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);
}

View file

@ -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;

View file

@ -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]

View file

@ -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);

View file

@ -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 {

View file

@ -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")]

View file

@ -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")]

View file

@ -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,
}

View file

@ -34,6 +34,8 @@ pub enum Message {
#[child]
Portfolio(PortfolioMessage),
#[child]
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),

View file

@ -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;

View file

@ -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),

View file

@ -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,
},

View file

@ -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 {
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(|| path.as_slice())),
font_cache,
},
responses,
);
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());

View file

@ -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 } => {

View file

@ -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("")

View file

@ -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,
}

View file

@ -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());
}
}
}

View file

@ -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;

View file

@ -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)]

View file

@ -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() {

View file

@ -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]),
}

View file

@ -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(

View file

@ -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};

View file

@ -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,
}

View file

@ -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]
MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id {
if let Some(document) = self.documents.get_mut(&document_id) {
document.process_message(message, (document_id, ipp, &self.persistent_data, preferences), responses)
}
}
}
// 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
}
}

View 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,
}

View file

@ -0,0 +1,7 @@
mod preferences_message;
mod preferences_message_handler;
#[doc(inline)]
pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant};
#[doc(inline)]
pub use preferences_message_handler::PreferencesMessageHandler;

View 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 },
}

View file

@ -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(),
);
}

View file

@ -6,6 +6,7 @@ pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscrimin
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::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};

View file

@ -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,
},

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View 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());
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
}

View file

@ -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 {
.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,74 +277,79 @@ 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())),
],
]
}
pub fn coming_soon_tools() -> Vec<Vec<ToolEntry>> {
vec![vec![
ToolEntry {
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,
},
ToolEntry {
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Heal,
icon_name: "RasterHealTool".into(),
tooltip: "Coming Soon: Heal Tool (J)".into(),
tooltip_shortcut: None,
},
ToolEntry {
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Clone,
icon_name: "RasterCloneTool".into(),
tooltip: "Coming Soon: Clone Tool (C)".into(),
tooltip_shortcut: None,
},
ToolEntry {
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Patch,
icon_name: "RasterPatchTool".into(),
tooltip: "Coming Soon: Patch Tool".into(),
tooltip_shortcut: None,
},
ToolEntry {
}),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Detail,
icon_name: "RasterDetailTool".into(),
tooltip: "Coming Soon: Detail Tool (D)".into(),
tooltip_shortcut: None,
},
ToolEntry {
}),
ToolAvailability::ComingSoon(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 {
@ -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

View file

@ -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;

View file

@ -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

Before After
Before After

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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),

View file

@ -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 {

View file

@ -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) => {

View file

@ -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>

View file

@ -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 {

View file

@ -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 },

View file

@ -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 },

View file

@ -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>

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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>

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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>

View file

@ -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 },

View file

@ -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>

View file

@ -1,20 +0,0 @@
import { type Editor } from "@/wasm-communication/editor";
import { UpdateImageData } from "@/wasm-communication/messages";
export function createBlobManager(editor: Editor): void {
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
// Using updateImageData.imageData.buffer returns undefined for some reason?
const buffer = new Uint8Array(element.imageData.values()).buffer;
const blob = new Blob([buffer], { type: element.mime });
// TODO: Call `URL.revokeObjectURL` at the appropriate time to avoid a memory leak
const blobURL = URL.createObjectURL(blob);
const image = await createImageBitmap(blob);
editor.instance.setImageBlobUrl(element.path, blobURL, image.width, image.height);
});
});
}

View file

@ -25,8 +25,8 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
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,
};

View file

@ -1,58 +1,37 @@
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));
}
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();
}
async function closeDatabaseConnection(): Promise<void> {
const db = await databaseConnection;
db.close();
}
// 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);
});
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
const databaseConnection = new Promise<IDBDatabase>((resolve) => {
return new Promise<IDBDatabase>((resolve) => {
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
// 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;
// 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" });
// 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);
db.createObjectStore(store.name, { keyPath: store.keyPath });
});
};
// 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.
@ -64,16 +43,35 @@ export async function createPersistenceManager(editor: Editor, portfolio: Portfo
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
};
// 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;
});
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 => {
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;
@ -83,17 +81,75 @@ export async function createPersistenceManager(editor: Editor, portfolio: Portfo
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
orderedSavedDocuments.forEach((doc: TriggerIndexedDbWriteDocument) => {
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 {
removeDocument(doc.details.id);
await removeDocument(doc.details.id, db);
}
});
resolve(undefined);
};
});
});
return closeDatabaseConnection;
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);
});
const databaseConnection = initialize();
// Destructor
return () => {
databaseConnection.then((connection) => connection.close());
};
}

View file

@ -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;
}

View file

@ -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,

View file

@ -0,0 +1,14 @@
/* eslint-disable no-useless-escape */
/* eslint-disable quotes */
export function escapeJSON(str: string): string {
return str
.replace(/[\\]/g, "\\\\")
.replace(/[\"]/g, '\\"')
.replace(/[\/]/g, "\\/")
.replace(/[\b]/g, "\\b")
.replace(/[\f]/g, "\\f")
.replace(/[\n]/g, "\\n")
.replace(/[\r]/g, "\\r")
.replace(/[\t]/g, "\\t");
}

View file

@ -52,3 +52,31 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
}
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never;
export function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = (): void => resolve(typeof reader.result === "string" ? reader.result : "");
reader.readAsDataURL(blob);
});
}
export async function replaceBlobURLsWithBase64(svg: string): Promise<string> {
const splitByBlobs = svg.split(/(?<=")(blob:.*?)(?=")/);
const onlyBlobs = splitByBlobs.filter((_, i) => i % 2 === 1);
const onlyBlobsConverted = onlyBlobs.map(async (blobURL) => {
const data = await fetch(blobURL);
const dataBlob = await data.blob();
return blobToBase64(dataBlob);
});
const base64Images = await Promise.all(onlyBlobsConverted);
const substituted = splitByBlobs.map((segment, i) => {
if (i % 2 === 0) return segment;
const blobsIndex = Math.floor(i / 2);
return base64Images[blobsIndex];
});
return substituted.join("");
}

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