Files
aego/editor/panels/playdiff/dialog.go
sanspie f22242e5aa
ci / release (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
Stage 11g2–11g3: registry texts, CLI and the site in Russian (D348)
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
2026-10-25 15:31:22 +03:00

266 lines
7.0 KiB
Go

package playdiff
import (
"strconv"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/document"
pd "aego/editor/core/playdiff"
"aego/editor/core/services"
"aego/engine/i18n"
"aego/platform/input"
"aego/ui/core"
"aego/ui/widgets"
)
type Deps struct {
Docs services.Documents
Notices services.Notices
ShowAll func() bool
SetShowAll func(bool)
}
type Dialog struct {
d Deps
id core.ID
open bool
doc document.ID
res pd.Result
hist pd.Result
full pd.Result
scroll float32
}
func New(d Deps) *Dialog {
return &Dialog{d: d, id: core.IDOf("playdiff")}
}
func (g *Dialog) Active() bool {
return g.open
}
func (g *Dialog) Open(ui *core.Context, doc document.ID, hist, full pd.Result) {
g.open, g.doc, g.hist, g.full, g.scroll = true, doc, hist, full, 0
g.res = hist
if g.d.ShowAll != nil && g.d.ShowAll() {
g.res = full
}
ui.OpenModal(g.id)
}
func (g *Dialog) showAll(ui *core.Context) {
on := g.d.ShowAll != nil && g.d.ShowAll()
if widgets.Checkbox(ui, i18n.Tr("editor.playdiff.show_all_differences_including_those", "Show all differences, including those made by the game"), &on) {
if g.d.SetShowAll != nil {
g.d.SetShowAll(on)
}
if on {
g.res = g.full
} else {
g.res = g.hist
}
g.scroll = 0
}
}
func (g *Dialog) close(ui *core.Context) {
g.open = false
g.res, g.hist, g.full = pd.Result{}, pd.Result{}, pd.Result{}
ui.CloseModal()
}
func check(ui *core.Context, id core.ID, r mathx.Rect, state int) bool {
th := &ui.Theme
clicked := ui.Clicked(id, r)
box := mathx.Rect{X: r.X + 3, Y: r.Y + 3, W: r.W - 6, H: r.H - 6}
switch state {
case 1:
ui.Rect(box, th.Accent)
widgets.CheckMark(ui, box, th.Background)
case 2:
ui.Rect(box, ui.WidgetColor(id))
ui.Outline(box, th.Border)
ui.Rect(mathx.Rect{X: box.X + 3, Y: box.Y + box.H/2 - 1, W: box.W - 6, H: 2}, th.Accent)
default:
ui.Rect(box, ui.WidgetColor(id))
ui.Outline(box, th.Border)
}
return clicked
}
type row struct {
header bool
first int
last int
}
func (g *Dialog) rows() []row {
var out []row
ch := g.res.Changes
for i := 0; i < len(ch); {
j := i
for j < len(ch) && ch[j].Node == ch[i].Node {
j++
}
out = append(out, row{header: true, first: i, last: j})
for k := i; k < j; k++ {
out = append(out, row{first: k, last: k + 1})
}
i = j
}
return out
}
func (g *Dialog) state(a, b int) int {
on := 0
for i := a; i < b; i++ {
if g.res.Changes[i].Checked {
on++
}
}
switch on {
case 0:
return 0
case b - a:
return 1
}
return 2
}
func (g *Dialog) set(a, b int, v bool) {
for i := a; i < b; i++ {
g.res.Changes[i].Checked = v
}
}
func (g *Dialog) apply() {
d := g.d.Docs.Documents().Get(g.doc)
if d == nil {
g.d.Notices.Notify(context.LevelError, i18n.Note("editor.playdiff.the_scene_was_closed_the", "The scene was closed, the changes are dropped"))
return
}
n, err := pd.Apply(d, g.res.Changes)
switch {
case err != nil:
g.d.Notices.Notify(context.LevelError, err.Error())
case n > 0:
g.d.Notices.Notify(context.LevelInfo, i18n.Note("editor.playdiff.applied_n_change_s_from", "Applied {n} change(s) from Play, Undo reverts them", i18n.Str("n", strconv.Itoa(n))))
}
}
func (g *Dialog) Draw(ui *core.Context) {
if !g.open {
return
}
th := &ui.Theme
sz := ui.Size()
w := min(760, sz.X-40)
h := min(520, sz.Y-60)
r := mathx.Rect{X: (sz.X - w) / 2, Y: max(20, sz.Y*0.1), W: w, H: h}
if !ui.BeginModal(g.id, r) {
g.open = false
return
}
if ui.KeyPressed(input.KeyEscape) {
ui.EndModal()
g.close(ui)
return
}
title := ui.Cursor().Next(0)
ui.Text(i18n.Tr("editor.playdiff.changes_made_during_play", "Changes made during Play"), mathx.Vec2{X: title.X, Y: ui.TextY(title)}, th.Text)
all := g.state(0, len(g.res.Changes))
ar := mathx.Rect{X: title.X + title.W - title.H, Y: title.Y, W: title.H, H: title.H}
if len(g.res.Changes) > 0 && check(ui, ui.ID("all"), ar, all) {
g.set(0, len(g.res.Changes), all != 1)
}
widgets.LabelDim(ui, i18n.Tr("editor.playdiff.changes_you_made_during_play", "Changes you made during Play. Undo in the scene reverts them after Apply."))
g.showAll(ui)
rows := g.rows()
foot := th.RowHeight*2 + th.Padding*2
if len(g.res.Created)+len(g.res.Removed) > 0 {
foot += th.RowHeight
}
cur := ui.Cursor()
list := mathx.Rect{X: cur.X, Y: cur.Y, W: cur.W, H: max(0, r.Y+r.H-cur.Y-foot)}
rowH := th.RowHeight + 2
widgets.VirtualList(ui, ui.ID("list"), list, len(rows), rowH, &g.scroll, func(i int, rr mathx.Rect) {
g.drawRow(ui, i, rows[i], rr)
})
if len(rows) == 0 {
ui.TextIn(list, i18n.Tr("editor.playdiff.no_fields_differ", "No fields differ from the scene"), th.TextDisabled, core.AlignCenter)
}
ui.Cursor().Y = list.Y + list.H + th.Spacing
if n, m := len(g.res.Created), len(g.res.Removed); n+m > 0 {
widgets.LabelDim(ui, i18n.Tr("editor.playdiff.n_node_s_created_and", "{n} node(s) created and {m} removed during Play are not transferred", i18n.Str("n", strconv.Itoa(n)), i18n.Str("m", strconv.Itoa(m))))
}
btn := ui.Cursor().Next(0)
bw := float32(140)
if widgets.ButtonRect(ui, ui.ID("discard"), mathx.Rect{X: btn.X + btn.W - bw, Y: btn.Y, W: bw, H: btn.H}, i18n.Tr("editor.playdiff.discard", "Discard")) {
ui.EndModal()
g.close(ui)
return
}
n := 0
for i := range g.res.Changes {
if g.res.Changes[i].Checked {
n++
}
}
label := i18n.Tr("editor.playdiff.apply_n_to_scene", "Apply {n} to scene", i18n.Str("n", strconv.Itoa(n)))
if widgets.ButtonRect(ui, ui.ID("apply"), mathx.Rect{X: btn.X + btn.W - 2*bw - 6, Y: btn.Y, W: bw, H: btn.H}, label) && n > 0 {
g.apply()
ui.EndModal()
g.close(ui)
return
}
ui.EndModal()
}
func (g *Dialog) drawRow(ui *core.Context, i int, rw row, rr mathx.Rect) {
th := &ui.Theme
id := core.IDSeq(ui.ID("row"), i)
cb := mathx.Rect{X: rr.X, Y: rr.Y, W: rr.H, H: rr.H}
if rw.header {
ui.Rect(rr, th.CardHeader)
st := g.state(rw.first, rw.last)
if check(ui, id, cb, st) {
g.set(rw.first, rw.last, st != 1)
}
c := &g.res.Changes[rw.first]
x := cb.X + cb.W + 4
if c.Icon != "" {
widgets.IconAt(ui, mathx.Rect{X: x, Y: rr.Y + (rr.H-16)/2, W: 16, H: 16}, c.Icon, th.TextDim)
x += 20
}
ui.PushClip(rr)
ui.Text(c.Name, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.Text)
ui.PopClip()
widgets.Tooltip(ui, id, c.Tip)
return
}
c := &g.res.Changes[rw.first]
cb.X += th.Indent
if check(ui, id, cb, map[bool]int{false: 0, true: 1}[c.Checked]) {
c.Checked = !c.Checked
}
x := cb.X + cb.W + 4
name := c.Owner
if c.Kind == pd.KindField {
name += " · " + c.Label
}
nameW := min(rr.W*0.3, ui.TextSize(name).X+8)
ui.PushClip(mathx.Rect{X: x, Y: rr.Y, W: nameW, H: rr.H})
ui.Text(name, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.TextDim)
ui.PopClip()
x += nameW + 8
text := c.Text(false) + " → " + c.Text(true)
ui.PushClip(mathx.Rect{X: x, Y: rr.Y, W: max(0, rr.X+rr.W-x), H: rr.H})
ui.Text(text, mathx.Vec2{X: x, Y: ui.TextY(rr)}, th.Text)
if c.Gameplay {
ui.Text(i18n.Tr("editor.playdiff.from_gameplay", " · from gameplay"), mathx.Vec2{X: x + ui.TextSize(text).X, Y: ui.TextY(rr)}, th.TextDisabled)
}
ui.PopClip()
widgets.Tooltip(ui, id, c.Text(false)+"\n→ "+c.Text(true))
}