Files

1164 lines
30 KiB
Go

package shell
import (
"path"
"time"
"aego/asset"
"aego/asset/loader"
"aego/asset/pipeline"
"aego/contracts/persistence"
"aego/core/ids"
"aego/core/log"
"aego/core/mathx"
"aego/editor/core/assetfiles"
"aego/editor/core/assetops"
edclip "aego/editor/core/clipboard"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/dataedit"
"aego/editor/core/document"
"aego/editor/core/favorites"
"aego/editor/core/fieldgizmo"
"aego/editor/core/gizmo"
"aego/editor/core/play"
pd "aego/editor/core/playdiff"
"aego/editor/core/prefabs"
"aego/editor/core/presets"
"aego/editor/core/preview"
"aego/editor/core/refs"
"aego/editor/core/sceneops"
"aego/editor/core/selection"
"aego/editor/core/services"
"aego/editor/core/session"
"aego/editor/core/shape"
"aego/editor/core/tasks"
"aego/editor/core/templates"
"aego/editor/core/vcs"
"aego/editor/core/vectors"
"aego/editor/core/workspace"
"aego/editor/ext"
"aego/editor/ext/inspect"
"aego/editor/panels/assets"
"aego/editor/panels/batchrename"
"aego/editor/panels/console"
"aego/editor/panels/crashes"
"aego/editor/panels/datatable"
edocs "aego/editor/panels/docs"
"aego/editor/panels/events"
exportpanel "aego/editor/panels/export"
"aego/editor/panels/gameview"
"aego/editor/panels/health"
"aego/editor/panels/help"
"aego/editor/panels/hierarchy"
"aego/editor/panels/inspector"
playdiffpanel "aego/editor/panels/playdiff"
eproblems "aego/editor/panels/problems"
"aego/editor/panels/profiler"
"aego/editor/panels/projectsettings"
"aego/editor/panels/references"
"aego/editor/panels/saves"
"aego/editor/panels/sceneview"
"aego/editor/panels/scripteditor"
"aego/editor/panels/search"
"aego/editor/panels/taskcenter"
"aego/editor/panels/vcsdiff"
"aego/editor/panels/watch"
"aego/editor/panels/welcome"
"aego/engine"
"aego/engine/docs"
"aego/engine/module"
"aego/engine/problems"
"aego/engine/settings"
"aego/platform"
"aego/platform/input"
"aego/project"
"aego/render"
"aego/render/icons"
"aego/render/text"
"aego/render/view"
"aego/save"
"aego/scene"
"aego/ui/core"
"aego/ui/dnd"
"aego/ui/draw"
"aego/ui/theme"
"aego/ui/widgets"
)
const ChannelEditor log.Channel = "editor"
type Options struct {
Project *project.Project
Pipeline *pipeline.Pipeline
Loader *loader.Loader
Types *scene.Types
Renderer *render.Renderer
Log *log.Logger
Panels []workspace.Desc
Modules *module.Manifest
Sections *module.Sections
Problems *problems.Service
Settings *settings.Store
Hooks []module.Hook
RenderStats func() render.Stats
Supervised bool
Paced bool
Scripts context.ScriptHost
Code *scripteditor.Host
CreatedDirs []string
DirsErr error
Help *docs.Registry
RunSnippet func(code string) []string
NewProject func(dir, template string) (string, error)
Crash func(where string, rec any, stack []byte)
Exporter services.Exporter
}
type App struct {
data *dataedit.Service
tablePanel *datatable.Panel
opt Options
eng *engine.Engine
ui *core.Context
ctx *context.Editor
ws *workspace.Workspace
list *draw.List
log *log.ChannelLogger
unlock func()
modals []modal
drops []string
dropPos mathx.Vec2
title string
textOn bool
captured bool
checkAt float64
recoverAt float64
discard bool
quitting bool
templates *templates.Registry
shapes *shape.Registry
inspectors *inspect.Registry
gizmos *gizmo.Registry
hierarchy *hierarchy.Panel
sceneView *sceneview.Panel
assetsPanel *assets.Panel
thumbs *preview.Service
presets *presets.Service
favs *favorites.Store
refs *refs.Service
refsPanel *references.Panel
missing *references.Missing
renameDlg *batchrename.Dialog
diffPanel *vcsdiff.Panel
healthP *health.Panel
exportP *exportpanel.Panel
native *nativeMenu
taskPanel *taskcenter.Panel
playDiff *playdiffpanel.Dialog
diffMode pd.Mode
diffPending bool
diffDoc document.ID
diffRes pd.Result
tasks *tasks.Queue
files *assetfiles.Files
prefabs *prefabs.Service
gameView *gameview.Panel
profiler *profiler.Panel
viewport *services.Viewport
settings *projectsettings.Panel
consoleSink *console.Sink
play *play.Controller
playReturn document.ID
ext *ext.Registry
restart bool
sessionAt float64
buildSeen time.Time
lastInput float64
lastFrame time.Time
buildState widgets.DotState
consolePanel *console.Panel
inspectorPanel *inspector.Panel
problemsPanel *eproblems.Panel
eventsPanel *events.Panel
watchPanel *watch.Panel
crashesPanel *crashes.Panel
savesPanel *saves.Panel
search *search.Overlay
shiftAt float64
shiftArmed bool
welcome *welcome.Panel
docs *edocs.Panel
keymap *help.Keymap
welcomeAt float64
genAtPlay uint64
wasPlaying bool
pulse pulse
playRect mathx.Rect
full bool
scriptLog *log.ChannelLogger
problemRev uint64
clip *clipboard
picker scenePicker
}
func New(opt Options) *App {
return &App{opt: opt}
}
func (a *App) Context() *context.Editor {
return a.ctx
}
func (a *App) Workspace() *workspace.Workspace {
return a.ws
}
func (a *App) Data() *dataedit.Service {
return a.data
}
func (a *App) Play() *play.Controller {
return a.play
}
func (a *App) Extensions() *ext.Registry {
return a.ext
}
func (a *App) Templates() *templates.Registry {
return a.templates
}
func (a *App) Profiler() *profiler.Panel {
return a.profiler
}
func (a *App) Prefabs() *prefabs.Service {
return a.prefabs
}
func (a *App) Viewport() *services.Viewport {
return a.viewport
}
// UIList is for golden tests only; it returns the last built UI frame.
func (a *App) UIList() *draw.List {
return a.list
}
func (a *App) Init(e *engine.Engine) error {
a.eng = e
lg := a.opt.Log
if lg == nil {
lg = e.Log()
}
a.log = lg.With(ChannelEditor)
a.consoleSink = console.NewSink(4000)
lg.AddSink(a.consoleSink)
a.reportDirs()
r := a.opt.Renderer
ui := core.New(r.UIFont())
ui.Theme = theme.Editor()
if sh, err := draw.NewShapes(e.GPU(), r.Textures()); err == nil {
ui.SetShapes(sh)
} else {
a.log.Warn("ui shapes are unavailable, corners stay square", log.F("err", err.Error()))
}
if set, err := text.LoadSet(e.VFS(), r.Textures(), "engine://fonts", "ui", []int{12, 14, 16, 20}, []int{1, 2}); err == nil {
ui.SetFonts(set)
} else {
a.log.Warn("ui fonts are missing, using the debug font", log.F("err", err.Error()))
}
if set, err := text.LoadSet(e.VFS(), r.Textures(), "engine://fonts", "ui-mono", []int{12, 14, 16, 20}, []int{1, 2}); err == nil {
ui.SetMonoFonts(set)
} else {
a.log.Warn("code fonts are missing, the script editor uses the ui font", log.F("err", err.Error()))
}
a.clip = &clipboard{win: e.Window(), now: e.Platform().Now}
ui.Clipboard = a.clip
a.ui = ui
if set, err := icons.Load(e.VFS(), r.Textures(), "engine://icons"); err == nil {
set.OnMissing = func(id string) {
a.log.Warn("icon is missing from the atlas", log.F("id", id))
}
ui.Icons = set
} else {
a.log.Warn("icon atlas is missing, run make genicons", log.F("err", err.Error()))
}
var res asset.Resolver
if a.opt.Loader != nil {
res = a.opt.Loader
}
p := a.opt.Project
a.shapes = shape.NewRegistry()
a.ctx = &context.Editor{
Engine: e,
Renderer: r,
Project: p,
Pipeline: a.opt.Pipeline,
Loader: a.opt.Loader,
Types: a.opt.Types,
Docs: document.NewManager(document.Options{Root: p.Root, Types: a.opt.Types, Resolver: res, Log: lg, OnChange: module.RunChanged}),
Selection: selection.New(),
Commands: command.New(),
UI: ui,
DnD: &dnd.State{},
Clipboard: a.clip,
Log: a.log,
Settings: context.DefaultSettings(),
RenderStats: a.opt.RenderStats,
Scripts: a.opt.Scripts,
Collapsed: map[string]bool{},
Shapes: a.shapes,
Sections: a.opt.Sections,
}
a.ctx.Problems = a.opt.Problems
if a.ctx.Problems == nil {
a.ctx.Problems = problems.New()
}
a.ctx.Problems.Add(a.ctx.SceneChecks())
a.ctx.Commands.OnError = func(id string, err error) {
a.ctx.Notify(context.LevelError, err.Error())
}
a.ctx.DnD.Trace = func(event string, p dnd.Payload) {
a.log.Debug("dnd "+event, log.F("kind", p.Kind), log.F("label", p.Label))
}
a.templates = templates.New()
if a.opt.Modules != nil {
if err := a.templates.Load(a.opt.Modules.Templates); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
a.inspectors = inspect.NewRegistry()
if err := a.inspectors.Load(a.opt.Types.Schema()); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
a.inspectors.SetEditor(a.ctx)
if a.opt.Modules != nil {
ids := make([]string, len(a.opt.Modules.Modules))
for i, m := range a.opt.Modules.Modules {
ids[i] = m.ID
}
a.inspectors.SetModules(ids)
}
a.gizmos = gizmo.NewRegistry()
if err := fieldgizmo.Register(a.ctx, a.gizmos); err != nil {
return err
}
a.play = play.New(a.ctx)
a.play.SetBeforeStop(a.beforeStop)
a.ctx.Play = a.play
a.play.Subscribe(func(st play.State) {
if st == play.Stopped {
for _, d := range a.ctx.Docs.Docs() {
a.ctx.PruneSelectionOf(d.ID)
}
a.afterPlay()
if a.data != nil {
a.data.PlayStopped()
}
}
})
if a.opt.Modules != nil {
for _, m := range a.opt.Modules.Modules {
if m.NonDeterministic != "" {
a.play.SetBlocked("Rewind is off: module " + m.ID + " " + m.NonDeterministic)
break
}
}
}
a.files = assetfiles.New(a.ctx)
a.prefabs = prefabs.New(a.ctx)
a.ctx.BeforeSwap = a.prefabs.External
a.data = dataedit.New(a.ctx)
a.data.OnLive = func() { a.play.Invalidate("a data asset was tuned during Play") }
a.ctx.Selection.Subscribe(a.data.Clear)
a.tablePanel = datatable.New(datatable.Deps{Data: a.data, Notices: a.ctx})
a.templates.SetPrefabs(a.prefabs)
a.tasks = tasks.New(tasks.Deps{
Submit: func(fn func()) { e.Jobs().Submit(fn) },
OnMain: func(fn func()) { e.Jobs().OnMain(fn) },
Budget: 4 * time.Millisecond,
})
a.thumbs = preview.New(preview.Deps{
Engine: e,
Pipeline: a.opt.Pipeline,
Renderer: r,
GPU: e.GPU(),
Types: a.opt.Types,
Resolver: res,
Shapes: a.shapes,
Tasks: a.tasks,
CacheRoot: p.CacheDir(),
Background: ui.Theme.Background,
Cover: func() ids.AssetID { return a.opt.Project.MainScene },
Reference: func() mathx.Vec2 {
w := a.opt.Project.Window
return mathx.Vec2{X: float32(w.Width), Y: float32(w.Height)}
},
})
a.ctx.Thumbs = a.thumbs
sops := sceneops.New(a.ctx, a.files.History())
a.refs = refs.New(a.ctx)
a.refs.Prefabs = a.prefabs
rd := references.Deps{
Refs: a.refs, Pipeline: a.opt.Pipeline, Docs: a.ctx, Selection: a.ctx, Nav: a.ctx, Notices: a.ctx, Tasks: a.tasks,
Delete: func(list []ids.AssetID) { a.assetsPanel.DeleteNow(list) },
Show: func(id ids.AssetID) { a.showReferences(id, nil) },
ShowHierarchy: func() { a.ws.Show(workspace.Hierarchy) },
}
a.refsPanel = references.New(rd)
a.missing = references.NewMissing(rd)
a.renameDlg = batchrename.New(batchrename.Deps{
Pipeline: a.opt.Pipeline,
Docs: a.ctx,
Exists: func(rel string) bool { return assetops.Exists(a.ctx, rel) },
Move: a.moveAssets,
Notices: a.ctx,
})
prov, _ := vcs.Detect(p.Root)
a.diffPanel = vcsdiff.New(vcsdiff.Deps{VCS: prov, Pipeline: a.opt.Pipeline, Docs: a.ctx, Selection: a.ctx, Notices: a.ctx, Tasks: a.tasks,
ShowHierarchy: func() { a.ws.Show(workspace.Hierarchy) }})
a.healthP = health.New(a.healthDeps())
if err := a.initExport(); err != nil {
return err
}
a.taskPanel = taskcenter.New(a.tasks)
var compare func(string)
if prov != nil {
compare = a.compare
}
a.favs = favorites.Load(p.Root)
if err := a.favs.Err(); err != nil {
a.log.Warn("favorites are unreadable", log.F("err", err.Error()))
}
a.assetsPanel = assets.New(assets.Deps{
Favorites: a.favs,
Refs: a.refs,
ShowReferences: a.showReferences,
BatchRename: a.batchRenameAssets,
Compare: compare,
Schema: a.opt.Types.Schema(),
Inspect: func(id ids.AssetID) { a.data.Inspect(id) },
OpenTable: a.openTable,
DataType: a.data.TypeOf,
Files: a.files,
Pipeline: a.opt.Pipeline,
Thumbs: a.thumbs,
Docs: a.ctx,
Selection: a.ctx,
Scenes: sops,
Commands: a.ctx.Commands,
Notices: a.ctx,
Nav: a.ctx,
Project: p,
Prefabs: a.prefabs,
})
a.assetsPanel.Instantiate = a.instantiatePrefab
a.hierarchy = hierarchy.New(hierarchy.Deps{
Templates: a.templates,
Docs: a.ctx,
Selection: a.ctx,
Commands: a.ctx.Commands,
Scenes: sops,
Notices: a.ctx,
Tools: &a.ctx.Settings,
Prefabs: a.prefabs,
})
a.viewport = &services.Viewport{
UI: ui,
GPU: e.GPU(),
Renderer: r,
Shapes: a.shapes,
Overlay: &a.ctx.Overlay,
OnRender: a.ctx.OnRender,
RenderWorld: func(w *engine.World) {
e.RunPhase(w, engine.PhaseRenderPrepare, engine.ScopeEditor, 0, 0)
e.RunPhase(w, engine.PhaseRender, engine.ScopeEditor, 0, 0)
},
}
a.sceneView = sceneview.New(a.gizmos, sceneview.Deps{
Capture: a.captureTexture,
Viewport: a.viewport,
Resolver: res,
Docs: a.ctx,
Selection: a.ctx,
Tools: &a.ctx.Settings,
Commands: a.ctx.Commands,
Notices: a.ctx,
Scenes: sops,
Files: a.files,
Pipeline: a.opt.Pipeline,
Project: p,
Templates: a.templates,
})
a.ctx.NewScene = a.createScene
a.ctx.NewPrefab = a.createPrefab
a.gameView = gameview.New(a.play, a.viewport)
a.profiler = profiler.New(profiler.Deps{
Tracer: e.Trace(),
Root: p.Root,
Notices: a.ctx,
Nav: a.ctx,
PhaseAllocs: e.PhaseAllocs,
SetProbe: e.SetPhaseAllocs,
Sections: func() []context.ProfilerSection { return a.ctx.Profiler },
Throttled: func() bool { return a.opt.Paced && a.idle() },
})
a.ws = workspace.New(p.Root, a.log)
a.ws.Notices, a.ws.Tracer = a.ctx, e.Trace()
a.consolePanel = console.New(a.consoleSink)
a.problemsPanel = eproblems.New(a.ctx, a.ctx)
playWorld := func() *engine.World { return a.play.World() }
a.eventsPanel = events.New(events.Deps{World: playWorld, Selection: a.ctx, Notices: a.ctx, Root: p.Root})
a.watchPanel = watch.New(watch.Deps{World: playWorld, Scripts: func() context.ScriptHost { return a.ctx.Scripts }, Viewport: a.viewport})
a.crashesPanel = crashes.New(p.Root, a.ctx)
a.savesPanel = saves.New(saves.Deps{
Store: func() *save.Store {
st, _ := engine.Service[*save.Store](e, persistence.StoreID)
return st
},
Service: func() persistence.Service {
w := a.play.World()
if w == nil {
return nil
}
s, _ := engine.ServiceOf[persistence.Service](w, persistence.ServiceID)
return s
},
Label: func(id ids.AssetID) string {
if a.opt.Pipeline != nil {
if rel, ok := a.opt.Pipeline.PathOf(id); ok {
return rel
}
}
return id.Short()
},
Notices: a.ctx,
})
a.ctx.Crash = a.opt.Crash
a.ws.OnCrash = func(id string, rec any, stack []byte) {
if a.ctx.Crash != nil {
a.ctx.Crash("panel "+id, rec, stack)
}
}
a.ctx.ShowEvents = func(frame uint64) {
a.ws.Show(events.ID)
a.eventsPanel.Focus(frame)
}
a.play.SetBreak(func() (string, bool) {
if why, ok := a.eventsPanel.Check(); ok {
return why, true
}
return a.watchPanel.Check()
})
a.presets = presets.New(a.ctx)
a.inspectorPanel = inspector.New(a.inspectors, inspector.Deps{
Presets: a.presets,
Data: a.data,
Schema: a.opt.Types.Schema(),
Docs: a.ctx,
Selection: a.ctx,
Commands: a.ctx.Commands,
Notices: a.ctx,
Nav: a.ctx,
Problems: a.ctx,
Sections: a.opt.Sections,
Pipeline: a.opt.Pipeline,
Thumbs: a.thumbs,
Play: a.play,
Code: func() context.ScriptNav { return a.ctx.Code },
Clip: edclip.EnvOf(a.ctx),
Assets: sops,
Scenes: sops,
Collapsed: a.ctx.Collapsed,
Watch: func(expr string) {
a.watchPanel.Add(expr)
a.ws.Show(watch.ID)
},
Prefabs: a.prefabs,
})
a.docs = edocs.New(a.opt.Help, a.ctx, a.ctx)
a.docs.Run = a.opt.RunSnippet
a.ctx.ShowDocs = func(ref, query string) {
a.ws.Show(edocs.ID)
a.docs.Show(ref, query)
}
a.keymap = help.NewKeymap(a.ctx.Commands)
a.settings = projectsettings.New(projectsettings.Deps{
Project: p,
Sections: a.opt.Sections,
Settings: a.opt.Settings,
OnApply: a.liveSettings,
Pages: func() []context.SettingsPage { return a.ctx.Pages },
Pipeline: a.opt.Pipeline,
Notices: a.ctx,
})
a.welcome = welcome.New(p.Root, a.welcomeChecks())
a.welcome.Heading, _ = a.helpEntry("welcome")
_, a.welcome.Outro = a.helpEntry("welcome/done")
a.welcome.Reference = func() { a.ctx.ShowDocs(edocs.Language, "") }
if a.opt.NewProject != nil {
a.welcome.NewProject = a.askNewProject
a.welcome.Empty = a.emptyProject
}
if err := a.registerPanels(); err != nil {
return err
}
a.scriptLog = lg.With(log.Channel("script"))
a.ctx.ShowProblems = func(filter string) {
a.ws.Show(workspace.Problems)
a.problemsPanel.Focus(filter)
}
a.ctx.RevealPath = func(rel string) {
a.ws.Show(workspace.Assets)
a.assetsPanel.Reveal(rel)
}
a.search = search.New(search.Deps{
Favorites: a.favs,
Pipeline: a.opt.Pipeline,
Symbols: func() []context.SearchItem {
if a.ctx.Code == nil {
return nil
}
return a.ctx.Code.Symbols()
},
Docs: a.ctx,
Selection: a.ctx,
Commands: a.ctx.Commands,
Notices: a.ctx,
Nav: a.ctx,
Settings: a.settingItems,
})
a.search.ShowHierarchy = func() { a.ws.Show(workspace.Hierarchy) }
a.attachCode()
if err := a.registerCommands(); err != nil {
return err
}
if err := a.registerMeta(); err != nil {
return err
}
if err := a.registerTime(); err != nil {
return err
}
if err := a.registerSearch(); err != nil {
return err
}
if err := a.registerHelp(); err != nil {
return err
}
if err := a.registerWorkspaces(); err != nil {
return err
}
if err := a.registerEditExtras(); err != nil {
return err
}
if err := a.registerCapture(); err != nil {
return err
}
if err := a.registerReferences(); err != nil {
return err
}
if err := a.registerAssetTools(); err != nil {
return err
}
a.playDiff = playdiffpanel.New(playdiffpanel.Deps{Docs: a.ctx, Notices: a.ctx})
if err := a.registerPlayDiff(); err != nil {
return err
}
a.ext = ext.New(ext.Deps{
Editor: a.ctx,
Workspace: a.ws,
Inspectors: a.inspectors,
Gizmos: a.gizmos,
Templates: a.templates,
Shapes: a.shapes,
Commands: a.ctx.Commands,
Schema: a.opt.Types.Schema(),
})
for _, h := range a.opt.Hooks {
if err := a.ext.Run(h.Owner, h.Fn); err != nil {
a.ctx.Notify(context.LevelError, "extension "+h.Owner+" failed to register: "+err.Error())
}
}
if err := vectors.Register(a.ctx, a.shapes); err != nil {
return err
}
if err := gameview.Register(a.ctx.Commands, &a.ctx.Overlay); err != nil {
return err
}
a.ctx.ShowSettings = func(page string) {
a.settings.Show(page)
a.ws.Show(projectsettings.ID)
}
if err := a.ctx.Commands.Register(command.Desc{
ID: projectsettings.CommandOpen, Name: "Project Settings…", Category: "File", Order: 800,
Run: func() error {
a.ctx.Open(context.SettingsAt(""))
return nil
},
}); err != nil {
return err
}
for _, d := range []command.Desc{
{ID: projectsettings.CommandUndo, Name: "Undo Settings Change", Scope: projectsettings.Scope,
Shortcut: command.Key(input.KeyZ, command.ModPrimary), Enabled: a.settings.CanUndo,
Run: func() error { a.settings.Undo(); return nil }},
{ID: projectsettings.CommandRedo, Name: "Redo Settings Change", Scope: projectsettings.Scope,
Shortcut: command.Key(input.KeyZ, command.ModPrimary|command.ModShift), Alt: []command.Shortcut{command.Key(input.KeyY, command.ModPrimary)},
Enabled: a.settings.CanRedo, Run: func() error { a.settings.Redo(); return nil }},
} {
if err := a.ctx.Commands.Register(d); err != nil {
return err
}
}
if err := a.registerWindowCommands(); err != nil {
return err
}
a.initNativeMenu()
a.describeCommands()
a.iconCommands()
a.ws.LoadLayout()
unlock, other, err := document.AcquireLock(p.Root)
a.unlock = unlock
if err != nil {
a.log.Warn("cannot write the editor lock", log.F("err", err.Error()))
}
if other != nil {
a.push(modal{kind: modalLock, lock: *other})
}
a.restoreState()
a.buildState = widgets.DotOK
if st, ok := session.ReadStatus(p.Root); ok {
a.buildSeen = st.Time
}
if a.ctx.Docs.Active() == nil {
a.openMainScene()
}
for _, rec := range a.ctx.Docs.FindRecovery() {
a.push(modal{kind: modalRecovery, rec: rec})
}
a.firstRun()
now := e.Platform().Now()
a.checkAt, a.recoverAt = now+1, now+30
return nil
}
func (a *App) openMainScene() {
p := a.opt.Project
if p.MainScene.IsZero() || a.opt.Pipeline == nil {
return
}
rel, ok := a.opt.Pipeline.PathOf(p.MainScene)
if !ok {
return
}
if _, err := a.ctx.Docs.Open(rel); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
func (a *App) Event(ev platform.Event) {
a.lastInput = a.eng.Platform().Now()
switch ev.Kind {
case platform.EvKeyDown:
a.shiftTap(ev)
a.paletteKey(ev)
case platform.EvTextEditing:
a.ui.SetComposition(ev.Str, int(ev.EditStart))
case platform.EvTextInput:
a.ui.SetComposition("", 0)
case platform.EvWindowFocusGained:
a.checkAt = 0
a.clip.invalidate()
case platform.EvWindowFocusLost:
a.gameView.Release()
if a.opt.Code != nil {
a.opt.Code.FlushAutosave()
}
case platform.EvDropFile:
a.drops = append(a.drops, ev.Str)
a.dropPos = ev.Mouse.Pos
case platform.EvDropComplete:
if len(a.drops) > 0 {
label := a.drops[0]
if len(a.drops) > 1 {
label = "files"
}
a.ctx.DnD.External(dnd.Payload{Kind: context.KindFiles, Label: label, Data: a.drops}, a.dropPos)
a.drops = nil
}
case platform.EvQuit:
if !a.quitting && a.ctx.Docs.Dirty() {
a.eng.CancelQuit()
a.push(modal{kind: modalQuit})
}
}
}
func (a *App) FixedUpdate(float64) {}
func (a *App) Update(dt float64) {
e := a.eng
now := e.Platform().Now()
ui := a.ui
ui.Time = now
var rs render.Stats
if a.opt.RenderStats != nil {
rs = a.opt.RenderStats()
}
var sc context.ScriptStats
hasSc := false
if a.ctx.Scripts != nil {
sc, hasSc = a.ctx.Scripts.Stats()
}
a.profiler.Record(now, e.Stats(), rs, a.play.Stats(), sc, hasSc, a.play.State() != play.Stopped)
a.play.Tick(dt)
a.tickTasks()
a.syncNativeMenu(now)
a.showPlayDiff()
if a.full && !a.play.Captured() {
a.exitFull()
}
in := e.Input()
switch {
case ui.ModalActive() || a.playDiff.Active() || a.renameDlg.Active() || a.ws.Dragging() || a.ctx.DnD.Active() || a.play.Captured():
case ui.Focus() == 0:
a.ctx.Commands.Dispatch(in, a.ws.Scope(a.ws.Hovered()))
default:
a.globalKeys(in)
}
m := e.Metrics()
size := mathx.Vec2{X: float32(m.LogicalW), Y: float32(m.LogicalH)}
ui.Begin(in, size, m.Density())
a.ctx.FocusedPanel = a.ws.Focused()
th := &ui.Theme
full := mathx.Rect{W: size.X, H: size.Y}
ui.Rect(full, th.Background)
frame := services.Frame{UI: ui, DnD: a.ctx.DnD, Now: now}
if a.full {
frame.Focused = true
ui.Region(full)
a.gameView.Draw(&frame, full)
ui.PopCursor()
} else {
bh := th.BarHeight
menuH := bh
if a.native != nil {
menuH = 0
}
tool := float32(0)
if a.ws.Toolbar() {
tool = bh
}
menuR := mathx.Rect{W: size.X, H: menuH}
toolR := mathx.Rect{Y: menuH, W: size.X, H: tool}
bottom := mathx.Rect{Y: size.Y - th.MenuHeight, W: size.X, H: th.MenuHeight}
body := mathx.Rect{Y: menuH + tool, W: size.X, H: max(0, size.Y-menuH-tool-bottom.H)}
a.ws.Draw(frame, body)
if a.play.State() != play.Stopped {
l := ui.List()
l.PushLayer(draw.LayerTop)
l.RectOutline(body, th.PlayTint, 2)
l.PopLayer()
}
a.drawScriptsBanner(body)
a.drawStatus(bottom)
if tool > 0 {
a.drawToolbar(toolR)
}
if menuH > 0 {
a.drawMenuBar(menuR)
}
a.drawPulse()
}
for _, q := range a.ctx.TakeQuestions() {
a.modals = append(a.modals, modal{kind: modalAsk, ask: q, vals: append([]string(nil), q.Values...), focus: true})
}
a.drawSearch()
a.playDiff.Draw(ui)
a.renameDlg.Draw(ui)
a.drawScenePicker()
a.drawModals()
a.ctx.DnD.Frame(ui)
a.ext.Flush()
a.list = ui.End()
a.applyPlatform()
a.periodic(now)
}
func (a *App) Render(float64) {
defer a.pace()
a.ctx.RunRenderHooks()
if a.list == nil || a.list.Empty() {
return
}
m := a.eng.Metrics()
v := view.ScreenView(m.PixelW, m.PixelH, m.Density())
v.Clear = a.ui.Theme.Background
v.DoClear = true
v.Label = "editor.ui"
r := a.opt.Renderer
white := r.White()
if f := a.ui.Font(); f != nil && !f.White.IsZero() {
white = f.White
}
f := r.View(v)
a.list.Flush(f, a.ui.Font(), white, 0)
f.End()
}
func (a *App) tickTasks() {
a.thumbs.Begin()
a.tasks.Tick()
if a.tasks.WantsRender() {
a.ctx.OnRender(a.tasks.Render)
}
}
func (a *App) Previews() *preview.Service {
return a.thumbs
}
func (a *App) Tasks() *tasks.Queue {
return a.tasks
}
func (a *App) idle() bool {
now := a.eng.Platform().Now()
return a.play.State() == play.Stopped &&
now-a.lastInput > 1 &&
!a.ctx.DnD.Active() &&
!a.ws.Dragging() &&
!a.tasks.Busy() &&
a.ui.Captured() == 0 &&
len(a.modals) == 0
}
func (a *App) pace() {
if !a.opt.Paced {
return
}
target := time.Second / 60
if a.idle() {
target = time.Second / 15
}
if el := time.Since(a.lastFrame); el < target {
time.Sleep(target - el)
}
a.lastFrame = time.Now()
}
func (a *App) Shutdown() {
a.play.Stop()
a.saveState()
if err := a.ws.SaveLayout(); err != nil {
a.log.Warn("cannot save the layout", log.F("err", err.Error()))
}
if a.discard {
a.ctx.Docs.ClearAllRecovery()
} else if _, err := a.ctx.Docs.WriteRecovery(); err != nil {
a.log.Warn("cannot write recovery files", log.F("err", err.Error()))
}
a.sceneView.Close()
a.gameView.Close()
a.tasks.CancelAll()
a.thumbs.Close()
if a.unlock != nil {
a.unlock()
}
}
func (a *App) requestQuit() {
if a.ctx.Docs.Dirty() {
a.push(modal{kind: modalQuit})
return
}
a.quitting = true
a.eng.Quit()
}
func (a *App) scriptErrors() int {
if a.ctx.Scripts == nil {
return 0
}
return a.ctx.Scripts.Errors()
}
func (a *App) reportScripts() {
if a.ctx.Scripts == nil {
return
}
ps, rev := a.ctx.Scripts.Problems()
if rev == a.problemRev {
return
}
a.problemRev = rev
errors := 0
for _, p := range ps {
fields := []log.Field{log.F("path", p.Path), log.F("line", p.Line), log.F("col", p.Col), log.F("code", p.Code), log.F("package", p.Package)}
if p.Error {
errors++
a.scriptLog.Error(p.Message, fields...)
} else {
a.scriptLog.Warn(p.Message, fields...)
}
}
if errors == 0 {
a.scriptLog.Info("scripts compiled")
return
}
if !a.welcome.Done(hintErrors) {
a.welcome.Mark(hintErrors)
a.ctx.Notify(context.LevelInfo, "Tip: click a problem in the Problems panel to jump to its line, F2 walks through errors")
}
}
func (a *App) periodic(now float64) {
a.pollSession(now)
if a.restart {
return
}
a.reportScripts()
if now >= a.welcomeAt {
a.welcomeAt = now + 0.5
a.welcome.Tick()
}
if now >= a.checkAt {
a.checkAt = now + 1
reloaded, conflicts := a.ctx.Docs.CheckExternal()
for _, id := range reloaded {
a.ctx.PruneSelectionOf(id)
if d := a.ctx.Docs.Get(id); d != nil {
a.ctx.Notify(context.LevelInfo, "Reloaded "+d.Path)
}
}
for _, c := range conflicts {
a.push(modal{kind: modalConflict, doc: c.Doc, path: c.Path, removed: c.Removed})
}
}
if now >= a.recoverAt {
a.recoverAt = now + 30
if _, err := a.ctx.Docs.WriteRecovery(); err != nil {
a.log.Warn("cannot write recovery files", log.F("err", err.Error()))
}
}
a.ws.Tick(now)
if a.opt.Code != nil {
a.opt.Code.Tick(now)
}
a.updateTitle()
}
func (a *App) Restarting() bool {
return a.restart
}
func (a *App) Handoff() error {
a.play.Stop()
if err := session.Write(a.opt.Project.Root, a.handoff()); err != nil {
return err
}
a.restart = true
a.quitting = true
a.eng.Quit()
return nil
}
func (a *App) pollSession(now float64) {
if !a.opt.Supervised || a.restart || now < a.sessionAt {
return
}
a.sessionAt = now + 0.25
root := a.opt.Project.Root
if st, ok := session.ReadStatus(root); ok && st.Time.After(a.buildSeen) {
a.buildSeen = st.Time
switch st.State {
case session.StateBuilding:
a.buildState = widgets.DotBusy
case session.StateOK:
a.buildState = widgets.DotOK
a.ctx.Notify(context.LevelInfo, "Native code rebuilt, reloading the editor")
default:
a.buildState = widgets.DotError
a.log.Error("native build failed", log.F("output", st.Log))
a.ctx.Notify(context.LevelError, "Native build failed, the editor keeps the previous build (see Console)")
}
}
if session.RestartRequested(root) {
if err := a.Handoff(); err != nil {
session.ClearRestart(root)
a.ctx.Notify(context.LevelError, "cannot hand off the session: "+err.Error())
}
}
}
func (a *App) updateTitle() {
t := a.opt.Project.Name + " — Aego Editor"
if d := a.ctx.Docs.Edit(); d != nil {
name := path.Base(d.Path)
if d.Dirty() {
name += "*"
}
switch a.play.State() {
case play.Playing:
name += " [Play]"
case play.Paused:
name += " [Paused]"
}
t = name + " — " + t
}
if a.native != nil && a.opt.Supervised {
switch a.buildState {
case widgets.DotBusy:
t += " — native building"
case widgets.DotError:
t += " — Native API is stale"
}
}
if t != a.title {
a.title = t
a.eng.Window().SetTitle(t)
}
}
var _ services.Prefabs = (*prefabs.Service)(nil)
func (a *App) settingItems() []search.Item {
refs := a.settings.Index()
out := make([]search.Item, len(refs))
for i, r := range refs {
out[i] = search.Item{Label: r.Label, Detail: r.Detail, Run: func() {
a.settings.FocusRef(r)
a.ws.Show(projectsettings.ID)
}}
}
return out
}
func (a *App) liveSettings() string {
if a.play.State() == play.Stopped || a.opt.Modules == nil {
return ""
}
a.play.Invalidate("settings changed during Play")
ctx := &module.AttachContext{World: a.play.World(), Scene: a.play.Scene(), Log: a.opt.Log, Editor: true, Play: true}
if err := a.opt.Modules.Reconfigure(ctx); err != nil {
a.log.Warn("settings were saved but not applied to the running game", log.F("err", err.Error()))
return "some settings apply on the next Play"
}
return "applied to the running game"
}
func (a *App) openTable(id ids.AssetID) {
if err := a.tablePanel.Open(id); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
return
}
a.ws.Show(datatable.ID)
}