Skip to content

Commit 01cb4a4

Browse files
committed
debugui: fix DrawOnlyWidget rendering in scrolled grid cells
DrawOnlyWidget created a nested widget even when it was called inside a layout callback such as GridCell's. That nested widget got the bounds of a new layout item (the default height) instead of the cell bounds, so it was culled as soon as that item scrolled out of the container body, even though the cell was still visible. Skip creating the nested widget while a layout callback is running and just add the draw command: the current clip rect already limits the drawing to the visible area. Save and restore the layout callback state so that a nested GridCell does not clear it for the enclosing callback. Also replace the deprecated vector.DrawFilledRect with vector.FillRect, and test the clip rect instead of the container body bounds for the widget visibility check, consistent with how the drawing is clipped. Fixes #47
1 parent 5b80bc7 commit 01cb4a4

5 files changed

Lines changed: 64 additions & 2 deletions

File tree

‎context.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type Context struct {
5858
segStack []segmenter.Segmenter
5959
segStackIdx int
6060

61+
// inLayoutCallback is true during a widget layout callback to block nested widgets.
62+
inLayoutCallback bool
63+
6164
err error
6265
}
6366

‎debugui_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/ebitengine/debugui"
12+
"github.com/hajimehoshi/ebiten/v2"
1213
)
1314

1415
func TestMultipleIDPartFromCallersInForLoop(t *testing.T) {
@@ -138,3 +139,37 @@ func TestUnusedContainer(t *testing.T) {
138139
t.Errorf("got: %v, want: %v", got, want)
139140
}
140141
}
142+
143+
func TestDrawOnlyWidgetInGridCellDuringScroll(t *testing.T) {
144+
var d debugui.DebugUI
145+
const cellCount = 5
146+
147+
update := func(scroll int) int {
148+
t.Helper()
149+
if _, err := d.Update(func(ctx *debugui.Context) error {
150+
ctx.Window("Window", image.Rect(0, 0, 300, 300), func(layout debugui.ContainerLayout) {
151+
ctx.Text("test")
152+
ctx.SetGridLayout(nil, []int{150})
153+
ctx.Loop(cellCount, func(i int) {
154+
ctx.GridCell(func(bounds image.Rectangle) {
155+
ctx.DrawOnlyWidget(func(screen *ebiten.Image) {})
156+
})
157+
})
158+
ctx.SetScroll(image.Pt(0, scroll))
159+
})
160+
return nil
161+
}); err != nil {
162+
t.Fatal(err)
163+
}
164+
return d.DrawCommandCount()
165+
}
166+
167+
update(0)
168+
for _, scroll := range []int{0, 60, 100, 200, 300, 450} {
169+
update(scroll)
170+
got := update(scroll)
171+
if got != cellCount {
172+
t.Errorf("scroll=%d: %d draw commands, want %d", scroll, got, cellCount)
173+
}
174+
}
175+
}

‎draw.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (c *Context) draw(screen *ebiten.Image) {
106106
for cmd := range c.commands() {
107107
switch cmd.typ {
108108
case commandRect:
109-
vector.DrawFilledRect(
109+
vector.FillRect(
110110
target,
111111
float32(cmd.rect.rect.Min.X*scale),
112112
float32(cmd.rect.rect.Min.Y*scale),
@@ -205,6 +205,17 @@ func (c *Context) drawIcon(icon icon, rect image.Rectangle, color color.Color) {
205205
// DrawOnlyWidget adds a widget that only draws the given function without user interaction.
206206
func (c *Context) DrawOnlyWidget(f func(screen *ebiten.Image)) {
207207
_ = c.wrapEventHandlerAndError(func() (EventHandler, error) {
208+
// Creating a nested widget here would use the bounds of a new layout item,
209+
// which do not represent the area f draws in. Add the draw command directly
210+
// and let the clip rect limit the drawing instead.
211+
if c.inLayoutCallback {
212+
c.setClip(c.clipRect())
213+
defer c.setClip(unclippedRect)
214+
cmd := c.appendCommand(commandDraw)
215+
cmd.draw.f = f
216+
return nil, nil
217+
}
218+
208219
_, _ = c.widget(widgetID{}, 0, nil, nil, func(bounds image.Rectangle) {
209220
c.setClip(c.clipRect())
210221
defer c.setClip(unclippedRect)

‎export_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ func TextIndexFromX(str string, x int) int {
8989
func TextWidth(str string) int {
9090
return textWidth(str)
9191
}
92+
93+
func (d *DebugUI) DrawCommandCount() int {
94+
n := 0
95+
for cmd := range d.ctx.commands() {
96+
if cmd.typ == commandDraw {
97+
n++
98+
}
99+
}
100+
return n
101+
}

‎widget.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ func (c *Context) widget(id widgetID, opt option, layout func(bounds image.Recta
139139
err = err2
140140
}
141141
}()
142+
wasInLayoutCallback := c.inLayoutCallback
143+
c.inLayoutCallback = true
142144
layout(bounds)
145+
c.inLayoutCallback = wasInLayoutCallback
143146
}
144147

145148
wasFocused := c.handleInputForWidget(id, bounds, opt)
@@ -148,7 +151,7 @@ func (c *Context) widget(id widgetID, opt option, layout func(bounds image.Recta
148151
e = handleInput(bounds, wasFocused)
149152
}
150153
// Handling input is still needed even if the widget is out of bounds, especially for Header.
151-
if !c.currentContainer().layout.BodyBounds.Overlaps(bounds) {
154+
if !c.clipRect().Overlaps(bounds) {
152155
return e, nil
153156
}
154157

0 commit comments

Comments
 (0)