339 lines
7.8 KiB
Go
339 lines
7.8 KiB
Go
package settings
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/engine"
|
|
es "aego/engine/settings"
|
|
"aego/engine/settings/settingsui"
|
|
raw "aego/platform/input"
|
|
"aego/render"
|
|
"aego/render/text"
|
|
"aego/render/view"
|
|
"aego/scene/frame"
|
|
"aego/schema"
|
|
"aego/ui/core"
|
|
"aego/ui/draw"
|
|
"aego/ui/theme"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const uiScaleKey = "display.uiScale"
|
|
|
|
type View struct {
|
|
st *es.Store
|
|
world *engine.World
|
|
ui *core.Context
|
|
in raw.State
|
|
ed settingsui.Editor
|
|
bar widgets.ScrollState
|
|
open bool
|
|
paused bool
|
|
cat string
|
|
keys []es.Key
|
|
draft map[es.Key]schema.Value
|
|
scroll float32
|
|
content float32
|
|
note string
|
|
uiKey es.Key
|
|
}
|
|
|
|
func NewView(st *es.Store, w *engine.World) *View {
|
|
return &View{st: st, world: w, draft: map[es.Key]schema.Value{}, uiKey: st.Key(uiScaleKey)}
|
|
}
|
|
|
|
func (v *View) Opened() bool {
|
|
return v.open
|
|
}
|
|
|
|
func (v *View) Open() {
|
|
if v.open {
|
|
return
|
|
}
|
|
v.open = true
|
|
if t := v.world.Time; t != nil {
|
|
v.paused = t.Paused()
|
|
t.SetPaused(true)
|
|
}
|
|
if cats := v.cats(); v.cat == "" && len(cats) > 0 {
|
|
v.cat = cats[0]
|
|
}
|
|
v.refresh()
|
|
}
|
|
|
|
func (v *View) Close() {
|
|
if !v.open {
|
|
return
|
|
}
|
|
v.open = false
|
|
v.ed.Cancel()
|
|
clear(v.draft)
|
|
v.note = ""
|
|
if t := v.world.Time; t != nil {
|
|
t.SetPaused(v.paused)
|
|
}
|
|
}
|
|
|
|
func (v *View) Toggle() {
|
|
if v.open {
|
|
v.Close()
|
|
} else {
|
|
v.Open()
|
|
}
|
|
}
|
|
|
|
func (v *View) shows(d *es.Desc, cat string) bool {
|
|
return d.Flags.Has(es.User) && (d.Category == cat || (cat == es.CategoryAccessibility && d.Flags.Has(es.Accessibility)))
|
|
}
|
|
|
|
func (v *View) cats() []string {
|
|
var out []string
|
|
for _, c := range es.Categories {
|
|
has := false
|
|
v.st.Each(func(_ es.Key, d *es.Desc) { has = has || v.shows(d, c) })
|
|
if has {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (v *View) refresh() {
|
|
v.keys = v.keys[:0]
|
|
v.st.Each(func(k es.Key, d *es.Desc) {
|
|
if v.shows(d, v.cat) {
|
|
v.keys = append(v.keys, k)
|
|
}
|
|
})
|
|
slices.SortStableFunc(v.keys, func(a, b es.Key) int { return v.st.Desc(a).Order - v.st.Desc(b).Order })
|
|
v.scroll = 0
|
|
}
|
|
|
|
func (v *View) setup(e *engine.Engine, r *render.Renderer) {
|
|
ui := core.New(r.UIFont())
|
|
ui.Theme = theme.Editor()
|
|
if e != nil {
|
|
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)
|
|
}
|
|
if sh, err := draw.NewShapes(e.GPU(), r.Textures()); err == nil {
|
|
ui.SetShapes(sh)
|
|
}
|
|
}
|
|
v.ui = ui
|
|
}
|
|
|
|
func (v *View) draw(ctx *engine.Context, fr *frame.Runner) {
|
|
rnd := fr.Renderer()
|
|
out := fr.Output()
|
|
if out.W <= 0 || out.H <= 0 {
|
|
return
|
|
}
|
|
if v.ui == nil {
|
|
v.setup(ctx.Engine, rnd)
|
|
}
|
|
scale := out.Scale
|
|
if scale <= 0 {
|
|
scale = 1
|
|
}
|
|
us := float32(v.st.Float(v.uiKey))
|
|
if us <= 0 {
|
|
us = 1
|
|
}
|
|
px := scale * us
|
|
size := mathx.Vec2{X: float32(out.W) / px, Y: float32(out.H) / px}
|
|
f := ctx.Input().Snapshot()
|
|
f.MousePos = f.MousePos.Scale(1 / us)
|
|
v.in.Restore(f)
|
|
v.ui.Begin(&v.in, size, px)
|
|
v.layout(size)
|
|
list := v.ui.End()
|
|
sv := view.ScreenView(out.W, out.H, px)
|
|
sv.Target, sv.TargetW, sv.TargetH = out.Target, out.TargetW, out.TargetH
|
|
sv.DoClear = false
|
|
sv.Label = SystemDraw
|
|
vf := rnd.View(sv)
|
|
white := rnd.White()
|
|
if font := v.ui.Font(); font != nil && !font.White.IsZero() {
|
|
white = font.White
|
|
}
|
|
list.Flush(vf, v.ui.Font(), white, 0)
|
|
vf.End()
|
|
}
|
|
|
|
func (v *View) layout(size mathx.Vec2) {
|
|
ui := v.ui
|
|
th := &ui.Theme
|
|
if ui.KeyPressed(raw.KeyEscape) && !v.ed.Capturing() {
|
|
v.Close()
|
|
return
|
|
}
|
|
ui.Rect(mathx.Rect{W: size.X, H: size.Y}, mathx.Color{A: 0.55})
|
|
w, h := min(860, size.X-32), min(620, size.Y-32)
|
|
if w < 240 || h < 180 {
|
|
return
|
|
}
|
|
r := mathx.Rect{X: (size.X - w) / 2, Y: (size.Y - h) / 2, W: w, H: h}
|
|
ui.Shadow(r, th.Radius.Popup)
|
|
ui.Box(r, th.Radius.Popup, th.Panel, th.Border)
|
|
pad := th.Padding * 2
|
|
head := mathx.Rect{X: r.X + pad, Y: r.Y + pad, W: r.W - 2*pad, H: th.RowHeight}
|
|
ui.Text("Settings", mathx.Vec2{X: head.X, Y: ui.TextY(head)}, th.Text)
|
|
tabs := mathx.Rect{X: head.X, Y: head.Y + head.H + th.Spacing*3, W: head.W, H: th.RowHeight}
|
|
v.tabs(ui, tabs)
|
|
foot := mathx.Rect{X: head.X, Y: r.Y + r.H - pad - th.RowHeight, W: head.W, H: th.RowHeight}
|
|
body := mathx.Rect{X: head.X, Y: tabs.Y + tabs.H + th.Spacing*4, W: head.W}
|
|
body.H = max(0, foot.Y-th.Spacing*3-body.Y)
|
|
if left, ok := v.st.Pending(); ok {
|
|
banner := mathx.Rect{X: body.X, Y: body.Y + body.H - th.RowHeight, W: body.W, H: th.RowHeight}
|
|
body.H = max(0, body.H-th.RowHeight-th.Spacing*2)
|
|
v.confirm(ui, banner, left)
|
|
}
|
|
v.list(ui, body)
|
|
v.footer(ui, foot)
|
|
}
|
|
|
|
func (v *View) tabs(ui *core.Context, r mathx.Rect) {
|
|
x := r.X
|
|
for i, c := range v.cats() {
|
|
w := ui.TextSize(c).X + 24
|
|
on := c == v.cat
|
|
if widgets.ToggleButton(ui, core.IDSeq(ui.ID("tab"), i), mathx.Rect{X: x, Y: r.Y, W: w, H: r.H}, c, &on) && c != v.cat {
|
|
v.cat = c
|
|
v.ed.Cancel()
|
|
v.refresh()
|
|
}
|
|
x += w + 4
|
|
}
|
|
}
|
|
|
|
func (v *View) list(ui *core.Context, r mathx.Rect) {
|
|
th := &ui.Theme
|
|
if ui.MouseOver(ui.ID("scroll"), r) {
|
|
if d := ui.ScrollY(th.RowHeight * 3); d != 0 {
|
|
v.scroll -= d
|
|
}
|
|
}
|
|
v.scroll = mathx.Clamp(v.scroll, 0, max(0, v.content-r.H))
|
|
ui.PushClip(r)
|
|
ui.PushCursor(core.Cursor{X: r.X, Y: r.Y - v.scroll, W: r.W - th.ScrollbarW, RowH: th.RowHeight, Pad: th.Padding, Space: th.Spacing + 2})
|
|
start := ui.Cursor().Y
|
|
value := func(k es.Key) schema.Value {
|
|
if val, ok := v.draft[k]; ok {
|
|
return val
|
|
}
|
|
return v.st.Value(k)
|
|
}
|
|
for _, k := range v.keys {
|
|
if settingsui.Paired(v.st, k) {
|
|
continue
|
|
}
|
|
d := v.st.Desc(k)
|
|
ui.PushID(d.ID)
|
|
val := value(k)
|
|
if m := settingsui.Partner(v.st, k); m != 0 {
|
|
mv := value(m)
|
|
vc, mc := v.ed.Pair(ui, v.st, k, &val, m, &mv)
|
|
if vc {
|
|
v.draft[k] = val
|
|
}
|
|
if mc {
|
|
v.draft[m] = mv
|
|
}
|
|
} else if v.ed.Field(ui, v.st, k, &val) {
|
|
v.draft[k] = val
|
|
}
|
|
ui.PopID()
|
|
}
|
|
v.content = ui.Cursor().Y - start
|
|
ui.PopCursor()
|
|
ui.PopClip()
|
|
track := mathx.Rect{X: r.X + r.W - th.ScrollbarW, Y: r.Y, W: th.ScrollbarW, H: r.H}
|
|
widgets.ScrollBar(ui, ui.ID("vbar"), track, &v.scroll, v.content, r.H, &v.bar)
|
|
}
|
|
|
|
func (v *View) confirm(ui *core.Context, r mathx.Rect, left float64) {
|
|
th := &ui.Theme
|
|
bw := float32(90)
|
|
ui.Text("Keep these display settings? Reverting in "+strconv.Itoa(int(left)+1)+" s", mathx.Vec2{X: r.X, Y: ui.TextY(r)}, th.Warn)
|
|
if widgets.ButtonRect(ui, ui.ID("keep"), mathx.Rect{X: r.X + r.W - 2*bw - 4, Y: r.Y, W: bw, H: r.H}, "Keep") {
|
|
v.st.Confirm()
|
|
}
|
|
if widgets.ButtonRect(ui, ui.ID("undo"), mathx.Rect{X: r.X + r.W - bw, Y: r.Y, W: bw, H: r.H}, "Revert") {
|
|
_ = v.st.RevertPending()
|
|
}
|
|
}
|
|
|
|
func (v *View) footer(ui *core.Context, r mathx.Rect) {
|
|
th := &ui.Theme
|
|
bw := float32(90)
|
|
x := r.X + r.W
|
|
button := func(name, label string) bool {
|
|
x -= bw
|
|
hit := widgets.ButtonRect(ui, ui.ID(name), mathx.Rect{X: x, Y: r.Y, W: bw, H: r.H}, label)
|
|
x -= 4
|
|
return hit
|
|
}
|
|
if button("close", "Close") {
|
|
v.Close()
|
|
return
|
|
}
|
|
if button("defaults", "Defaults") {
|
|
for _, k := range v.keys {
|
|
v.draft[k] = v.st.Base(k)
|
|
}
|
|
}
|
|
if button("revert", "Revert") {
|
|
clear(v.draft)
|
|
v.note = ""
|
|
}
|
|
if button("apply", "Apply") && len(v.draft) > 0 {
|
|
v.apply()
|
|
}
|
|
if v.note != "" {
|
|
ui.PushClip(mathx.Rect{X: r.X, Y: r.Y, W: max(0, x-r.X), H: r.H})
|
|
ui.Text(v.note, mathx.Vec2{X: r.X, Y: ui.TextY(r)}, th.TextDim)
|
|
ui.PopClip()
|
|
}
|
|
}
|
|
|
|
func (v *View) apply() {
|
|
tx := v.st.Begin()
|
|
confirm := false
|
|
for k, val := range v.draft {
|
|
if err := tx.Set(k, val); err != nil {
|
|
v.note = err.Error()
|
|
return
|
|
}
|
|
confirm = confirm || v.st.Desc(k).Flags.Has(es.Confirm)
|
|
}
|
|
var err error
|
|
if confirm {
|
|
err = tx.ApplyWithConfirm(es.DefaultConfirm)
|
|
} else {
|
|
err = tx.Apply()
|
|
}
|
|
if err != nil {
|
|
v.note = err.Error()
|
|
return
|
|
}
|
|
clear(v.draft)
|
|
v.note = ""
|
|
if v.st.NeedsRestart() {
|
|
v.note = "Some changes apply after a restart"
|
|
}
|
|
}
|
|
|
|
func (v *View) DrawUI(ui *core.Context, size mathx.Vec2) {
|
|
v.ui = ui
|
|
v.layout(size)
|
|
}
|
|
|
|
func (v *View) Select(cat string) {
|
|
v.cat = cat
|
|
v.ed.Cancel()
|
|
v.refresh()
|
|
}
|