Skip to content

Commit 631b155

Browse files
committed
debugui: add (*Context).IDScope
1 parent daeb8ea commit 631b155

7 files changed

Lines changed: 126 additions & 68 deletions

File tree

container.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,14 @@ func (c *Context) Window(title string, rect image.Rectangle, f func(layout Conta
7777
}
7878

7979
func (c *Context) window(title string, bounds image.Rectangle, opt option, f func(layout ContainerLayout)) (err error) {
80-
id := c.idFromString(title, emptyControlID)
80+
id := c.idFromString(title)
81+
c.idScopeFromControlID(id, func() {
82+
err = c.doWindow(title, bounds, opt, id, f)
83+
})
84+
return
85+
}
8186

87+
func (c *Context) doWindow(title string, bounds image.Rectangle, opt option, id controlID, f func(layout ContainerLayout)) (err error) {
8288
cnt := c.container(id, opt)
8389
if cnt == nil || !cnt.open {
8490
return nil
@@ -130,7 +136,7 @@ func (c *Context) window(title string, bounds image.Rectangle, opt option, f fun
130136

131137
// do title text
132138
if (^opt & optionNoTitle) != 0 {
133-
id := c.idFromString("title", id)
139+
id := c.idFromString("title")
134140
r := image.Rect(tr.Min.X+tr.Dy()-c.style().padding, tr.Min.Y, tr.Max.X, tr.Max.Y)
135141
c.updateControl(id, r, opt)
136142
c.drawControlText(title, r, colorTitleText, opt)
@@ -142,7 +148,7 @@ func (c *Context) window(title string, bounds image.Rectangle, opt option, f fun
142148

143149
// do `collapse` button
144150
if (^opt & optionNoClose) != 0 {
145-
id := c.idFromString("collapse", id)
151+
id := c.idFromString("collapse")
146152
r := image.Rect(tr.Min.X, tr.Min.Y, tr.Min.X+tr.Dy(), tr.Max.Y)
147153
icon := iconExpanded
148154
if collapsed {
@@ -160,7 +166,7 @@ func (c *Context) window(title string, bounds image.Rectangle, opt option, f fun
160166
return nil
161167
}
162168

163-
if err := c.pushContainerBodyLayout(cnt, body, opt, id); err != nil {
169+
if err := c.pushContainerBodyLayout(cnt, body, opt); err != nil {
164170
return err
165171
}
166172
defer func() {
@@ -172,7 +178,7 @@ func (c *Context) window(title string, bounds image.Rectangle, opt option, f fun
172178
// do `resize` handle
173179
if (^opt & optionNoResize) != 0 {
174180
sz := c.style().titleHeight
175-
id := c.idFromString("resize", id)
181+
id := c.idFromString("resize")
176182
r := image.Rect(bounds.Max.X-sz, bounds.Max.Y-sz, bounds.Max.X, bounds.Max.Y)
177183
c.updateControl(id, r, opt)
178184
if id == c.focus && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
@@ -207,7 +213,7 @@ func (c *Context) window(title string, bounds image.Rectangle, opt option, f fun
207213

208214
func (c *Context) OpenPopup(name string) {
209215
c.wrapError(func() error {
210-
id := c.idFromString(name, emptyControlID)
216+
id := c.idFromString(name)
211217
cnt := c.container(id, 0)
212218
// set as hover root so popup isn't closed in begin_window_ex()
213219
c.nextHoverRoot = cnt
@@ -226,7 +232,7 @@ func (c *Context) OpenPopup(name string) {
226232

227233
func (c *Context) ClosePopup(name string) {
228234
c.wrapError(func() error {
229-
id := c.idFromString(name, emptyControlID)
235+
id := c.idFromString(name)
230236
cnt := c.container(id, 0)
231237
cnt.open = false
232238
return nil

context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Context struct {
2020
numberEditBuf string
2121
numberEdit controlID
2222

23+
idStack []controlID
2324
commandList []*command
2425
rootList []*container
2526
containerStack []*container

control.go

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"image"
99
"math"
10-
"strings"
1110
"unsafe"
1211

1312
"github.com/hajimehoshi/ebiten/v2"
@@ -18,8 +17,6 @@ type controlID string
1817

1918
const emptyControlID controlID = ""
2019

21-
const idSeparator = "\x00"
22-
2320
type option int
2421

2522
const (
@@ -107,11 +104,11 @@ func (c *Context) updateControl(id controlID, bounds image.Rectangle, opt option
107104
return
108105
}
109106

110-
func (c *Context) Control(idStr string, f func(bounds image.Rectangle) bool) bool {
107+
func (c *Context) Control(f func(bounds image.Rectangle) bool) bool {
111108
pc := caller()
112109
var res bool
113110
c.wrapError(func() error {
114-
id := c.idFromCaller(pc, idStr)
111+
id := c.idFromCaller(pc)
115112
var err error
116113
res, err = c.control(id, 0, func(bounds image.Rectangle, wasFocused bool) (bool, error) {
117114
return f(bounds), nil
@@ -179,8 +176,7 @@ func (c *Context) Text(text string) {
179176
}
180177

181178
func (c *Context) button(label string, opt option, callerPC uintptr) (controlID, bool, error) {
182-
label, idStr, _ := strings.Cut(label, idSeparator)
183-
id := c.idFromCaller(callerPC, idStr)
179+
id := c.idFromCaller(callerPC)
184180
res, err := c.control(id, opt, func(bounds image.Rectangle, wasFocused bool) (bool, error) {
185181
var res bool
186182
// handle click
@@ -330,8 +326,7 @@ func (c *Context) sliderF(value *float64, low, high, step float64, digits int, o
330326
}
331327

332328
func (c *Context) header(label string, isTreeNode bool, opt option, callerPC uintptr, f func() error) error {
333-
label, idStr, _ := strings.Cut(label, idSeparator)
334-
id := c.idFromCaller(callerPC, idStr)
329+
id := c.idFromCaller(callerPC)
335330
c.SetGridLayout(nil, nil)
336331

337332
var expanded bool
@@ -404,7 +399,7 @@ func (c *Context) treeNode(label string, opt option, callerPC uintptr, f func())
404399
}
405400

406401
// x = x, y = y, w = w, h = h
407-
func (c *Context) scrollbarVertical(cnt *container, b image.Rectangle, cs image.Point, containerID controlID) {
402+
func (c *Context) scrollbarVertical(cnt *container, b image.Rectangle, cs image.Point) {
408403
maxscroll := cs.Y - b.Dy()
409404
if maxscroll > 0 && b.Dy() > 0 {
410405
// get sizing / positioning
@@ -413,7 +408,7 @@ func (c *Context) scrollbarVertical(cnt *container, b image.Rectangle, cs image.
413408
base.Max.X = base.Min.X + c.style().scrollbarSize
414409

415410
// handle input
416-
id := c.idFromString("scrollbar-y", containerID)
411+
id := c.idFromString("scrollbar-y")
417412
c.updateControl(id, base, 0)
418413
if c.focus == id && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
419414
cnt.layout.ScrollOffset.Y += c.mouseDelta().Y * cs.Y / base.Dy()
@@ -439,7 +434,7 @@ func (c *Context) scrollbarVertical(cnt *container, b image.Rectangle, cs image.
439434
}
440435

441436
// x = y, y = x, w = h, h = w
442-
func (c *Context) scrollbarHorizontal(cnt *container, b image.Rectangle, cs image.Point, containerID controlID) {
437+
func (c *Context) scrollbarHorizontal(cnt *container, b image.Rectangle, cs image.Point) {
443438
maxscroll := cs.X - b.Dx()
444439
if maxscroll > 0 && b.Dx() > 0 {
445440
// get sizing / positioning
@@ -448,7 +443,7 @@ func (c *Context) scrollbarHorizontal(cnt *container, b image.Rectangle, cs imag
448443
base.Max.Y = base.Min.Y + c.style().scrollbarSize
449444

450445
// handle input
451-
id := c.idFromString("scrollbar-x", containerID)
446+
id := c.idFromString("scrollbar-x")
452447
c.updateControl(id, base, 0)
453448
if c.focus == id && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
454449
cnt.layout.ScrollOffset.X += c.mouseDelta().X * cs.X / base.Dx()
@@ -474,15 +469,15 @@ func (c *Context) scrollbarHorizontal(cnt *container, b image.Rectangle, cs imag
474469
}
475470

476471
// if `swap` is true, X = Y, Y = X, W = H, H = W
477-
func (c *Context) scrollbar(cnt *container, b image.Rectangle, cs image.Point, swap bool, containerID controlID) {
472+
func (c *Context) scrollbar(cnt *container, b image.Rectangle, cs image.Point, swap bool) {
478473
if swap {
479-
c.scrollbarHorizontal(cnt, b, cs, containerID)
474+
c.scrollbarHorizontal(cnt, b, cs)
480475
} else {
481-
c.scrollbarVertical(cnt, b, cs, containerID)
476+
c.scrollbarVertical(cnt, b, cs)
482477
}
483478
}
484479

485-
func (c *Context) scrollbars(cnt *container, body image.Rectangle, containerID controlID) image.Rectangle {
480+
func (c *Context) scrollbars(cnt *container, body image.Rectangle) image.Rectangle {
486481
sz := c.style().scrollbarSize
487482
cs := cnt.layout.ContentSize
488483
cs.X += c.style().padding * 2
@@ -497,15 +492,15 @@ func (c *Context) scrollbars(cnt *container, body image.Rectangle, containerID c
497492
}
498493
// to create a horizontal or vertical scrollbar almost-identical code is
499494
// used; only the references to `x|y` `w|h` need to be switched
500-
c.scrollbar(cnt, body, cs, false, containerID)
501-
c.scrollbar(cnt, body, cs, true, containerID)
495+
c.scrollbar(cnt, body, cs, false)
496+
c.scrollbar(cnt, body, cs, true)
502497
c.popClipRect()
503498
return body
504499
}
505500

506-
func (c *Context) pushContainerBodyLayout(cnt *container, body image.Rectangle, opt option, containerID controlID) error {
501+
func (c *Context) pushContainerBodyLayout(cnt *container, body image.Rectangle, opt option) error {
507502
if (^opt & optionNoScroll) != 0 {
508-
body = c.scrollbars(cnt, body, containerID)
503+
body = c.scrollbars(cnt, body)
509504
}
510505
if err := c.pushLayout(body.Inset(c.style().padding), cnt.layout.ScrollOffset, opt&optionAutoSize != 0); err != nil {
511506
return err

example/ui.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func (g *Game) testWindow(ctx *debugui.Context) {
120120
ctx.Text("Blue:")
121121
ctx.Slider(&g.bg[2], 0, 255, 1)
122122
})
123-
ctx.Control("", func(bounds image.Rectangle) bool {
123+
ctx.Control(func(bounds image.Rectangle) bool {
124124
ctx.DrawControl(func(screen *ebiten.Image) {
125125
scale := ctx.Scale()
126126
vector.DrawFilledRect(
@@ -193,9 +193,11 @@ func (g *Game) buttonWindows(ctx *debugui.Context) {
193193
ctx.Window("Button Windows", image.Rect(350, 300, 650, 500), func(layout debugui.ContainerLayout) {
194194
ctx.SetGridLayout([]int{-1, -1, -1, -1}, nil)
195195
for i := 0; i < 100; i++ {
196-
if ctx.Button("Button\x00" + fmt.Sprintf("%d", i)) {
197-
g.writeLog(fmt.Sprintf("Pressed button %d in Button Window", i))
198-
}
196+
ctx.IDScope(fmt.Sprintf("%d", i), func() {
197+
if ctx.Button("Button") {
198+
g.writeLog(fmt.Sprintf("Pressed button %d in Button Window", i))
199+
}
200+
})
199201
}
200202
})
201203
}

helpers.go

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ package debugui
55

66
import (
77
"errors"
8-
"fmt"
9-
"runtime"
108
"slices"
119
"sort"
12-
"unsafe"
1310

1411
"github.com/hajimehoshi/ebiten/v2"
1512
"github.com/hajimehoshi/ebiten/v2/inpututil"
@@ -19,34 +16,6 @@ func clamp[T int | float64](x, a, b T) T {
1916
return min(b, max(a, x))
2017
}
2118

22-
// caller returns a program counter of the caller.
23-
func caller() uintptr {
24-
pc, _, _, ok := runtime.Caller(2)
25-
if !ok {
26-
return 0
27-
}
28-
return pc
29-
}
30-
31-
func (c *Context) idFromPointer(pointer unsafe.Pointer) controlID {
32-
return controlID(fmt.Sprintf("pointer:%p", pointer))
33-
}
34-
35-
func (c *Context) idFromString(str string, parentID controlID) controlID {
36-
if parentID != emptyControlID {
37-
return controlID(fmt.Sprintf("string:%q:%s", str, parentID))
38-
}
39-
return controlID(fmt.Sprintf("string:%q", str))
40-
}
41-
42-
// idFromCaller returns a hash value based on the caller's file and line number.
43-
func (c *Context) idFromCaller(callerPC uintptr, str string) controlID {
44-
if len(str) > 0 {
45-
return controlID(fmt.Sprintf("caller:%d:%q", callerPC, str))
46-
}
47-
return controlID(fmt.Sprintf("caller:%d", callerPC))
48-
}
49-
5019
func (c *Context) bringToFront(cnt *container) {
5120
c.lastZIndex++
5221
cnt.zIndex = c.lastZIndex
@@ -92,14 +61,17 @@ func (c *Context) begin() {
9261

9362
func (c *Context) end() error {
9463
// check stacks
64+
if len(c.idStack) > 0 {
65+
return errors.New("debugui: id stack must be empty")
66+
}
9567
if len(c.containerStack) > 0 {
96-
return errors.New("debugui: container stack not empty")
68+
return errors.New("debugui: container stack must be empty")
9769
}
9870
if len(c.clipStack) > 0 {
99-
return errors.New("debugui: clip stack not empty")
71+
return errors.New("debugui: clip stack must be empty")
10072
}
10173
if len(c.layoutStack) > 0 {
102-
return errors.New("debugui: layout stack not empty")
74+
return errors.New("debugui: layout stack must be empty")
10375
}
10476

10577
// handle scroll input

id.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+
package debugui
5+
6+
import (
7+
"fmt"
8+
"runtime"
9+
"slices"
10+
"unsafe"
11+
)
12+
13+
// caller returns a program counter of the caller.
14+
func caller() uintptr {
15+
pc, _, _, ok := runtime.Caller(2)
16+
if !ok {
17+
return 0
18+
}
19+
return pc
20+
}
21+
22+
// IDScope creates a new scope for control IDs.
23+
// IDScope creates a unique scope based on the caller's position and the given name string.
24+
//
25+
// IDScope is useful when you want to create multiple controls at the same position e.g. in a for loop.
26+
func (c *Context) IDScope(name string, f func()) {
27+
pc := caller()
28+
c.idStack = append(c.idStack, c.idFromCaller(pc))
29+
c.idStack = append(c.idStack, c.idFromString(name))
30+
defer func() {
31+
c.idStack = slices.Delete(c.idStack, len(c.idStack)-2, len(c.idStack))
32+
}()
33+
f()
34+
}
35+
36+
func (c *Context) idScopeFromControlID(controlID controlID, f func()) {
37+
c.idStack = append(c.idStack, controlID)
38+
defer func() {
39+
c.idStack = slices.Delete(c.idStack, len(c.idStack)-1, len(c.idStack))
40+
}()
41+
f()
42+
}
43+
44+
func (c *Context) idFromPointer(pointer unsafe.Pointer) controlID {
45+
return controlID(fmt.Sprintf("pointer:%p", pointer))
46+
}
47+
48+
func (c *Context) idScopeToControlID() controlID {
49+
var newID controlID
50+
for _, id := range c.idStack {
51+
if len(newID) > 0 {
52+
newID += ":"
53+
}
54+
newID += id
55+
}
56+
return newID
57+
}
58+
59+
func (c *Context) idFromString(str string) controlID {
60+
newID := c.idScopeToControlID()
61+
if len(newID) > 0 {
62+
newID += ":"
63+
}
64+
newID += controlID(fmt.Sprintf("string:%q", str))
65+
return newID
66+
}
67+
68+
// idFromCaller returns a hash value based on the caller's file and line number.
69+
func (c *Context) idFromCaller(callerPC uintptr) controlID {
70+
newID := c.idScopeToControlID()
71+
if len(newID) > 0 {
72+
newID += ":"
73+
}
74+
newID += controlID(fmt.Sprintf("caller:%d", callerPC))
75+
return newID
76+
}

panel.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ func (c *Context) Panel(name string, f func(layout ContainerLayout)) {
1313
}
1414

1515
func (c *Context) panel(name string, opt option, f func(layout ContainerLayout)) (err error) {
16-
id := c.idFromString(name, emptyControlID)
16+
id := c.idFromString(name)
17+
c.idScopeFromControlID(id, func() {
18+
err = c.doPanel(opt, id, f)
19+
})
20+
return
21+
}
1722

23+
func (c *Context) doPanel(opt option, id controlID, f func(layout ContainerLayout)) (err error) {
1824
cnt := c.container(id, opt)
1925
l, err := c.layoutNext()
2026
if err != nil {
@@ -28,7 +34,7 @@ func (c *Context) panel(name string, opt option, f func(layout ContainerLayout))
2834
c.pushContainer(cnt)
2935
defer c.popContainer()
3036

31-
if err := c.pushContainerBodyLayout(cnt, cnt.layout.Bounds, opt, id); err != nil {
37+
if err := c.pushContainerBodyLayout(cnt, cnt.layout.Bounds, opt); err != nil {
3238
return err
3339
}
3440
defer func() {

0 commit comments

Comments
 (0)