+
+
+
diff --git a/src/components/Editors/Jigsaw/JigsawNode.vue b/src/components/Editors/Jigsaw/JigsawNode.vue
new file mode 100644
index 000000000..588dcba32
--- /dev/null
+++ b/src/components/Editors/Jigsaw/JigsawNode.vue
@@ -0,0 +1,198 @@
+
+
+
+ {{ icon }}
+
+
+
+
+
+
diff --git a/src/components/Editors/Jigsaw/README.md b/src/components/Editors/Jigsaw/README.md
new file mode 100644
index 000000000..e36533a2c
--- /dev/null
+++ b/src/components/Editors/Jigsaw/README.md
@@ -0,0 +1,37 @@
+# Flow Editor
+
+Rethinking Minecraft Add-On creation.
+
+## What is Flow Editor?
+
+Flow is an experimental visual entity editor for Minecraft Add-Ons. Components are placed on a grid and component groups and events are inferred naturally by the way the components are connected. This allows for a more intuitive and visual way to create Minecraft Add-Ons.
+
+## Technical Details
+
+### Internal Representation
+
+Flow grids will have a fixed size of horizontal cells and a theoretically infinite number of vertical cells. We are storing components within an one-dimensional array of cells. Each cell can contain a single component or an "event line" to connect components together.
+
+### Adding components
+
+Components can be dragged from an "inventory" onto the grid. A component group is simply defined by having multiple components right next to each other. Events are connecting component groups together. A red line means "remove this group" and a green line means "add this group".
+
+### Editing components
+
+We need to write a small library which can turn a JSON schema into a simple, visual user interface. It will open upon clicking on a component in the grid.
+
+### Turning flow into code
+
+In order to turn the visual representation of an entity into code, we first need to create a data structure that stores all component groups. This is done in the following way:
+
+- Iterate over each cell line by line
+ - If the current cell is empty, jump to the next cell
+ - Otherwise, check if the cell above contains a component and the cell to the left contains a component
+ - If it does, merge the two components into one group and add the new component to the group as well
+ - If it doesn't, check if the cell above contains a component
+ - If it does, add the current component to the group above
+ - If it doesn't, check if the cell to the left contains a component
+ - If it does, add the current component to the group to the left
+ - Otherwise, add the current component to a new group
+
+TODO: Event parsing
diff --git a/src/components/Editors/Jigsaw/Tab.ts b/src/components/Editors/Jigsaw/Tab.ts
new file mode 100644
index 000000000..ec2cbd149
--- /dev/null
+++ b/src/components/Editors/Jigsaw/Tab.ts
@@ -0,0 +1,144 @@
+import { FileTab, TReadOnlyMode } from '/@/components/TabSystem/FileTab'
+import JigsawTabComponent from './Tab.vue'
+import { App } from '/@/App'
+import { TabSystem } from '/@/components/TabSystem/TabSystem'
+import json5 from 'json5'
+import { settingsState } from '/@/components/Windows/Settings/SettingsState'
+import { debounce } from 'lodash-es'
+import { InformationWindow } from '../../Windows/Common/Information/InformationWindow'
+import { AnyFileHandle } from '../../FileSystem/Types'
+
+const throttledCacheUpdate = debounce<(tab: JigsawTab) => Promise | void>(
+ async (tab) => {
+ const fileContent = JSON.stringify({
+ // TODO: Get JSON from jigsaw tab
+ })
+ const app = await App.getApp()
+
+ app.project.fileChange.dispatch(tab.getPath(), await tab.getFile())
+
+ await app.project.packIndexer.updateFile(
+ tab.getPath(),
+ fileContent,
+ tab.isForeignFile,
+ true
+ )
+ await app.project.jsonDefaults.updateDynamicSchemas(tab.getPath())
+ },
+ 600
+)
+
+export class JigsawTab extends FileTab {
+ component = JigsawTabComponent
+ position = {
+ x: 0,
+ y: 0,
+ }
+
+ constructor(
+ parent: TabSystem,
+ fileHandle: AnyFileHandle,
+ readonlyMode?: TReadOnlyMode
+ ) {
+ super(parent, fileHandle, readonlyMode)
+
+ this.fired.then(async () => {
+ const app = await App.getApp()
+ await app.projectManager.projectReady.fired
+
+ app.project.tabActionProvider.addTabActions(this)
+ })
+ }
+
+ get app() {
+ return this.parent.app
+ }
+ get project() {
+ return this.parent.project
+ }
+
+ static is(fileHandle: AnyFileHandle) {
+ return fileHandle.name.endsWith('.flow')
+ }
+ async setup() {
+ let json: unknown
+ try {
+ const fileStr = await this.fileHandle
+ .getFile()
+ .then((file) => file.text())
+
+ if (fileStr === '') json = {}
+ else json = json5.parse(fileStr)
+ } catch {
+ new InformationWindow({
+ name: 'windows.invalidJson.title',
+ description: 'windows.invalidJson.description',
+ })
+ this.close()
+ return
+ }
+
+ await super.setup()
+ }
+ async getFile() {
+ return new File(
+ [
+ JSON.stringify({
+ // TODO: Get JSON from jigsaw tab
+ }),
+ ],
+ this.name
+ )
+ }
+
+ updateCache() {
+ throttledCacheUpdate(this)
+ }
+
+ async onActivate() {}
+ async onDeactivate() {}
+
+ loadEditor() {}
+ setReadOnly(val: TReadOnlyMode) {
+ this.readOnlyMode = val
+ }
+
+ async save() {
+ this.isTemporary = false
+
+ const app = await App.getApp()
+ const fileContent = JSON.stringify(
+ {
+ // TODO: Stringify jigsaw editor here
+ },
+ null,
+ '\t'
+ )
+
+ await app.fileSystem.write(this.fileHandle, fileContent)
+ }
+
+ async paste() {}
+
+ async copy() {}
+
+ async cut() {}
+
+ async close() {
+ const didClose = await super.close()
+
+ // We need to clear the lightning cache store from temporary data if the user doesn't save changes
+ if (!this.isForeignFile && didClose && this.isUnsaved) {
+ const file = await this.fileHandle.getFile()
+ const fileContent = await file.text()
+ await this.project.packIndexer.updateFile(
+ this.getPath(),
+ fileContent
+ )
+ }
+
+ return didClose
+ }
+
+ protected _save(): void | Promise {}
+}
diff --git a/src/components/Editors/Jigsaw/Tab.vue b/src/components/Editors/Jigsaw/Tab.vue
new file mode 100644
index 000000000..ad335c7ca
--- /dev/null
+++ b/src/components/Editors/Jigsaw/Tab.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TabSystem/TabProvider.ts b/src/components/TabSystem/TabProvider.ts
index 195df07e7..e8836bb49 100644
--- a/src/components/TabSystem/TabProvider.ts
+++ b/src/components/TabSystem/TabProvider.ts
@@ -1,5 +1,6 @@
import { ImageTab } from '../Editors/Image/ImageTab'
import { TargaTab } from '../Editors/Image/TargaTab'
+import { JigsawTab } from '../Editors/Jigsaw/Tab'
import { SoundTab } from '../Editors/Sound/SoundTab'
import { TextTab } from '../Editors/Text/TextTab'
import { TreeTab } from '../Editors/TreeEditor/Tab'
@@ -8,6 +9,7 @@ import { FileTab } from './FileTab'
export class TabProvider {
protected static _tabs = new Set([
TextTab,
+ JigsawTab,
TreeTab,
ImageTab,
TargaTab,