Skip to content

Commit 7059efa

Browse files
committed
debugui: draw DrawOnlyWidget without culling by its layout item
DrawOnlyWidget created a nested widget, and that widget was culled when the bounds of its layout item did not overlap the container body. The callback can draw outside those bounds, so they cannot tell whether the output is visible: in a grid cell the item has the default height at the top of the cell, and the drawing disappeared once that item scrolled out while the cell was still visible. Allocate the layout item as before, so that the layout does not depend on whether the drawing is visible, and add the draw command without the visibility check. The clip rect already limits the output to the visible area. Also replace the deprecated vector.DrawFilledRect with vector.FillRect. Fixes #47
1 parent 85357fb commit 7059efa

3 files changed

Lines changed: 90 additions & 7 deletions

File tree

‎debugui_test.go‎

Lines changed: 66 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,68 @@ 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+
}
176+
177+
func TestDrawOnlyWidgetInPanelInsideGridCell(t *testing.T) {
178+
var d debugui.DebugUI
179+
var offset int
180+
181+
if _, err := d.Update(func(ctx *debugui.Context) error {
182+
ctx.Window("Window", image.Rect(0, 0, 300, 300), func(layout debugui.ContainerLayout) {
183+
ctx.SetGridLayout(nil, []int{300})
184+
ctx.GridCell(func(image.Rectangle) {
185+
ctx.SetGridLayout(nil, []int{250})
186+
ctx.Panel(func(panelLayout debugui.ContainerLayout) {
187+
ctx.SetGridLayout(nil, []int{150})
188+
ctx.DrawOnlyWidget(func(*ebiten.Image) {})
189+
ctx.GridCell(func(bounds image.Rectangle) {
190+
offset = bounds.Min.Y - panelLayout.BodyBounds.Min.Y
191+
})
192+
})
193+
})
194+
})
195+
return nil
196+
}); err != nil {
197+
t.Fatal(err)
198+
}
199+
200+
// A panel has its own layout, so DrawOnlyWidget must allocate its 150px row
201+
// there: the following cell starts after the body padding, that row and one
202+
// spacing. If DrawOnlyWidget allocated nothing, the cell would start at 5.
203+
if want := 5 + 150 + 4; offset != want {
204+
t.Errorf("the cell after DrawOnlyWidget starts at %d, want %d", offset, want)
205+
}
206+
}

‎draw.go‎

Lines changed: 14 additions & 7 deletions
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,12 +205,19 @@ 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-
_, _ = c.widget(widgetID{}, 0, nil, nil, func(bounds image.Rectangle) {
209-
c.setClip(c.clipRect())
210-
defer c.setClip(unclippedRect)
211-
cmd := c.appendCommand(commandDraw)
212-
cmd.draw.f = f
213-
})
208+
// The layout item is allocated as usual, so that the layout does not depend
209+
// on whether the drawing is visible. The item's bounds must not be used to
210+
// cull the drawing though: f can draw outside them, and the clip rect
211+
// already limits the output to the visible area.
212+
if _, err := c.layoutNext(); err != nil {
213+
return nil, err
214+
}
215+
216+
c.setClip(c.clipRect())
217+
defer c.setClip(unclippedRect)
218+
219+
cmd := c.appendCommand(commandDraw)
220+
cmd.draw.f = f
214221
return nil, nil
215222
})
216223
}

‎export_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,13 @@ func TextIndexFromX(str string, x int) int {
140140
func TextWidth(str string) int {
141141
return textWidth(str)
142142
}
143+
144+
func (d *DebugUI) DrawCommandCount() int {
145+
n := 0
146+
for cmd := range d.ctx.commands() {
147+
if cmd.typ == commandDraw {
148+
n++
149+
}
150+
}
151+
return n
152+
}

0 commit comments

Comments
 (0)