@@ 2,11 2,16 @@
package debug
import (
+ "fmt"
"image"
"image/color"
+ "strconv"
+ "strings"
"sync"
"gioui.org/f32"
+ "gioui.org/font"
+ "gioui.org/font/opentype"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/pointer"
@@ 14,18 19,27 @@ import (
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
+ "gioui.org/text"
"gioui.org/unit"
+ "gioui.org/widget"
+ "golang.org/x/image/font/gofont/gomono"
)
var (
- mapLock sync.RWMutex
- stateMap map[any]*ConstraintEditor
+ mapLock sync.RWMutex
+ stateMap map[any]*ConstraintEditor
+ shaperLock sync.Mutex
+ shaper *text.Shaper
)
func init() {
mapLock.Lock()
defer mapLock.Unlock()
stateMap = make(map[any]*ConstraintEditor)
+ shaperLock.Lock()
+ defer shaperLock.Unlock()
+ coll, _ := opentype.ParseCollection(gomono.TTF)
+ shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(coll))
}
func getTag(tag any) *ConstraintEditor {
@@ 112,6 126,18 @@ func (d *dragBox) Reset() {
type ConstraintEditor struct {
maxBox dragBox
minBox dragBox
+
+ // LineWidth is the width of debug overlay lines like those outlining the constraints
+ // and widget size.
+ LineWidth unit.Dp
+ // MinSize is the side length of the smallest the editor is allowed to go. If the editor
+ // makes the constraints smaller than this, it will reset itself. If the constraints are
+ // already smaller than this, the editor will not display itself.
+ MinSize unit.Dp
+ // TextSize determines the size of the on-screen contextual help text.
+ TextSize unit.Sp
+
+ MinColor, MaxColor, SizeColor, SurfaceColor color.NRGBA
}
func outline(ops *op.Ops, width int, area image.Point) clip.PathSpec {
@@ 141,29 167,146 @@ func (c *ConstraintEditor) Wrap(w layout.Widget) layout.Widget {
}
}
+// rgb converts a string of the form "#abcdef" or "#abcdef01" into an NRGBA color.
+// If the hex does not provide alpha, max alpha is assumed.
+func rgb(s string) color.NRGBA {
+ s = strings.TrimPrefix(s, "#")
+ if len(s)%2 != 0 || len(s) > 8 {
+ panic(fmt.Errorf("invalid color #%s", s))
+ }
+ r, err := strconv.ParseUint(s[:2], 16, 8)
+ if err != nil {
+ panic(err)
+ }
+ g, err := strconv.ParseUint(s[2:4], 16, 8)
+ if err != nil {
+ panic(err)
+ }
+ b, err := strconv.ParseUint(s[4:6], 16, 8)
+ if err != nil {
+ panic(err)
+ }
+ a := uint64(255)
+ if len(s) > 6 {
+ a, err = strconv.ParseUint(s[6:8], 16, 8)
+ if err != nil {
+ panic(err)
+ }
+ }
+ return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
+}
+
+func (c *ConstraintEditor) init() {
+ if c.LineWidth == 0 {
+ c.LineWidth = 1
+ }
+ if c.MinSize == 0 {
+ c.MinSize = 5
+ }
+ if c.TextSize == 0 {
+ c.TextSize = 12
+ }
+ if c.MinColor == (color.NRGBA{}) {
+ c.MinColor = rgb("#c077f9")
+ }
+ if c.MaxColor == (color.NRGBA{}) {
+ c.MaxColor = rgb("#f95f98")
+ }
+ if c.SizeColor == (color.NRGBA{}) {
+ c.SizeColor = rgb("#1fbd51")
+ }
+ if c.SurfaceColor == (color.NRGBA{}) {
+ // TODO: find better color for this.
+ c.SurfaceColor = color.NRGBA{R: 1, G: 1, B: 1, A: 0}
+ }
+}
+
+func record(gtx layout.Context, w layout.Widget) (op.CallOp, layout.Dimensions) {
+ macro := op.Record(gtx.Ops)
+ dims := w(gtx)
+ return macro.Stop(), dims
+}
+
+func colorMaterial(ops *op.Ops, col color.NRGBA) op.CallOp {
+ macro := op.Record(ops)
+ paint.ColorOp{Color: col}.Add(ops)
+ return macro.Stop()
+}
+
+func labelOp(gtx layout.Context, sz unit.Sp, col color.NRGBA, str string) (op.CallOp, layout.Dimensions) {
+ gtx.Constraints.Min = image.Point{}
+ return record(gtx, func(gtx layout.Context) layout.Dimensions {
+ shaperLock.Lock()
+ defer shaperLock.Unlock()
+ return widget.Label{
+ MaxLines: 1,
+ }.Layout(gtx, shaper, font.Font{}, sz, str, colorMaterial(gtx.Ops, col))
+ })
+}
+
+func recorded(call op.CallOp, dims layout.Dimensions) layout.Widget {
+ return func(gtx layout.Context) layout.Dimensions {
+ call.Add(gtx.Ops)
+ return dims
+ }
+}
+
// Layout the constraint editor to debug the layout of w.
func (c *ConstraintEditor) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
+ c.init()
originalConstraints := gtx.Constraints
gtx.Constraints = gtx.Constraints.SubMax(c.maxBox.CurrentDrag())
gtx.Constraints = gtx.Constraints.AddMin(image.Point{}.Sub(c.minBox.CurrentDrag()))
- if gtx.Constraints.Max.X < gtx.Dp(5) || gtx.Constraints.Max.Y < gtx.Dp(5) {
+ if minSize := gtx.Dp(c.MinSize); gtx.Constraints.Max.X < minSize || gtx.Constraints.Max.Y < minSize {
gtx.Constraints = originalConstraints
c.maxBox.Reset()
c.minBox.Reset()
}
dims := w(gtx)
- oneDp := gtx.Dp(1)
- minSpec := outline(gtx.Ops, oneDp, gtx.Constraints.Min)
- maxSpec := outline(gtx.Ops, oneDp, gtx.Constraints.Max)
- sizeSpec := outline(gtx.Ops, oneDp, dims.Size)
- rec := op.Record(gtx.Ops)
+ lineWidth := gtx.Dp(c.LineWidth)
+ minSpec := outline(gtx.Ops, lineWidth, gtx.Constraints.Min)
+ maxSpec := outline(gtx.Ops, lineWidth, gtx.Constraints.Max)
+ sizeSpec := outline(gtx.Ops, lineWidth, dims.Size)
// Display the static widget size.
- paint.FillShape(gtx.Ops, color.NRGBA{G: 255, A: 150}, clip.Outline{Path: sizeSpec}.Op())
- paint.FillShape(gtx.Ops, color.NRGBA{G: 255, A: 50}, clip.Rect{Max: dims.Size}.Op())
+ paint.FillShape(gtx.Ops, c.SizeColor, clip.Outline{Path: sizeSpec}.Op())
+ sizeFill := c.SizeColor
+ sizeFill.A = 50
+ paint.FillShape(gtx.Ops, sizeFill, clip.Rect{Max: dims.Size}.Op())
+
+ // Display textual overlays.
+ minText := fmt.Sprintf("(%d,%d) Min", gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
+ minOp, minDims := labelOp(gtx, c.TextSize, c.MinColor, minText)
+ maxText := fmt.Sprintf("(%d,%d) Max", gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
+ maxOp, maxDims := labelOp(gtx, c.TextSize, c.MaxColor, maxText)
+ szText := fmt.Sprintf("(%d,%d) Size", dims.Size.X, dims.Size.Y)
+ szOp, szDims := labelOp(gtx, c.TextSize, c.SizeColor, szText)
+ rec := op.Record(gtx.Ops)
+
+ flexAxis := layout.Vertical
+ if minDims.Size.Y+maxDims.Size.Y+szDims.Size.Y > gtx.Constraints.Max.Y {
+ flexAxis = layout.Horizontal
+ }
+ layout.Stack{}.Layout(gtx,
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ paint.FillShape(gtx.Ops, c.SurfaceColor, clip.Rect{Max: gtx.Constraints.Min}.Op())
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return layout.Flex{
+ Axis: flexAxis,
+ }.Layout(gtx,
+ layout.Rigid(recorded(minOp, minDims)),
+ layout.Rigid(recorded(maxOp, maxDims)),
+ layout.Rigid(recorded(szOp, szDims)),
+ )
+ }),
+ )
// Display the interactive max constraint controls.
- paint.FillShape(gtx.Ops, color.NRGBA{R: 255, A: 150}, clip.Outline{Path: maxSpec}.Op())
+ paint.FillShape(gtx.Ops, c.MaxColor, clip.Outline{Path: maxSpec}.Op())
if c.maxBox.Active(gtx.Queue) {
- paint.FillShape(gtx.Ops, color.NRGBA{R: 255, A: 50}, clip.Rect{Max: gtx.Constraints.Max}.Op())
+ maxFill := c.MaxColor
+ maxFill.A = 50
+ paint.FillShape(gtx.Ops, maxFill, clip.Rect{Max: gtx.Constraints.Max}.Op())
}
maxDragArea := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
@@ 171,9 314,11 @@ func (c *ConstraintEditor) Layout(gtx layout.Context, w layout.Widget) layout.Di
maxDragArea.Pop()
// Display the interactive min constraint controls.
- paint.FillShape(gtx.Ops, color.NRGBA{B: 255, A: 150}, clip.Outline{Path: minSpec}.Op())
+ paint.FillShape(gtx.Ops, c.MinColor, clip.Outline{Path: minSpec}.Op())
if c.minBox.Active(gtx.Queue) {
- paint.FillShape(gtx.Ops, color.NRGBA{B: 255, A: 50}, clip.Rect{Max: gtx.Constraints.Min}.Op())
+ minFill := c.MinColor
+ minFill.A = 50
+ paint.FillShape(gtx.Ops, minFill, clip.Rect{Max: gtx.Constraints.Min}.Op())
}
minDragArea := clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops)