- Add Draw(gtx, r *Renderer) to Element interface - Add Container type that implements Element and holds []Element children - Leaf types (Label, Icon, Button) implement Draw themselves - Single recursive drawElement function in Renderer — no switch/case - Remove StatusBar/BottomBar — replaced by Container - Remove shaper.go — no longer needed with Gio material labels - Simplify main.go — scale via ScaleProvider interface - 5 files changed, shaper.go deleted
149 lines
3.6 KiB
Go
149 lines
3.6 KiB
Go
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()
|
|
}
|