Skip to content

Commit

Permalink
Vue initialization and FloatingMenu codebase refactoring and cleanup (G…
Browse files Browse the repository at this point in the history
…raphiteEditor#649)

* Clean up Vue initialization-related code

* Rename folder: dispatcher -> interop

* Rename folder: state -> providers

* Comments and clarification

* Rename JS dispatcher to subscription router

* Assorted cleanup and renaming

* Rename: js-messages.ts -> messages.ts

* Comments

* Remove unused Vue component injects

* Clean up coming soon and add warning about freezing the app

* Further cleanup

* Dangerous changes

* Simplify App.vue code

* Move more disparate init code from components into managers

* Rename folder: providers -> state-providers

* Other

* Move Document panel options bar separator to backend

* Add destructors to managers to fix HMR

* Comments and code style

* Rename variable: font -> font_file_url

* Fix async font loading; refactor janky floating menu openness and min-width measurement; fix Vetur errors

* Fix misaligned canvas in viewport until panning on page (re)load

* Add Vue bidirectional props documentation

* More folder renaming for better terminology; add some documentation
  • Loading branch information
Keavon committed May 22, 2022
1 parent 4c3c925 commit fc2d983
Show file tree
Hide file tree
Showing 73 changed files with 1,570 additions and 1,460 deletions.
4 changes: 2 additions & 2 deletions editor/src/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub enum DocumentMessage {
affected_folder_path: Vec<LayerId>,
},
FontLoaded {
font: String,
font_file_url: String,
data: Vec<u8>,
is_default: bool,
},
Expand All @@ -82,7 +82,7 @@ pub enum DocumentMessage {
affected_layer_path: Vec<LayerId>,
},
LoadFont {
font: String,
font_file_url: String,
},
MoveSelectedLayersTo {
folder_path: Vec<LayerId>,
Expand Down
65 changes: 36 additions & 29 deletions editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ impl DocumentMessageHandler {
}
}

// TODO: Loading the default font should happen on a per-application basis, not a per-document basis
pub fn load_default_font(&self, responses: &mut VecDeque<Message>) {
if !self.graphene_document.font_cache.has_default() {
responses.push_back(FrontendMessage::TriggerFontLoadDefault.into())
Expand Down Expand Up @@ -669,30 +670,36 @@ impl DocumentMessageHandler {
}]);

let document_mode_layout = WidgetLayout::new(vec![LayoutRow::Row {
widgets: vec![WidgetHolder::new(Widget::DropdownInput(DropdownInput {
entries: vec![vec![
DropdownEntryData {
label: DocumentMode::DesignMode.to_string(),
icon: DocumentMode::DesignMode.icon_name(),
..DropdownEntryData::default()
},
DropdownEntryData {
label: DocumentMode::SelectMode.to_string(),
icon: DocumentMode::SelectMode.icon_name(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(330) }.into()),
..DropdownEntryData::default()
},
DropdownEntryData {
label: DocumentMode::GuideMode.to_string(),
icon: DocumentMode::GuideMode.icon_name(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(331) }.into()),
..DropdownEntryData::default()
},
]],
selected_index: Some(self.document_mode as u32),
draw_icon: true,
..Default::default()
}))],
widgets: vec![
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
entries: vec![vec![
DropdownEntryData {
label: DocumentMode::DesignMode.to_string(),
icon: DocumentMode::DesignMode.icon_name(),
..DropdownEntryData::default()
},
DropdownEntryData {
label: DocumentMode::SelectMode.to_string(),
icon: DocumentMode::SelectMode.icon_name(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(330) }.into()),
..DropdownEntryData::default()
},
DropdownEntryData {
label: DocumentMode::GuideMode.to_string(),
icon: DocumentMode::GuideMode.icon_name(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(331) }.into()),
..DropdownEntryData::default()
},
]],
selected_index: Some(self.document_mode as u32),
draw_icon: true,
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Section,
direction: SeparatorDirection::Horizontal,
})),
],
}]);

responses.push_back(
Expand Down Expand Up @@ -1107,8 +1114,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
let affected_layer_path = affected_folder_path;
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
}
FontLoaded { font, data, is_default } => {
self.graphene_document.font_cache.insert(font, data, is_default);
FontLoaded { font_file_url, data, is_default } => {
self.graphene_document.font_cache.insert(font_file_url, data, is_default);
responses.push_back(DocumentMessage::DirtyRenderDocument.into());
}
GroupSelectedLayers => {
Expand Down Expand Up @@ -1147,9 +1154,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
self.update_layer_tree_options_bar_widgets(responses);
}
LoadFont { font } => {
if !self.graphene_document.font_cache.loaded_font(&font) {
responses.push_front(FrontendMessage::TriggerFontLoad { font }.into());
LoadFont { font_file_url } => {
if !self.graphene_document.font_cache.loaded_font(&font_file_url) {
responses.push_front(FrontendMessage::TriggerFontLoad { font_file_url }.into());
}
}
MoveSelectedLayersTo {
Expand Down
1 change: 1 addition & 0 deletions editor/src/document/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl PortfolioMessageHandler {
new_document.update_layer_tree_options_bar_widgets(responses);

new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new());
// TODO: Loading the default font should happen on a per-application basis, not a per-document basis
new_document.load_default_font(responses);

self.documents.insert(document_id, new_document);
Expand Down
8 changes: 4 additions & 4 deletions editor/src/document/properties_panel_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,12 +714,12 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow {
is_style_picker: false,
font_family: layer.font_family.clone(),
font_style: layer.font_style.clone(),
font_file: String::new(),
font_file_url: String::new(),
on_update: WidgetCallback::new(move |font_input: &FontInput| {
PropertiesPanelMessage::ModifyFont {
font_family: font_input.font_family.clone(),
font_style: font_input.font_style.clone(),
font_file: Some(font_input.font_file.clone()),
font_file: Some(font_input.font_file_url.clone()),
size,
}
.into()
Expand All @@ -741,12 +741,12 @@ fn node_section_font(layer: &TextLayer) -> LayoutRow {
is_style_picker: true,
font_family: layer.font_family.clone(),
font_style: layer.font_style.clone(),
font_file: String::new(),
font_file_url: String::new(),
on_update: WidgetCallback::new(move |font_input: &FontInput| {
PropertiesPanelMessage::ModifyFont {
font_family: font_input.font_family.clone(),
font_style: font_input.font_style.clone(),
font_file: Some(font_input.font_file.clone()),
font_file: Some(font_input.font_file_url.clone()),
size,
}
.into()
Expand Down
2 changes: 1 addition & 1 deletion editor/src/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum FrontendMessage {
// Trigger prefix: cause a browser API to do something
TriggerFileDownload { document: String, name: String },
TriggerFileUpload,
TriggerFontLoad { font: String },
TriggerFontLoad { font_file_url: String },
TriggerFontLoadDefault,
TriggerIndexedDbRemoveDocument { document_id: u64 },
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
Expand Down
8 changes: 4 additions & 4 deletions editor/src/layout/layout_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
let update_value = value.as_object().expect("FontInput update was not of type: object");
let font_family_value = update_value.get("fontFamily").expect("FontInput update does not have a fontFamily");
let font_style_value = update_value.get("fontStyle").expect("FontInput update does not have a fontStyle");
let font_file_value = update_value.get("fontFile").expect("FontInput update does not have a fontFile");
let font_file_url_value = update_value.get("fontFileUrl").expect("FontInput update does not have a fontFileUrl");

let font_family = font_family_value.as_str().expect("FontInput update fontFamily was not of type: string");
let font_style = font_style_value.as_str().expect("FontInput update fontStyle was not of type: string");
let font_file = font_file_value.as_str().expect("FontInput update fontFile was not of type: string");
let font_file_url = font_file_url_value.as_str().expect("FontInput update fontFileUrl was not of type: string");

font_input.font_family = font_family.into();
font_input.font_style = font_style.into();
font_input.font_file = font_file.into();
font_input.font_file_url = font_file_url.into();

responses.push_back(DocumentMessage::LoadFont { font: font_file.into() }.into());
responses.push_back(DocumentMessage::LoadFont { font_file_url: font_file_url.into() }.into());
let callback_message = (font_input.on_update.callback)(font_input);
responses.push_back(callback_message);
}
Expand Down
5 changes: 3 additions & 2 deletions editor/src/layout/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ impl WidgetLayout {

pub type SubLayout = Vec<LayoutRow>;

// TODO: Rename LayoutRow to something more generic
#[remain::sorted]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LayoutRow {
Expand Down Expand Up @@ -254,8 +255,8 @@ pub struct FontInput {
pub font_family: String,
#[serde(rename = "fontStyle")]
pub font_style: String,
#[serde(rename = "fontFile")]
pub font_file: String,
#[serde(rename = "fontFileUrl")]
pub font_file_url: String,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<FontInput>,
Expand Down
4 changes: 2 additions & 2 deletions editor/src/viewport_tools/tools/text_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl PropertyHolder for TextTool {
TextMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
file: font_input.font_file.clone(),
file: font_input.font_file_url.clone(),
})
.into()
}),
Expand All @@ -102,7 +102,7 @@ impl PropertyHolder for TextTool {
TextMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
file: font_input.font_file.clone(),
file: font_input.font_file_url.clone(),
})
.into()
}),
Expand Down
112 changes: 65 additions & 47 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<MainWindow />

<div class="unsupported-modal-backdrop" v-if="showUnsupportedModal">
<div class="unsupported-modal-backdrop" v-if="apiUnsupported" ref="unsupported">
<LayoutCol class="unsupported-modal">
<h2>Your browser currently doesn't support Graphite</h2>
<p>Unfortunately, some features won't work properly. Please upgrade to a modern browser such as Firefox, Chrome, Edge, or Safari version 15 or later.</p>
Expand All @@ -11,7 +11,7 @@
API which is required for using the editor. However, you can still explore the user interface.
</p>
<LayoutRow>
<button class="unsupported-modal-button" @click="() => closeModal()">I understand, let's just see the interface</button>
<button class="unsupported-modal-button" @click="() => closeUnsupportedWarning()">I understand, let's just see the interface</button>
</LayoutRow>
</LayoutCol>
</div>
Expand Down Expand Up @@ -258,78 +258,96 @@ img {
<script lang="ts">
import { defineComponent } from "vue";
import { createAutoSaveManager } from "@/lifetime/auto-save";
import { initErrorHandling } from "@/lifetime/errors";
import { createInputManager, InputManager } from "@/lifetime/input";
import { createDialogState, DialogState } from "@/state/dialog";
import { createFullscreenState, FullscreenState } from "@/state/fullscreen";
import { createPortfolioState, PortfolioState } from "@/state/portfolio";
import { createEditorState, EditorState } from "@/state/wasm-loader";
import { createWorkspaceState, WorkspaceState } from "@/state/workspace";
import { createBuildMetadataManager } from "@/io-managers/build-metadata";
import { createClipboardManager } from "@/io-managers/clipboard";
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
import { createInputManager } from "@/io-managers/input";
import { createPanicManager } from "@/io-managers/panic";
import { createPersistenceManager } from "@/io-managers/persistence";
import { createDialogState, DialogState } from "@/state-providers/dialog";
import { createFontsState, FontsState } from "@/state-providers/fonts";
import { createFullscreenState, FullscreenState } from "@/state-providers/fullscreen";
import { createPortfolioState, PortfolioState } from "@/state-providers/portfolio";
import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace";
import { createEditor, Editor } from "@/wasm-communication/editor";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import MainWindow from "@/components/window/MainWindow.vue";
// Vue injects don't play well with TypeScript, and all injects will show up as `any`. As a workaround, we can define these types.
const managerDestructors: {
createBuildMetadataManager?: () => void;
createClipboardManager?: () => void;
createHyperlinkManager?: () => void;
createInputManager?: () => void;
createPanicManager?: () => void;
createPersistenceManager?: () => void;
} = {};
// Vue injects don't play well with TypeScript (all injects will show up as `any`) but we can define these types as a solution
declare module "@vue/runtime-core" {
// Systems `provide`d by the root App to be `inject`ed into descendant components and used for reactive bindings
interface ComponentCustomProperties {
// Graphite WASM editor instance
editor: Editor;
// State provider systems
dialog: DialogState;
fonts: FontsState;
fullscreen: FullscreenState;
portfolio: PortfolioState;
workspace: WorkspaceState;
fullscreen: FullscreenState;
editor: EditorState;
// This must be set to optional because there is a time in the lifecycle of the component where inputManager is undefined.
// That's because we initialize inputManager in `mounted()` rather than `data()` since the div hasn't been created yet.
inputManager?: InputManager;
}
}
export default defineComponent({
provide() {
return {
editor: this.editor,
dialog: this.dialog,
portfolio: this.portfolio,
workspace: this.workspace,
fullscreen: this.fullscreen,
inputManager: this.inputManager,
};
return { ...this.$data };
},
data() {
// Initialize the Graphite WASM editor instance
const editor = createEditorState();
// Initialize other stateful Vue systems
const dialog = createDialogState(editor);
const portfolio = createPortfolioState(editor);
const workspace = createWorkspaceState(editor);
const fullscreen = createFullscreenState();
initErrorHandling(editor, dialog);
createAutoSaveManager(editor, portfolio);
const editor = createEditor();
return {
// Graphite WASM editor instance
editor,
dialog,
portfolio,
workspace,
fullscreen,
showUnsupportedModal: !("BigInt64Array" in window),
inputManager: undefined as undefined | InputManager,
// State provider systems
dialog: createDialogState(editor),
fonts: createFontsState(editor),
fullscreen: createFullscreenState(),
portfolio: createPortfolioState(editor),
workspace: createWorkspaceState(editor),
};
},
computed: {
apiUnsupported() {
return !("BigInt64Array" in window);
},
},
methods: {
closeModal() {
this.showUnsupportedModal = false;
closeUnsupportedWarning() {
const element = this.$refs.unsupported as HTMLElement;
element.parentElement?.removeChild(element);
},
},
mounted() {
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen);
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, {
createBuildMetadataManager: createBuildMetadataManager(this.editor),
createClipboardManager: createClipboardManager(this.editor),
createHyperlinkManager: createHyperlinkManager(this.editor),
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
createPanicManager: createPanicManager(this.editor, this.dialog),
createPersistenceManager: await createPersistenceManager(this.editor, this.portfolio),
});
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
this.editor.instance.init_app();
},
beforeUnmount() {
this.inputManager?.removeListeners();
// Call the destructor for each manager
Object.values(managerDestructors).forEach((destructor) => destructor?.());
// Destroy the WASM editor instance
this.editor.instance.free();
},
components: {
Expand Down
Loading

0 comments on commit fc2d983

Please sign in to comment.