271 lines
6.2 KiB
Go
271 lines
6.2 KiB
Go
package scenes
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/asset/pipeline"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/assetfiles"
|
|
"aego/editor/core/assetops"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/services"
|
|
"aego/project"
|
|
"aego/render/texture"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
ID = "scenes"
|
|
tileW = float32(150)
|
|
thumb = float32(96)
|
|
)
|
|
|
|
type Previews interface {
|
|
Get(info pipeline.AssetInfo) (texture.Region, bool)
|
|
}
|
|
|
|
type Deps struct {
|
|
Assets func() []pipeline.AssetInfo
|
|
Previews Previews
|
|
Files *assetfiles.Files
|
|
Docs services.Documents
|
|
Notices services.Notices
|
|
Project *project.Project
|
|
Reveal func(rel string)
|
|
}
|
|
|
|
type Panel struct {
|
|
d Deps
|
|
f *services.Frame
|
|
all []pipeline.AssetInfo
|
|
view []int
|
|
at float64
|
|
filter string
|
|
sel ids.AssetID
|
|
menuOn pipeline.AssetInfo
|
|
menu core.ID
|
|
scroll float32
|
|
}
|
|
|
|
func New(d Deps) *Panel {
|
|
return &Panel{d: d, at: -10}
|
|
}
|
|
|
|
func (p *Panel) ID() string {
|
|
return ID
|
|
}
|
|
|
|
func (p *Panel) Title() string {
|
|
return "Scenes"
|
|
}
|
|
|
|
func (p *Panel) Icon() string {
|
|
return "panel.scenes"
|
|
}
|
|
|
|
func (p *Panel) Refresh() {
|
|
p.at = -10
|
|
}
|
|
|
|
func (p *Panel) startup(id ids.AssetID) bool {
|
|
return p.d.Project != nil && p.d.Project.MainScene == id
|
|
}
|
|
|
|
func (p *Panel) refresh(now float64) {
|
|
if now-p.at < 0.5 || p.d.Assets == nil {
|
|
return
|
|
}
|
|
p.at = now
|
|
p.all = p.all[:0]
|
|
for _, a := range p.d.Assets() {
|
|
if a.Kind == asset.ArtScene {
|
|
p.all = append(p.all, a)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Panel) build() {
|
|
q := strings.ToLower(strings.TrimSpace(p.filter))
|
|
p.view = p.view[:0]
|
|
for i := range p.all {
|
|
if q == "" || strings.Contains(strings.ToLower(p.all[i].Path), q) {
|
|
p.view = append(p.view, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
|
p.f = f
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
p.menu = ui.ID("menu")
|
|
p.refresh(f.Now)
|
|
widgets.SearchField(ui, ui.ID("filter"), ui.Cursor().Next(0), &p.filter)
|
|
p.build()
|
|
top := ui.Cursor().Y
|
|
body := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
|
|
if len(p.view) == 0 {
|
|
msg := "No scenes yet, File → New Scene creates one"
|
|
if p.filter != "" {
|
|
msg = "No scenes match"
|
|
}
|
|
ui.TextIn(body, msg, th.TextDisabled, core.AlignCenter)
|
|
p.contextMenu()
|
|
return
|
|
}
|
|
cols := max(1, int(body.W/tileW))
|
|
tileH := thumb + 2*th.RowHeight + 12
|
|
rows := (len(p.view) + cols - 1) / cols
|
|
widgets.VirtualList(ui, ui.ID("grid"), body, rows, tileH, &p.scroll, func(row int, rr mathx.Rect) {
|
|
for c := 0; c < cols; c++ {
|
|
i := row*cols + c
|
|
if i >= len(p.view) {
|
|
return
|
|
}
|
|
p.tile(&p.all[p.view[i]], mathx.Rect{X: rr.X + float32(c)*tileW + 2, Y: rr.Y + 2, W: tileW - 4, H: tileH - 4})
|
|
}
|
|
})
|
|
p.contextMenu()
|
|
}
|
|
|
|
func (p *Panel) tile(a *pipeline.AssetInfo, r mathx.Rect) {
|
|
ui := p.f.UI
|
|
th := &ui.Theme
|
|
id := core.IDOf("scenes.tile/" + a.Path)
|
|
if ui.Pressed(id, r) {
|
|
p.sel = a.ID
|
|
if ui.DoubleClicked(id) {
|
|
p.open(a)
|
|
}
|
|
}
|
|
if ui.RightClicked(id, r) {
|
|
p.sel, p.menuOn = a.ID, *a
|
|
widgets.OpenContextMenu(ui, p.menu)
|
|
}
|
|
switch {
|
|
case p.sel == a.ID:
|
|
ui.Rect(r, th.Selection)
|
|
case ui.Hot() == id:
|
|
ui.Rect(r, th.WidgetHot)
|
|
}
|
|
img := mathx.Rect{X: r.X + (r.W-thumb)/2, Y: r.Y + 4, W: thumb, H: thumb}
|
|
ui.Rect(img, th.PanelAlt)
|
|
var reg texture.Region
|
|
ok := false
|
|
if p.d.Previews != nil {
|
|
reg, ok = p.d.Previews.Get(*a)
|
|
}
|
|
if ok {
|
|
ui.List().Texture(reg, img, mathx.Color{R: 1, G: 1, B: 1, A: 1})
|
|
} else {
|
|
widgets.IconAt(ui, mathx.Rect{X: img.X + thumb/2 - 16, Y: img.Y + thumb/2 - 16, W: 32, H: 32}, "file.scene", th.TextDim)
|
|
}
|
|
start := p.startup(a.ID)
|
|
if start {
|
|
widgets.IconAt(ui, mathx.Rect{X: img.X + img.W - 18, Y: img.Y + 2, W: 16, H: 16}, "play.start", th.Accent)
|
|
}
|
|
name := mathx.Rect{X: r.X + 2, Y: img.Y + img.H + 2, W: r.W - 4, H: th.RowHeight}
|
|
dir := mathx.Rect{X: name.X, Y: name.Y + th.RowHeight, W: name.W, H: th.RowHeight}
|
|
ui.PushClip(r)
|
|
ui.TextIn(name, assetops.Stem(a.Path), th.Text, core.AlignCenter)
|
|
ui.TextIn(dir, path.Dir(a.Path), th.TextDim, core.AlignCenter)
|
|
ui.PopClip()
|
|
tip := a.Path
|
|
if start {
|
|
tip += " · startup scene"
|
|
}
|
|
widgets.Tooltip(ui, id, tip)
|
|
}
|
|
|
|
func (p *Panel) notify(err error) {
|
|
if err != nil && p.d.Notices != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
}
|
|
|
|
func (p *Panel) open(a *pipeline.AssetInfo) {
|
|
if p.d.Docs == nil {
|
|
return
|
|
}
|
|
_, err := p.d.Docs.Documents().Open(a.Path)
|
|
p.notify(err)
|
|
}
|
|
|
|
func (p *Panel) contextMenu() {
|
|
ui := p.f.UI
|
|
if !widgets.ContextMenu(ui, p.menu) {
|
|
return
|
|
}
|
|
a := p.menuOn
|
|
files := p.d.Files != nil
|
|
if widgets.MenuItemIcon(ui, "Open", "", "file.open", true, false) {
|
|
p.open(&a)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Set as Startup", "", "play.start", files && !p.startup(a.ID), p.startup(a.ID)) {
|
|
p.d.Files.SetStartup(a.ID, a.Path)
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Rename…", "", "edit.rename", files, false) {
|
|
p.rename(a)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Duplicate", "", "edit.duplicate", files, false) {
|
|
_, err := p.d.Files.Duplicate(a.ID)
|
|
p.notify(err)
|
|
p.Refresh()
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Delete…", "", "edit.delete", files, false) {
|
|
p.remove(a)
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Show in Assets", "", "panel.assets", p.d.Reveal != nil, false) {
|
|
p.d.Reveal(a.Path)
|
|
}
|
|
widgets.EndContextMenu(ui)
|
|
}
|
|
|
|
func (p *Panel) rename(a pipeline.AssetInfo) {
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "Rename scene",
|
|
Fields: []string{"Name"},
|
|
Values: []string{assetops.Stem(a.Path)},
|
|
Buttons: []string{"Rename", "Cancel"},
|
|
Submit: func(v []string) {
|
|
name := strings.TrimSpace(v[0])
|
|
if !assetops.ValidName(name) {
|
|
p.notify(errInvalid(name))
|
|
return
|
|
}
|
|
to := path.Join(path.Dir(a.Path), name+path.Ext(a.Path))
|
|
if to == a.Path {
|
|
return
|
|
}
|
|
p.notify(p.d.Files.Apply([]assetops.FileOp{{ID: a.ID, From: a.Path, To: to}}))
|
|
p.Refresh()
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) remove(a pipeline.AssetInfo) {
|
|
msg := "Delete " + a.Path + "?"
|
|
if p.startup(a.ID) {
|
|
msg += " It is the startup scene; the project will have none until you pick another."
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "Delete scene",
|
|
Message: msg,
|
|
Buttons: []string{"Delete", "Cancel"},
|
|
Then: func(choice int) {
|
|
if choice != 0 {
|
|
return
|
|
}
|
|
p.notify(p.d.Files.Delete([]ids.AssetID{a.ID}))
|
|
p.sel = ids.AssetID{}
|
|
p.Refresh()
|
|
},
|
|
})
|
|
}
|