Skip to content

Commit

Permalink
Add Mount/Unmount callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchellh committed Sep 14, 2020
1 parent a678c1e commit 7170628
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 23 deletions.
25 changes: 25 additions & 0 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,31 @@ type ComponentFinalizer interface {
Finalize()
}

// ComponentMounter allows components to be notified when they are
// mounted and unmounted. A mounted component is one that is added to
// a render tree for the first time. A component is unmounted when it is
// removed from the render tree.
//
// The callbacks here may be called multiple times under certain scenarios:
// (1) a component is used in multiple Document instances, (2) a component
// is unmounted and then remounted in the future.
//
// A component mounted multiple times in the same render tree does NOT
// have the mount callbacks called multiple times.
//
// A good use case for this interface is setting up and cleaning up resources.
type ComponentMounter interface {
Component

// Mount is called when the component is added to a render tree.
Mount()

// Unmount is called when the component is removed from a render tree.
// This will be called under ANY scenario where the component is
// removed from the render tree, including finalization.
Unmount()
}

// componentLayout can be implemented to set custom layout settings
// for the component. This can only be implemented by internal components
// since we use an internal library.
Expand Down
89 changes: 73 additions & 16 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Document struct {
els []Component
refreshRate time.Duration
prevRoot *flex.Node
mounted map[ComponentMounter]struct{}
}

// New returns a Document that will output to stdout.
Expand Down Expand Up @@ -60,6 +61,14 @@ func (d *Document) Append(el ...Component) {
d.els = append(d.els, el...)
}

// Set sets the components for the document. This will replace all
// previous components.
func (d *Document) Set(els ...Component) {
d.mu.Lock()
defer d.mu.Unlock()
d.els = els
}

// Render starts a render loop that continues to render until the
// context is cancelled. This will render at the configured refresh rate.
// If the refresh rate is changed, it will not affect an active render loop.
Expand All @@ -69,7 +78,7 @@ func (d *Document) Render(ctx context.Context) {
dur := d.refreshRate
d.mu.Unlock()
if dur == 0 {
dur = time.Second / 12
dur = time.Second / 24
}

t := time.NewTicker(dur)
Expand Down Expand Up @@ -111,7 +120,7 @@ func (d *Document) RenderFrame() {
flex.CalculateLayout(root, flex.Undefined, flex.Undefined, flex.DirectionLTR)

// Fix any text nodes that need to be fixed.
d.resizeTextNodes(root)
d.handleNodes(root, nil)

// Render the tree
d.r.RenderRoot(root, d.prevRoot)
Expand All @@ -132,7 +141,7 @@ func (d *Document) RenderFrame() {
// component doesn't match our expectations it means we hit
// something weird and we exit too.
ctx, ok := child.Context.(*parentContext)
if !ok || ctx == nil || ctx.Component != el || !ctx.Finalized {
if !ok || ctx == nil || ctx.C != el || !ctx.Finalized {
break
}

Expand All @@ -156,26 +165,74 @@ func (d *Document) RenderFrame() {
d.prevRoot = root
}

func (d *Document) resizeTextNodes(parent *flex.Node) {
func (d *Document) handleNodes(
parent *flex.Node,
seen map[ComponentMounter]struct{},
) {
// For our first call, we detect the root since we use it later
// to do some final calls.
root := seen == nil
if root {
seen = map[ComponentMounter]struct{}{}
}

for _, child := range parent.Children {
// Get our node context. If we don't have one then we're a container
// and we render below.
ctx, ok := child.Context.(*TextNodeContext)
if !ok {
d.resizeTextNodes(child)
if ctx, ok := child.Context.(treeContext); ok {
c := ctx.Component()

// Mount callbacks
if mc, ok := c.(ComponentMounter); ok {
// Only if we haven't seen this already...
if _, ok := seen[mc]; !ok {
seen[mc] = struct{}{}

if d.mounted == nil {
d.mounted = map[ComponentMounter]struct{}{}
}

// And we haven't notified this already...
if _, ok := d.mounted[mc]; !ok {
d.mounted[mc] = struct{}{}

// Notify
mc.Mount()
}
}
}

continue
}

// If the height/width that the layout engine calculated is less than
// the height that we originally measured, then we need to give the
// element a chance to rerender into that dimension.
height := child.LayoutGetHeight()
width := child.LayoutGetWidth()
if height < ctx.Size.Height || width < ctx.Size.Width {
child.Measure(child,
width, flex.MeasureModeAtMost,
height, flex.MeasureModeAtMost,
)
if ctx, ok := child.Context.(*TextNodeContext); ok {
height := child.LayoutGetHeight()
width := child.LayoutGetWidth()
if height < ctx.Size.Height || width < ctx.Size.Width {
child.Measure(child,
width, flex.MeasureModeAtMost,
height, flex.MeasureModeAtMost,
)
}
}

d.handleNodes(child, seen)
}

// If we're the root call, then we preform some final calls. Otherwise
// we just return, we're done.
if !root {
return
}

// Go through our previously mounted set and if we didn't see it,
// then call unmount on it. After we're done, what we saw is our new
// map of mounted elements.
for mc := range d.mounted {
if _, ok := seen[mc]; !ok {
mc.Unmount()
}
}
d.mounted = seen
}
59 changes: 59 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package glint

import (
"sync/atomic"
"testing"

"github.com/stretchr/testify/require"
)

func TestDocument_mountUnmount(t *testing.T) {
require := require.New(t)

// Create our doc
r := &StringRenderer{}
d := New()
d.SetRenderer(r)

// Add our component
var c testMount
d.Append(&c)
require.Equal(uint32(0), atomic.LoadUint32(&c.mount))
require.Equal(uint32(0), atomic.LoadUint32(&c.unmount))

// Render once
d.RenderFrame()
require.Equal(uint32(1), atomic.LoadUint32(&c.mount))
require.Equal(uint32(0), atomic.LoadUint32(&c.unmount))

// Render again
d.RenderFrame()
require.Equal(uint32(1), atomic.LoadUint32(&c.mount))
require.Equal(uint32(0), atomic.LoadUint32(&c.unmount))

// Remove the old components
d.Set()
d.RenderFrame()
require.Equal(uint32(1), atomic.LoadUint32(&c.mount))
require.Equal(uint32(1), atomic.LoadUint32(&c.unmount))

// Render again
d.RenderFrame()
require.Equal(uint32(1), atomic.LoadUint32(&c.mount))
require.Equal(uint32(1), atomic.LoadUint32(&c.unmount))
}

type testMount struct {
terminalComponent

mount uint32
unmount uint32
}

func (c *testMount) Mount() { atomic.AddUint32(&c.mount, 1) }
func (c *testMount) Unmount() { atomic.AddUint32(&c.unmount, 1) }

var (
_ Component = (*testMount)(nil)
_ ComponentMounter = (*testMount)(nil)
)
2 changes: 2 additions & 0 deletions measure.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type TextNodeContext struct {
Size flex.Size
}

func (c *TextNodeContext) Component() Component { return c.C }

// MeasureTextNode implements flex.MeasureFunc and returns the measurements
// for the given node only if the node represents a TextComponent. This is
// the MeasureFunc that is typically used for renderers since all component
Expand Down
19 changes: 12 additions & 7 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ func tree(
node := flex.NewNodeWithConfig(parent.Config)
parent.InsertChild(node, len(parent.Children))

// Setup our default context
parentCtx := &parentContext{C: c}
node.Context = parentCtx

// Check if we're finalized and note it
if _, ok := c.(*finalizedComponent); ok {
node.Context = &parentContext{
Component: c,
Finalized: true,
}

finalize = true
parentCtx.Finalized = true
}

// Finalize
Expand Down Expand Up @@ -77,6 +76,12 @@ func tree(
}

type parentContext struct {
Component Component
C Component
Finalized bool
}

func (c *parentContext) Component() Component { return c.C }

type treeContext interface {
Component() Component
}

0 comments on commit 7170628

Please sign in to comment.