Files
aego/ui/widgets/popups.go
T
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00

227 lines
5.3 KiB
Go

package widgets
import (
"math/bits"
"strconv"
"aego/core/mathx"
"aego/platform/input"
"aego/ui/core"
"aego/ui/draw"
)
func glyph(c *core.Context, r rune, fallback string) string {
if f := c.Font(); f != nil && f.Has(r) {
return string(r)
}
return fallback
}
func Glyph(c *core.Context, r rune, fallback string) string {
return glyph(c, r, fallback)
}
func dropdownBox(c *core.Context, id core.ID, r mathx.Rect, label string) {
th := &c.Theme
c.Rect(r, c.WidgetColor(id))
c.Outline(r, th.Border)
inner := mathx.Rect{X: r.X, Y: r.Y, W: r.W - r.H, H: r.H}
c.PushClip(inner)
c.TextIn(inner, label, th.Text, core.AlignLeft)
c.PopClip()
Chevron(c, mathx.Rect{X: r.X + r.W - r.H, Y: r.Y, W: r.H, H: r.H}, true, th.TextDim)
}
func togglePopup(c *core.Context, id core.ID, r mathx.Rect) {
if c.Pressed(id, r) {
if c.PopupOpen(id) {
c.ClosePopup(id)
} else {
c.OpenPopup(id, mathx.Vec2{X: r.X, Y: r.Y + r.H})
}
}
}
func Combo(c *core.Context, id core.ID, r mathx.Rect, cur *int, opts []string, mixed bool) bool {
togglePopup(c, id, r)
label := c.Theme.Mixed
if !mixed && *cur >= 0 && *cur < len(opts) {
label = opts[*cur]
}
dropdownBox(c, id, r, label)
changed := false
if c.BeginPopup(id, max(r.W, 140)) {
c.PushRawID(id)
for i, o := range opts {
if Selectable(c, c.IDInt(i), o, !mixed && i == *cur) {
*cur = i
changed = true
c.ClosePopup(id)
}
}
c.PopID()
c.EndPopup()
}
return changed
}
func allBits(n int) uint64 {
if n >= 64 {
return ^uint64(0)
}
return uint64(1)<<uint(n) - 1
}
func maskSummary(mask uint64, names []string, n int, mixed bool, th *core.Theme) string {
switch {
case mixed:
return th.Mixed
case mask == 0:
return "Nothing"
case mask == ^uint64(0) || mask == allBits(n):
return "Everything"
case bits.OnesCount64(mask) == 1:
i := bits.TrailingZeros64(mask)
if i < len(names) {
return names[i]
}
return "Layer " + strconv.Itoa(i)
}
return strconv.Itoa(bits.OnesCount64(mask)) + " layers"
}
func CheckRow(c *core.Context, id core.ID, label string, on bool) bool {
th := &c.Theme
r := c.Cursor().Next(0)
clicked := c.Clicked(id, r)
if c.Hot() == id {
c.Rect(r, th.WidgetHot)
}
box := mathx.Rect{X: r.X + 2, Y: r.Y + 4, W: r.H - 8, H: r.H - 8}
if on {
c.Rect(box, th.Accent)
CheckMark(c, box, th.Background)
} else {
c.Rect(box, th.Widget)
c.Outline(box, th.Border)
}
c.Text(label, mathx.Vec2{X: box.X + box.W + 6, Y: c.TextY(r)}, th.Text)
return clicked
}
func MaskField(c *core.Context, id core.ID, r mathx.Rect, mask *uint64, names []string, mixed bool) bool {
n := len(names)
if n == 0 {
n = 32
}
togglePopup(c, id, r)
dropdownBox(c, id, r, maskSummary(*mask, names, n, mixed, &c.Theme))
changed := false
if c.BeginPopup(id, max(r.W, 180)) {
c.PushRawID(id)
if Selectable(c, c.ID("everything"), "Everything", !mixed && *mask == allBits(n)) {
*mask = allBits(n)
changed = true
}
if Selectable(c, c.ID("nothing"), "Nothing", !mixed && *mask == 0) {
*mask = 0
changed = true
}
Separator(c)
for i := 0; i < n; i++ {
name := "Layer " + strconv.Itoa(i)
if i < len(names) && names[i] != "" {
name = names[i]
}
bit := uint64(1) << uint(i)
if CheckRow(c, c.IDInt(i), name, *mask&bit != 0) {
*mask ^= bit
changed = true
}
}
c.PopID()
c.EndPopup()
}
return changed
}
func OpenContextMenu(c *core.Context, id core.ID) {
c.CloseAllPopups()
c.OpenPopup(id, c.Mouse())
}
func BeginContextMenu(c *core.Context, id core.ID, r mathx.Rect) bool {
if c.RightClicked(id, r) {
OpenContextMenu(c, id)
}
return c.BeginPopup(id, 220)
}
func ContextMenu(c *core.Context, id core.ID) bool {
return c.BeginPopup(id, 220)
}
func EndContextMenu(c *core.Context) {
c.EndPopup()
}
func Tooltip(c *core.Context, id core.ID, text string) {
if text == "" || c.HoverDuration(id) < 0.5 {
return
}
th := &c.Theme
m := c.Mouse()
size := c.TextSize(text)
r := mathx.Rect{X: m.X + 14, Y: m.Y + 18, W: size.X + 2*th.Padding, H: size.Y + th.Padding}
sz := c.Size()
if r.X+r.W > sz.X {
r.X = max(0, sz.X-r.W)
}
if r.Y+r.H > sz.Y {
r.Y = max(0, m.Y-r.H-4)
}
l := c.List()
l.PushLayer(draw.LayerTop)
l.Rect(r, th.PanelAlt)
l.RectOutline(r, th.Border, 1)
l.Text(text, mathx.Vec2{X: r.X + th.Padding, Y: r.Y + th.Padding/2}, th.Text)
l.PopLayer()
}
func Confirm(c *core.Context, id core.ID, title, message string, buttons ...string) int {
th := &c.Theme
sz := c.Size()
w := min(float32(480), sz.X-40)
lines := int(c.TextSize(message).X/(w-4*th.Padding)) + 1
h := th.RowHeight*float32(3+lines) + th.Padding*4
r := mathx.Rect{X: (sz.X - w) / 2, Y: (sz.Y - h) / 3, W: w, H: h}
if !c.BeginModal(id, r) {
return -1
}
c.PushRawID(id)
result := -1
Label(c, title)
LabelWrapped(c, message, true)
row := mathx.Rect{X: r.X + th.Padding, Y: r.Y + r.H - th.RowHeight - th.Padding, W: r.W - 2*th.Padding, H: th.RowHeight}
x := row.X + row.W
for i := len(buttons) - 1; i >= 0; i-- {
bw := c.TextSize(buttons[i]).X + 28
x -= bw
if ButtonRect(c, c.IDInt(i), mathx.Rect{X: x, Y: row.Y, W: bw, H: row.H}, buttons[i]) {
result = i
}
x -= 6
}
if result < 0 && len(buttons) > 0 && c.Focus() == 0 {
if c.KeyPressed(input.KeyEnter) || c.KeyPressed(input.KeyKPEnter) {
result = 0
}
if c.KeyPressed(input.KeyEscape) {
result = len(buttons) - 1
}
}
c.PopID()
c.EndModal()
return result
}