package ui import ( "bytes" "embed" "image" "image/color" _ "image/png" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" "gioui.org/widget/material" ) //go:embed icons/*.png var iconFS embed.FS // ScaleProvider provides access to the current scale factor. type ScaleProvider interface { Scale() float32 } // Renderer consumes a slice of elements and draws them. type Renderer struct { theme Theme shp *text.Shaper scale ScaleProvider icons map[string]image.Image } // New creates a new Renderer. func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer { r := &Renderer{theme: th, shp: shp, scale: scale, icons: make(map[string]image.Image)} r.loadIcons() return r } // loadIcons loads PNG icons from the embedded filesystem. func (r *Renderer) loadIcons() { data, err := iconFS.ReadFile("icons/cut.png") if err != nil { return } img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return } r.icons["cut"] = img } // icon returns a loaded icon image by name, or nil if not found. func (r *Renderer) icon(name string) image.Image { return r.icons[name] } // toPx converts Dp to physical pixels using State's scale. func (r *Renderer) toPx(dp Dp) Px { return ToPx(dp, r.scale.Scale()) } // toDp converts physical pixels to Dp using State's scale. func (r *Renderer) toDp(px Px) Dp { return ToDp(px, r.scale.Scale()) } // Draw iterates elements and draws each in slice order (back-to-front). func (r *Renderer) Draw(gtx layout.Context, elems []Element) { // Clip to window bounds clipRect := clip.Rect{ Min: gtx.Constraints.Min, Max: gtx.Constraints.Max, }.Push(gtx.Ops) for _, e := range elems { if !e.Visible() { continue } r.drawElement(gtx, e) } clipRect.Pop() } // drawElement clips to the element's bounds, draws background if any, // then dispatches to the element's Draw method. func (r *Renderer) drawElement(gtx layout.Context, e Element) { reg := e.Region() // Clip to element bounds clipRect := clip.Rect{ Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))}, Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))}, }.Push(gtx.Ops) // Draw children if this is a container if container, ok := e.(Container); ok { for _, child := range container.children { child.Draw(gtx, r) } } // Dispatch to element's Draw method e.Draw(gtx, r) clipRect.Pop() } // drawBg draws a solid-color background for the given region. func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) { bgClip := clip.Rect{ Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))}, Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))}, }.Push(gtx.Ops) paint.ColorOp{Color: color.NRGBA{R: col.R, G: col.G, B: col.B, A: col.A}}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) bgClip.Pop() } // drawText draws text using Gio's textView approach. func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, col Color) { th := material.NewTheme() th.Shaper = r.shp gtx.Constraints.Min.X = int(r.toPx(reg.X)) gtx.Constraints.Min.Y = int(r.toPx(reg.Y)) material.Label(th, size, str).Layout(gtx) } // drawPng draws a PNG image at the given region and size. func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) { if img == nil { return } xPx := int(r.toPx(reg.X)) yPx := int(r.toPx(reg.Y)) wPx := int(r.toPx(width)) hPx := int(r.toPx(height)) _ = image.Rect(xPx, yPx, xPx+wPx, yPx+hPx) stack := op.Offset(image.Pt(xPx, yPx)).Push(gtx.Ops) paint.NewImageOp(img).Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) stack.Pop() }