Skip to content

Commit

Permalink
Module runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Nov 22, 2024
1 parent 4cdb1ad commit 1e8c0d5
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 89 deletions.
11 changes: 8 additions & 3 deletions spx-gui/src/components/editor/EditorContextProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

<script lang="ts">
import { inject } from 'vue'
import { Runtime } from '@/models/runtime'
export type EditorCtx = {
project: Project
userInfo: UserInfo
runtime: Runtime
}
const editorCtxKey: InjectionKey<EditorCtx> = Symbol('editor-ctx')
Expand All @@ -23,16 +25,19 @@ export function useEditorCtx() {
import { provide, type InjectionKey } from 'vue'
import { Project } from '@/models/project'
import type { UserInfo } from '@/stores/user'
import { computedShallowReactive } from '@/utils/utils'
import { computedShallowReactive, useComputedDisposable } from '@/utils/utils'
const props = defineProps<{
project: Project
userInfo: UserInfo
}>()
const editorCtx = computedShallowReactive(() => ({
const runtimeRef = useComputedDisposable(() => new Runtime(props.project))
const editorCtx = computedShallowReactive<EditorCtx>(() => ({
project: props.project,
userInfo: props.userInfo
userInfo: props.userInfo,
runtime: runtimeRef.value
}))
provide(editorCtxKey, editorCtx)
Expand Down
4 changes: 1 addition & 3 deletions spx-gui/src/components/editor/code-editor/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useEditorCtx } from '../EditorContextProvider.vue'
import { Copilot } from './copilot'
import { DocumentBase } from './document-base'
import { Spxlc } from './lsp'
import { Runtime } from './runtime'
import {
CodeEditorUIComp,
type ICodeEditorUI,
Expand All @@ -24,7 +23,6 @@ function initialize(ui: ICodeEditorUI) {
const copilot = new Copilot()
const documentBase = new DocumentBase()
const spxlc = new Spxlc()
const runtime = new Runtime()
ui.registerAPIReferenceProvider({
async provideAPIReference(ctx, position) {
Expand Down Expand Up @@ -66,7 +64,7 @@ function initialize(ui: ICodeEditorUI) {
implements IDiagnosticsProvider
{
async provideDiagnostics(ctx: DiagnosticsContext): Promise<Diagnostic[]> {
console.warn('TODO', ctx, runtime)
console.warn('TODO', ctx, editorCtx.runtime)
return []
}
}
Expand Down
23 changes: 0 additions & 23 deletions spx-gui/src/components/editor/code-editor/runtime.ts

This file was deleted.

32 changes: 32 additions & 0 deletions spx-gui/src/components/editor/panels/ConsolePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { computed } from 'vue'
import { UICard, UICardHeader } from '@/components/ui'
import { useEditorCtx } from '../EditorContextProvider.vue'
const editorCtx = useEditorCtx()
const runtime = computed(() => editorCtx.runtime)
</script>

<template>
<UICard class="console-panel">
<UICardHeader>
{{ $t({ en: 'Console', zh: '控制台' }) }}
</UICardHeader>
<ul class="output-container">
<li v-for="(output, i) in runtime.outputs" :key="i" class="output">
{{ output.message }}
</li>
</ul>
</UICard>
</template>

<style lang="scss" scoped>
.console-panel {
display: flex;
flex-direction: column;
}
.output-container {
flex: 1 1 0;
overflow-y: auto;
}
</style>
11 changes: 9 additions & 2 deletions spx-gui/src/components/editor/panels/EditorPanels.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="editor-panels">
<div v-show="running !== 'debug'" class="editor-panels">
<UICard class="main">
<SpritesPanel :expanded="expandedPanel === 'sprites'" @expand="expand('sprites')" />
<SoundsPanel :expanded="expandedPanel === 'sounds'" @expand="expand('sounds')" />
Expand All @@ -8,22 +8,25 @@
<StagePanel></StagePanel>
</UICard>
</div>
<ConsolePanel v-show="running === 'debug'" class="console-panel" />
</template>

<script setup lang="ts">
import { ref, watch, shallowRef } from 'vue'
import { ref, watch, shallowRef, computed } from 'vue'
import { UICard } from '@/components/ui'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
import SoundsPanel from './sound/SoundsPanel.vue'
import SpritesPanel from './sprite/SpritesPanel.vue'
import StagePanel from './stage/StagePanel.vue'
import ConsolePanel from './ConsolePanel.vue'
const expandedPanel = ref<'sprites' | 'sounds'>('sprites')
function expand(panel: 'sprites' | 'sounds') {
expandedPanel.value = panel
}
const editorCtx = useEditorCtx()
const running = computed(() => editorCtx.runtime.running)
watch(
() => editorCtx.project.selected,
Expand Down Expand Up @@ -76,6 +79,10 @@ watch(
gap: var(--ui-gap-middle);
}
.console-panel {
flex: 1 1 0;
}
.main {
flex: 1 1 0;
min-width: 0;
Expand Down
69 changes: 57 additions & 12 deletions spx-gui/src/components/editor/preview/EditorPreview.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,66 @@
<template>
<UICard class="editor-preview">
<UICardHeader>
<UICardHeader v-if="running !== 'debug'">
<div class="header">
{{ $t({ en: 'Preview', zh: '预览' }) }}
</div>
<UIButton ref="runButtonRef" class="run-button" type="primary" icon="play" @click="running = true">
<UIButton ref="runButtonRef" class="button" type="primary" icon="play" @click="setRunning('debug')">
{{ $t({ en: 'Run', zh: '运行' }) }}
</UIButton>
<UIButton ref="fullScreenRunButtonRef" class="button" type="primary" icon="play" @click="setRunning('run')">
{{ $t({ en: 'Full screen', zh: '全屏' }) }}
</UIButton>
</UICardHeader>
<UICardHeader v-else>
<div class="header">
{{ $t({ en: 'Running', zh: '运行中' }) }}
</div>
<UIButton class="button" type="primary" icon="stop" @click="setRunning('none')">
{{ $t({ en: 'Stop', zh: '停止' }) }}
</UIButton>
</UICardHeader>

<!--
A hidden div is used instead of `UIModal` to initialize the runner early, allowing for flexible preload logic in the runner component.
Although naive-ui modal supports `display-directive: show`, it does not initialize the component until it is shown for the first time.
TODO: Update `UIModal` to support this requirement.
-->
<div class="project-runner-modal" :class="{ visible: running }" :style="modalStyle">
<RunnerContainer :project="editorCtx.project" :visible="running" @close="running = false" />
<div class="full-screen-runner-modal" :class="{ visible: running === 'run' }" :style="modalStyle">
<RunnerContainer :project="editorCtx.project" :visible="running === 'run'" @close="setRunning('none')" />
</div>
<div class="stage-viewer-container">
<StageViewer />

<div class="main">
<div class="stage-viewer-container">
<StageViewer />
<div v-show="running === 'debug'" class="in-place-runner">
<InPlaceRunner :project="editorCtx.project" :visible="running === 'debug'" />
</div>
</div>
</div>
</UICard>
</template>

<script lang="ts" setup>
import { ref, computed } from 'vue'
import type { RunningMode } from '@/models/runtime'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
import { UICard, UICardHeader, UIButton } from '@/components/ui'
import StageViewer from './stage-viewer/StageViewer.vue'
import RunnerContainer from './RunnerContainer.vue'
import InPlaceRunner from './InPlaceRunner.vue'
const running = ref(false)
const editorCtx = useEditorCtx()
const runButtonRef = ref<InstanceType<typeof UIButton>>()
const running = computed(() => editorCtx.runtime.running)
function setRunning(running: RunningMode) {
editorCtx.runtime.setRunning(running)
}
const fullScreenRunButtonRef = ref<InstanceType<typeof UIButton>>()
const modalStyle = computed(() => {
if (!runButtonRef.value) return null
const { top, left, width, height } = runButtonRef.value.$el.getBoundingClientRect()
if (!fullScreenRunButtonRef.value) return null
const { top, left, width, height } = fullScreenRunButtonRef.value.$el.getBoundingClientRect()
return { transformOrigin: `${left + width / 2}px ${top + height / 2}px` }
})
</script>
Expand All @@ -54,16 +78,28 @@ const modalStyle = computed(() => {
color: var(--ui-color-title);
}
.stage-viewer-container {
.button {
margin-left: 8px;
}
.main {
display: flex;
overflow: hidden;
justify-content: center;
padding: 12px;
height: 100%;
}
.stage-viewer-container {
position: relative;
width: 100%;
height: 100%;
border-radius: var(--ui-border-radius-1);
overflow: hidden;
}
}
.project-runner-modal {
.full-screen-runner-modal {
position: fixed;
z-index: 100;
left: 0;
Expand All @@ -83,4 +119,13 @@ const modalStyle = computed(() => {
opacity: 1;
}
}
.in-place-runner {
position: absolute;
z-index: 10;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
</style>
46 changes: 46 additions & 0 deletions spx-gui/src/components/editor/preview/InPlaceRunner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { untilNotNull } from '@/utils/utils'
import ProjectRunner from '@/components/project/runner/ProjectRunner.vue'
import { useEditorCtx } from '../EditorContextProvider.vue'
import { RuntimeOutputKind } from '@/models/runtime'
import { getCleanupSignal } from '@/utils/disposable'
const props = defineProps<{
visible: boolean
}>()
const editorCtx = useEditorCtx()
const projectRunnerRef = ref<InstanceType<typeof ProjectRunner>>()
function handleConsole(type: 'log' | 'warn', args: unknown[]) {
// TODO: parse source
editorCtx.runtime.addOutput({
kind: type === 'warn' ? RuntimeOutputKind.Error : RuntimeOutputKind.Log,
time: Date.now(),
message: args.join(' ')
})
}
watch(
() => props.visible,
async (visible, _, onCleanup) => {
if (!visible) return
const signal = getCleanupSignal(onCleanup)
const projectRunner = await untilNotNull(projectRunnerRef)
signal.throwIfAborted()
projectRunner.run()
signal.addEventListener('abort', () => {
projectRunner.stop()
})
},
{ immediate: true }
)
</script>

<template>
<ProjectRunner ref="projectRunnerRef" :project="editorCtx.project" @console="handleConsole" />
</template>

<style lang="scss" scoped></style>
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,10 @@ watchEffect((onCleanup) => {
align-items: center;
justify-content: center;
border-radius: var(--ui-border-radius-1);
background-image: url(@/assets/stage-bg.svg);
background-position: center;
background-repeat: repeat;
background-size: contain;
position: relative;
overflow: hidden;
}
</style>
14 changes: 7 additions & 7 deletions spx-gui/src/models/project/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,16 @@ describe('Project', () => {
const localClearMock = vi.spyOn(localHelper, 'clear').mockResolvedValue(undefined)

const project = makeProject()
project['filesHash'] = await hashHelper.hashFiles({})
project['lastSyncedFilesHash'] = project['filesHash']
project['gameFilesHash'] = await hashHelper.hashFiles({})
project['lastSyncedGameFilesHash'] = project['gameFilesHash']

project.setAutoSaveMode(AutoSaveMode.Cloud)
project['startAutoSaveToCloud']('localCacheKey')
await flushPromises() // finish immediate watchers
const autoSaveToCloud = project['autoSaveToCloud']
expect(autoSaveToCloud).toBeTruthy()

project['filesHash'] = 'newHash'
project['gameFilesHash'] = 'newHash'
expect(project.hasUnsyncedChanges).toBe(true)

autoSaveToCloud?.()
Expand All @@ -253,7 +253,7 @@ describe('Project', () => {
expect(cloudSaveMock).toHaveBeenCalledTimes(1)
expect(localSaveMock).toHaveBeenCalledTimes(1)

project['filesHash'] = project['lastSyncedFilesHash']
project['gameFilesHash'] = project['lastSyncedGameFilesHash']
expect(project.hasUnsyncedChanges).toBe(false)

vi.advanceTimersByTime(retryAutoSaveToCloudDelay)
Expand All @@ -270,16 +270,16 @@ describe('Project', () => {
const cloudSaveMock = vi.spyOn(cloudHelper, 'save').mockRejectedValue(undefined)

const project = makeProject()
project['filesHash'] = await hashHelper.hashFiles({})
project['lastSyncedFilesHash'] = project['filesHash']
project['gameFilesHash'] = await hashHelper.hashFiles({})
project['lastSyncedGameFilesHash'] = project['gameFilesHash']

project.setAutoSaveMode(AutoSaveMode.Cloud)
project['startAutoSaveToCloud']('localCacheKey')
await flushPromises() // finish immediate watchers
const autoSaveToCloud = project['autoSaveToCloud']
expect(autoSaveToCloud).toBeTruthy()

project['filesHash'] = 'newHash'
project['gameFilesHash'] = 'newHash'
expect(project.hasUnsyncedChanges).toBe(true)

autoSaveToCloud?.()
Expand Down
Loading

0 comments on commit 1e8c0d5

Please sign in to comment.