Files
aego/ui/dock/float.go
T
sanspie cf68c71f05 Stage 6: coroutines, signals, ScriptRef, workspace CLI, editor tooling
Language: yield/signal/coroutine become keywords, start is contextual.
Coroutine methods (func Name() coroutine) with start and
yield / wait / frames / fixed / await; signals with Emit/Connect and
Connection handles; ScriptRef<T> and node.GetScript<T>() for fields,
plain methods and signals of other scripts. Diagnostics AES2050-AES2059.

Bytecode v3: ScriptRefs, XFields, XMethods and Connects tables resolved by
name in vm.Link, per-script signals, coroutine flag on functions; new
opcodes START/YIELD/EMIT/CONNECT/DISCONNECT/CONNECTED/COSTOP/CORUN/
SELFREF/GETSCRIPT/GETXF/SETXF/CALLR, checked by verify.

Runtime: script/coroutine scheduler (frames/fixed counters, time heap,
await lists; only due coroutines are visited) and script/signal registry
(snapshot per Emit, deferred removal, no allocations in steady state).
Hot reload cancels coroutines of changed scripts, rebinds connections by
name and signature, then runs Reloaded.

Tooling: script/build removed, aego check/test/dump run on the workspace
(test builds include _test.aes); langsvc completion, hover, signatures and
semantic tokens for the new syntax; editor highlighting, snippets with
placeholders, coroutine/signal profiler graphs and stats.

Docs (script.md sections 16-19, editor.md, ARCHITECTURE), test fixtures and
golden files, platformer enemies wired to the player, scriptreload demo,
and run.Config.FrameTime for deterministic headless fixed steps.

Modules (6.5): engine/module registry and ComponentBuilder are the one
registration path for engine subsystems and native code; render2d,
render3d and script/engine are modules over scene/frame, the script
component moved to scene/scriptcomp, and scene/components and
scene/systems are gone. Editor inspector, templates, picking shapes and
extension hooks are driven by descriptors and editor/builtins; sdk gains
Component/EditorHook; depcheck checks the module graph and the
aego_runtime graph.

Also: stop tracking built tool binaries; gl33 activates the scratch unit
before binding for texture updates.

Verification: typed component method adapters (Method0..2, Proc0..2)
with direct field access and no allocations; app.Build as the single
construction path for run and the editor; modules attach per scene
document; depcheck aego_editor pass; diagnostic codes in one place and
every code covered by a test; indexed coroutine heaps with eager
cancel; dead component references and nil intrinsics fault with
AES6004, host faults keep their code; GC roots of struct fields checked
in verify and vm.Link; 255-field limit; AES4010 for script packages
shadowing host modules; architecture registry D111-D147.

Golden: the harness renders on the thread that owns the GL context,
ReadPixels returns rows in memory order (render targets are top-down per
D17), and the first rendering references are committed.
2026-10-12 14:22:08 +03:00

190 lines
4.6 KiB
Go

package dock
import (
"aego/core/mathx"
"aego/platform/input"
"aego/ui/core"
"aego/ui/draw"
"aego/ui/widgets"
)
const (
floatMinW = 220
floatMinH = 140
floatW = 560
floatH = 380
gripSize = 12
floatGrace = 80
)
var menuID = core.IDOf("dock.menu")
type Float struct {
Panel string
Rect mathx.Rect
}
type menuState struct {
panel string
floating bool
}
func (t *Tree) floatIndex(panel string) int {
for i := range t.floats {
if t.floats[i].Panel == panel {
return i
}
}
return -1
}
func (t *Tree) Floats() []Float {
return t.floats
}
func (t *Tree) floatContent(i int) mathx.Rect {
r := t.floats[i].Rect
return mathx.Rect{X: r.X, Y: r.Y + t.tabH, W: r.W, H: max(0, r.H-t.tabH)}
}
func (t *Tree) clampFloat(r mathx.Rect) mathx.Rect {
b := t.bounds
if b.W <= 0 || b.H <= 0 {
return r
}
r.W = mathx.Clamp(r.W, floatMinW, max(floatMinW, b.W))
r.H = mathx.Clamp(r.H, floatMinH, max(floatMinH, b.H))
r.X = mathx.Clamp(r.X, b.X-r.W+floatGrace, b.X+b.W-floatGrace)
r.Y = mathx.Clamp(r.Y, b.Y, max(b.Y, b.Y+b.H-t.tabH))
return r
}
func (t *Tree) floatAt(m mathx.Vec2) mathx.Rect {
return t.clampFloat(mathx.Rect{X: m.X - 60, Y: m.Y - 12, W: floatW, H: floatH})
}
func (t *Tree) Float(panel string, at mathx.Vec2) {
t.Undock(panel)
t.floats = append(t.floats, Float{Panel: panel, Rect: t.floatAt(at)})
t.changed = true
}
func (t *Tree) raise(i int) {
if i < 0 || i >= len(t.floats)-1 {
return
}
f := t.floats[i]
t.floats = append(t.floats[:i], t.floats[i+1:]...)
t.floats = append(t.floats, f)
t.changed = true
}
func (t *Tree) dockBack(panel string) {
t.DockPanel(panel, t.firstTabs(t.root), SideCenter)
}
func (t *Tree) close(panel string) {
if t.OnClose != nil && t.OnClose(panel) {
return
}
t.Undock(panel)
}
func (t *Tree) openMenu(ui *core.Context, panel string, floating bool) {
t.menu = menuState{panel: panel, floating: floating}
ui.CloseAllPopups()
ui.OpenPopup(menuID, ui.Mouse())
}
func (t *Tree) drawMenu(ui *core.Context) {
if !ui.BeginPopup(menuID, 180) {
return
}
p := t.menu.panel
if t.menu.floating {
if widgets.MenuItem(ui, "Dock", "", true, false) {
t.dockBack(p)
}
} else if widgets.MenuItem(ui, "Float", "Shift+drag", true, false) {
t.Float(p, ui.Mouse())
}
if widgets.MenuItem(ui, "Close", "", true, false) {
t.close(p)
}
ui.EndPopup()
}
func (t *Tree) drawFloats(ui *core.Context, fn func(string, mathx.Rect)) {
th := &ui.Theme
raise, closeAt, dockAt := "", "", ""
for i := range t.floats {
f := &t.floats[i]
f.Rect = t.clampFloat(f.Rect)
r := f.Rect
id := core.IDOf("dock.float/" + f.Panel)
ui.BeginOverlay(r, draw.LayerPopup)
ui.Rect(r, th.Panel)
bar := mathx.Rect{X: r.X, Y: r.Y, W: r.W, H: t.tabH}
ui.Rect(bar, th.Header)
cr := mathx.Rect{X: bar.X + bar.W - bar.H, Y: bar.Y, W: bar.H, H: bar.H}
dr := mathx.Rect{X: cr.X - bar.H, Y: bar.Y, W: bar.H, H: bar.H}
title := mathx.Rect{X: bar.X, Y: bar.Y, W: max(0, dr.X-bar.X), H: bar.H}
if ui.Clicked(core.IDSeq(id, 1), cr) {
closeAt = f.Panel
}
if ui.Clicked(core.IDSeq(id, 2), dr) {
dockAt = f.Panel
}
if d, ok := ui.Drag(id, title); ok && (d.X != 0 || d.Y != 0) {
f.Rect.X += d.X
f.Rect.Y += d.Y
t.changed = true
}
if ui.DoubleClicked(id) {
dockAt = f.Panel
}
if ui.RightClicked(id, title) {
t.openMenu(ui, f.Panel, true)
}
ui.Text(t.title(f.Panel), mathx.Vec2{X: bar.X + th.Padding + 4, Y: ui.TextY(bar)}, th.Text)
widgets.IconAt(ui, mathx.Rect{X: dr.X + 5, Y: dr.Y + 5, W: dr.W - 10, H: dr.H - 10}, "view.dock", th.TextDim)
widgets.IconAt(ui, mathx.Rect{X: cr.X + 5, Y: cr.Y + 5, W: cr.W - 10, H: cr.H - 10}, "ui.close", th.TextDim)
content := t.floatContent(i)
if content.W > 0 && content.H > 0 {
ui.PushClip(content)
ui.PushID(f.Panel)
fn(f.Panel, content)
ui.PopID()
ui.PopClip()
}
grip := mathx.Rect{X: r.X + r.W - gripSize, Y: r.Y + r.H - gripSize, W: gripSize, H: gripSize}
gid := core.IDSeq(id, 3)
if d, ok := ui.Drag(gid, grip); ok && (d.X != 0 || d.Y != 0) {
f.Rect.W = max(floatMinW, f.Rect.W+d.X)
f.Rect.H = max(floatMinH, f.Rect.H+d.Y)
t.changed = true
}
gc := th.Border
if ui.Hot() == gid || ui.Active() == gid {
gc = th.Accent
ui.SetCursorShape(core.CursorResizeNWSE)
}
for k := float32(0); k < 3; k++ {
ui.Rect(mathx.Rect{X: grip.X + 3 + k*3, Y: grip.Y + gripSize - 3 - k*3, W: 2, H: 2}, gc)
}
ui.Outline(r, th.Border)
ui.EndOverlay()
if ui.MousePressed(input.MouseLeft) && r.Contains(ui.Mouse()) {
raise = f.Panel
}
}
switch {
case closeAt != "":
t.close(closeAt)
case dockAt != "":
t.dockBack(dockAt)
case raise != "":
t.raise(t.floatIndex(raise))
}
}