blob: f55696b3de4078cf3715aac14f1d04012d88af9d [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package widget
import (
"image"
"image/draw"
"golang.org/x/exp/shiny/screen"
"golang.org/x/exp/shiny/widget/node"
"golang.org/x/image/math/f64"
"golang.org/x/mobile/event/lifecycle"
)
// TODO: scrolling.
// Sheet is a shell widget that provides *image.RGBA pixel buffers (analogous
// to blank sheets of paper) for its descendent widgets to paint on, via their
// PaintBase methods. Such buffers may be cached and their contents re-used for
// multiple paints, which can make scrolling and animation smoother and more
// efficient.
//
// A simple app may have only one Sheet, near the root of its widget tree. A
// more complicated app may have multiple Sheets. For example, consider a text
// editor consisting of a small header bar and a large text widget. Those two
// nodes may be backed by two separate Sheets, since scrolling the latter
// should not scroll the former.
type Sheet struct {
node.ShellEmbed
buf screen.Buffer
tex screen.Texture
}
// NewSheet returns a new Sheet widget.
func NewSheet(inner node.Node) *Sheet {
w := &Sheet{}
w.Wrapper = w
if inner != nil {
w.Insert(inner, nil)
}
return w
}
func (w *Sheet) release() {
if w.buf != nil {
w.buf.Release()
w.buf = nil
}
if w.tex != nil {
w.tex.Release()
w.tex = nil
}
}
func (w *Sheet) Paint(ctx *node.PaintContext, origin image.Point) (retErr error) {
w.Marks.UnmarkNeedsPaint()
c := w.FirstChild
if c == nil {
w.release()
return nil
}
fresh, size := false, w.Rect.Size()
if w.buf != nil && w.buf.Size() != size {
w.release()
}
if w.buf == nil {
w.buf, retErr = ctx.Screen.NewBuffer(size)
if retErr != nil {
w.release()
return retErr
}
w.tex, retErr = ctx.Screen.NewTexture(size)
if retErr != nil {
w.release()
return retErr
}
fresh = true
}
if fresh || c.Marks.NeedsPaintBase() {
c.Wrapper.PaintBase(&node.PaintBaseContext{
Theme: ctx.Theme,
Dst: w.buf.RGBA(),
}, image.Point{})
}
w.tex.Upload(image.Point{}, w.buf, w.buf.Bounds())
src2dst := ctx.Src2Dst
translate(&src2dst,
float64(origin.X+w.Rect.Min.X),
float64(origin.Y+w.Rect.Min.Y),
)
// TODO: should draw.Over be configurable?
ctx.Drawer.Draw(src2dst, w.tex, w.tex.Bounds(), draw.Over, nil)
return c.Wrapper.Paint(ctx, origin.Add(w.Rect.Min))
}
func translate(a *f64.Aff3, tx, ty float64) {
a[2] += a[0]*tx + a[1]*ty
a[5] += a[3]*tx + a[4]*ty
}
func (w *Sheet) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
w.Marks.UnmarkNeedsPaintBase()
// Do not recursively call PaintBase on our children. We create our own
// buffers, and Sheet.Paint will call PaintBase with our PaintBaseContext
// instead of our ancestor's.
return nil
}
func (w *Sheet) OnChildMarked(child node.Node, newMarks node.Marks) {
if newMarks&node.MarkNeedsPaintBase != 0 {
newMarks &^= node.MarkNeedsPaintBase
newMarks |= node.MarkNeedsPaint
}
w.Mark(newMarks)
}
func (w *Sheet) OnLifecycleEvent(e lifecycle.Event) {
if e.Crosses(lifecycle.StageVisible) == lifecycle.CrossOff {
w.release()
}
}