Multi-document management (create new document; tab switching) (#210)

* Multi-docs WIP

* Change to number

* Add new document and switch documents

* Remove keybind for previous document. Change keybind for next document.

* Switch documents by clicking tabs

* Remove keybind for previous document. Change keybind for next document.

* multi-docs

* Update package-lock.json

* Hook up File>New to add new document

* Remove console logs and empty lines. Start new documents from 2 instead of 1.

* Fix formatting
This commit is contained in:
George Atkinson
2021-06-13 20:24:09 -07:00
committed by GitHub
parent 1c2e667769
commit f6b7708a8f
11 changed files with 133 additions and 12 deletions
@@ -73,7 +73,7 @@ const menuEntries: MenuListEntries = [
ref: undefined, ref: undefined,
children: [ children: [
[ [
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"] }, { label: "New", icon: "File", shortcut: ["Ctrl", "N"], action: async () => (await wasm).new_document() },
{ label: "Open…", shortcut: ["Ctrl", "O"] }, { label: "Open…", shortcut: ["Ctrl", "O"] },
{ {
label: "Open Recent", label: "Open Recent",
@@ -2,7 +2,7 @@
<div class="panel"> <div class="panel">
<div class="tab-bar" :class="{ 'min-widths': tabMinWidths }"> <div class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
<div class="tab-group"> <div class="tab-group">
<div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel"> <div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel" @click="handleTabClick(tabIndex)">
<span>{{ tabLabel }}</span> <span>{{ tabLabel }}</span>
<IconButton :icon="'CloseX'" :size="16" v-if="tabCloseButtons" /> <IconButton :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</div> </div>
@@ -144,6 +144,8 @@ import IconButton from "../widgets/buttons/IconButton.vue";
import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue"; import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue";
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue"; import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
const wasm = import("../../../wasm/pkg");
export default defineComponent({ export default defineComponent({
components: { components: {
Document, Document,
@@ -153,6 +155,12 @@ export default defineComponent({
IconButton, IconButton,
PopoverButton, PopoverButton,
}, },
methods: {
async handleTabClick(tabIndex: number) {
const { select_document } = await wasm;
select_document(tabIndex);
},
},
props: { props: {
tabMinWidths: { type: Boolean, default: false }, tabMinWidths: { type: Boolean, default: false },
tabCloseButtons: { type: Boolean, default: false }, tabCloseButtons: { type: Boolean, default: false },
@@ -1,13 +1,7 @@
<template> <template>
<LayoutRow class="workspace-grid-subdivision"> <LayoutRow class="workspace-grid-subdivision">
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 1597"> <LayoutCol class="workspace-grid-subdivision" style="flex-grow: 1597">
<Panel <Panel :panelType="'Document'" :tabCloseButtons="true" :tabMinWidths="true" :tabLabels="documents" :tabActiveIndex="activeDocument" />
:panelType="'Document'"
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="['Untitled Document*', 'Document 2', 'Document 3', 'Document 4', 'Document 5', 'Document 6']"
:tabActiveIndex="0"
/>
</LayoutCol> </LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter"></LayoutCol> <LayoutCol class="workspace-grid-resize-gutter"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 319"> <LayoutCol class="workspace-grid-subdivision" style="flex-grow: 319">
@@ -52,6 +46,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { ResponseType, registerResponseHandler, Response, SetActiveDocument, NewDocument } from "../../response-handler";
import LayoutRow from "../layout/LayoutRow.vue"; import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue"; import LayoutCol from "../layout/LayoutCol.vue";
import Panel from "./Panel.vue"; import Panel from "./Panel.vue";
@@ -62,5 +57,24 @@ export default defineComponent({
LayoutCol, LayoutCol,
Panel, Panel,
}, },
mounted() {
registerResponseHandler(ResponseType.NewDocument, (responseData: Response) => {
const documentData = responseData as NewDocument;
if (documentData) this.documents.push(documentData.document_name);
});
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
const documentData = responseData as SetActiveDocument;
if (documentData) this.activeDocument = documentData.document_index;
});
},
data() {
return {
activeDocument: 0,
documents: ["Untitled Document"],
};
},
}); });
</script> </script>
+24
View File
@@ -16,6 +16,8 @@ export enum ResponseType {
ExpandFolder = "ExpandFolder", ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder", CollapseFolder = "CollapseFolder",
SetActiveTool = "SetActiveTool", SetActiveTool = "SetActiveTool",
SetActiveDocument = "SetActiveDocument",
NewDocument = "NewDocument",
UpdateWorkingColors = "UpdateWorkingColors", UpdateWorkingColors = "UpdateWorkingColors",
} }
@@ -52,6 +54,10 @@ function parseResponse(responseType: string, data: any): Response {
return newExpandFolder(data.ExpandFolder); return newExpandFolder(data.ExpandFolder);
case "SetActiveTool": case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool); return newSetActiveTool(data.SetActiveTool);
case "SetActiveDocument":
return newSetActiveDocument(data.SetActiveDocument);
case "NewDocument":
return newNewDocument(data.NewDocument);
case "UpdateCanvas": case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas); return newUpdateCanvas(data.UpdateCanvas);
case "ExportDocument": case "ExportDocument":
@@ -95,6 +101,24 @@ function newSetActiveTool(input: any): SetActiveTool {
}; };
} }
export interface SetActiveDocument {
document_index: number;
}
function newSetActiveDocument(input: any): SetActiveDocument {
return {
document_index: input.document_index,
};
}
export interface NewDocument {
document_name: string;
}
function newNewDocument(input: any): NewDocument {
return {
document_name: input.document_name,
};
}
export interface UpdateCanvas { export interface UpdateCanvas {
document: string; document: string;
} }
+10
View File
@@ -21,6 +21,16 @@ pub fn select_tool(tool: String) -> Result<(), JsValue> {
}) })
} }
#[wasm_bindgen]
pub fn select_document(document: usize) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectDocument(document)).map_err(convert_error))
}
#[wasm_bindgen]
pub fn new_document() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::NewDocument).map_err(convert_error))
}
// TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it) // TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it)
/// Mouse movement within the screenspace bounds of the viewport /// Mouse movement within the screenspace bounds of the viewport
#[wasm_bindgen] #[wasm_bindgen]
+1
View File
@@ -123,6 +123,7 @@ pub fn translate_key(name: &str) -> Key {
"backspace" => KeyBackspace, "backspace" => KeyBackspace,
"alt" => KeyAlt, "alt" => KeyAlt,
"escape" => KeyEscape, "escape" => KeyEscape,
"tab" => KeyTab,
_ => UnknownKey, _ => UnknownKey,
} }
} }
+11 -1
View File
@@ -14,7 +14,17 @@ impl Default for Document {
fn default() -> Self { fn default() -> Self {
Self { Self {
document: InteralDocument::default(), document: InteralDocument::default(),
name: String::from("Unnamed Document"), name: String::from("Untitled Document"),
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
}
}
}
impl Document {
pub fn with_name(name: String) -> Self {
Self {
document: InteralDocument::default(),
name,
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(), layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
} }
} }
@@ -16,6 +16,9 @@ pub enum DocumentMessage {
ToggleLayerVisibility(Vec<LayerId>), ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>), ToggleLayerExpansion(Vec<LayerId>),
SelectDocument(usize), SelectDocument(usize),
NewDocument,
NextDocument,
PrevDocument,
ExportDocument, ExportDocument,
RenderDocument, RenderDocument,
Undo, Undo,
@@ -85,6 +88,51 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
SelectDocument(id) => { SelectDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized"); assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
self.active_document = id; self.active_document = id;
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
);
}
NewDocument => {
self.active_document = self.documents.len();
let new_document = Document::with_name(format!("Untitled Document {}", self.active_document + 1));
self.documents.push(new_document);
responses.push_back(
FrontendMessage::NewDocument {
document_name: self.active_document().name.clone(),
}
.into(),
);
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
);
}
NextDocument => {
self.active_document = (self.active_document + 1) % self.documents.len();
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
);
}
PrevDocument => {
self.active_document = (self.active_document + self.documents.len() - 1) % self.documents.len();
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
);
} }
ExportDocument => responses.push_back( ExportDocument => responses.push_back(
FrontendMessage::ExportDocument { FrontendMessage::ExportDocument {
@@ -160,9 +208,9 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
} }
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
if self.active_document().layer_data.values().any(|data| data.selected) { if self.active_document().layer_data.values().any(|data| data.selected) {
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, RenderDocument, ExportDocument) actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, RenderDocument, ExportDocument, NewDocument, NextDocument, PrevDocument)
} else { } else {
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument) actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, NextDocument, PrevDocument)
} }
} }
} }
@@ -11,6 +11,8 @@ pub enum FrontendMessage {
CollapseFolder { path: Vec<LayerId> }, CollapseFolder { path: Vec<LayerId> },
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> }, ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
SetActiveTool { tool_name: String }, SetActiveTool { tool_name: String },
SetActiveDocument { document_index: usize },
NewDocument { document_name: String },
UpdateCanvas { document: String }, UpdateCanvas { document: String },
ExportDocument { document: String }, ExportDocument { document: String },
EnableTextInput, EnableTextInput,
@@ -38,6 +40,7 @@ impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
CollapseFolder, CollapseFolder,
ExpandFolder, ExpandFolder,
SetActiveTool, SetActiveTool,
NewDocument,
UpdateCanvas, UpdateCanvas,
EnableTextInput, EnableTextInput,
DisableTextInput, DisableTextInput,
+2
View File
@@ -161,6 +161,8 @@ impl Default for Mapping {
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace}, entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]}, entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyShift]},
// Global Actions // Global Actions
entry! {action=GlobalMessage::LogInfo, key_down=Key1}, entry! {action=GlobalMessage::LogInfo, key_down=Key1},
entry! {action=GlobalMessage::LogDebug, key_down=Key2}, entry! {action=GlobalMessage::LogDebug, key_down=Key2},
+1
View File
@@ -59,6 +59,7 @@ pub enum Key {
KeyBackspace, KeyBackspace,
KeyAlt, KeyAlt,
KeyEscape, KeyEscape,
KeyTab,
// This has to be the last element in the enum. // This has to be the last element in the enum.
NumKeys, NumKeys,