shiny/widget: add OnLifecycleEvent.

Change-Id: Ia2789e5a300a3b89188d3c009b40cc3ad9d39cc2
Reviewed-on: https://go-review.googlesource.com/25520
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/widget/node/node.go b/shiny/widget/node/node.go
index 03ebfff..bc19870 100644
--- a/shiny/widget/node/node.go
+++ b/shiny/widget/node/node.go
@@ -48,6 +48,7 @@
 	"golang.org/x/exp/shiny/screen"
 	"golang.org/x/exp/shiny/widget/theme"
 	"golang.org/x/image/math/f64"
+	"golang.org/x/mobile/event/lifecycle"
 	"golang.org/x/mobile/event/mouse"
 )
 
@@ -144,6 +145,10 @@
 	// needing paint.
 	OnChildMarked(child Node, newMarks Marks)
 
+	// OnLifecycleEvent propagates a lifecycle event to a node (and its
+	// children).
+	OnLifecycleEvent(e lifecycle.Event)
+
 	// OnInputEvent handles a key, mouse, touch or gesture event.
 	//
 	// origin is the parent widget's origin with respect to the event origin;
@@ -202,6 +207,8 @@
 
 func (m *LeafEmbed) OnChildMarked(child Node, newMarks Marks) {}
 
+func (m *LeafEmbed) OnLifecycleEvent(e lifecycle.Event) {}
+
 func (m *LeafEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled { return NotHandled }
 
 // ShellEmbed is designed to be embedded in struct types for nodes with at most
@@ -253,6 +260,12 @@
 	m.Mark(newMarks)
 }
 
+func (m *ShellEmbed) OnLifecycleEvent(e lifecycle.Event) {
+	if c := m.FirstChild; c != nil {
+		c.Wrapper.OnLifecycleEvent(e)
+	}
+}
+
 func (m *ShellEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled {
 	if c := m.FirstChild; c != nil {
 		return c.Wrapper.OnInputEvent(e, origin.Add(m.Rect.Min))
@@ -315,6 +328,12 @@
 	m.Mark(newMarks)
 }
 
+func (m *ContainerEmbed) OnLifecycleEvent(e lifecycle.Event) {
+	for c := m.FirstChild; c != nil; c = c.NextSibling {
+		c.Wrapper.OnLifecycleEvent(e)
+	}
+}
+
 func (m *ContainerEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled {
 	origin = origin.Add(m.Rect.Min)
 	var p image.Point
diff --git a/shiny/widget/sheet.go b/shiny/widget/sheet.go
index 5d76ef1..f55696b 100644
--- a/shiny/widget/sheet.go
+++ b/shiny/widget/sheet.go
@@ -11,6 +11,7 @@
 	"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.
@@ -42,35 +43,38 @@
 	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 {
-		if w.buf != nil {
-			w.buf.Release()
-			w.buf = nil
-			w.tex.Release()
-			w.tex = nil
-		}
+		w.release()
 		return nil
 	}
 
 	fresh, size := false, w.Rect.Size()
 	if w.buf != nil && w.buf.Size() != size {
-		w.buf.Release()
-		w.buf = nil
-		w.tex.Release()
-		w.tex = nil
+		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.buf.Release()
-			w.buf = nil
+			w.release()
 			return retErr
 		}
 		fresh = true
@@ -115,3 +119,9 @@
 	}
 	w.Mark(newMarks)
 }
+
+func (w *Sheet) OnLifecycleEvent(e lifecycle.Event) {
+	if e.Crosses(lifecycle.StageVisible) == lifecycle.CrossOff {
+		w.release()
+	}
+}
diff --git a/shiny/widget/widget.go b/shiny/widget/widget.go
index 4c0f3ce..1e35e5e 100644
--- a/shiny/widget/widget.go
+++ b/shiny/widget/widget.go
@@ -109,7 +109,7 @@
 
 		switch e := e.(type) {
 		case lifecycle.Event:
-			// TODO: drop buffers and textures when we're not visible.
+			root.OnLifecycleEvent(e)
 			if e.To == lifecycle.StageDead {
 				return nil
 			}