Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@ package gova
import "fmt"

type componentNode struct {
node viewNode
renderFn func(*Scope) View
scope *Scope
rendered *viewNode
node viewNode
renderFn func(*Scope) View
scope *Scope
rendered *viewNode
errFallback func(error) View
}

// renderComponent invokes the component's renderFn. If an ErrorBoundary
// has registered a fallback on this component, panics are recovered and
// the fallback view is returned in place of the panicking subtree.
func renderComponent(c *componentNode, s *Scope) (result View) {
if c.errFallback == nil {
return c.renderFn(s)
}
defer func() {
if r := recover(); r != nil {
result = c.errFallback(toError(r))
}
}()
return c.renderFn(s)
}

func (c *componentNode) viewNode() *viewNode {
Expand Down
26 changes: 8 additions & 18 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,24 @@ import "fmt"
// ErrorBoundary wraps a child view and catches panics during rendering.
// Accepts either a View, a Viewable, or a *componentNode. Viewable values
// are wrapped into a component so their Body() call is guarded.
//
// The fallback is registered on the component as a side channel; the
// rendering path consults it on each render (see renderComponent). This
// keeps the call idempotent — invoking ErrorBoundary on the same
// component across re-renders only updates the fallback, instead of
// nesting panic-recovery wrappers around renderFn.
func ErrorBoundary(child any, fallback func(err error) View) *viewNode {
view := asView(child)
if view == nil {
return &viewNode{kind: viewKindGroup}
}
if comp, ok := view.(*componentNode); ok {
return safeRenderComponent(comp, fallback)
comp.errFallback = fallback
return comp.viewNode()
}
return view.viewNode()
}

func safeRenderComponent(comp *componentNode, fallback func(err error) View) *viewNode {
origRenderFn := comp.renderFn
comp.renderFn = func(s *Scope) View {
var result View
func() {
defer func() {
if r := recover(); r != nil {
result = fallback(toError(r))
}
}()
result = origRenderFn(s)
}()
return result
}
return comp.viewNode()
}

func toError(r any) error {
switch v := r.(type) {
case error:
Expand Down
4 changes: 2 additions & 2 deletions gova.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func RunWithConfig(config AppConfig, root View) {
mu.Lock()
defer mu.Unlock()

newView := comp.renderFn(scope)
newView := renderComponent(comp, scope)
newNode := newView.viewNode()
comp.rendered = newNode
newSpec := toSpecWithScope(newNode, scope)
Expand All @@ -154,7 +154,7 @@ func RunWithConfig(config AppConfig, root View) {
Provide[dialogPresenter](scope, dialogPresenterKey, newPlatformDialogPresenter(w))
Provide(scope, themeStoreKey, config.Theme)

firstView := comp.renderFn(scope)
firstView := renderComponent(comp, scope)
firstNode := firstView.viewNode()
comp.rendered = firstNode
spec := toSpecWithScope(firstNode, scope)
Expand Down
12 changes: 11 additions & 1 deletion gova_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,22 @@ func TestErrorBoundary(t *testing.T) {

comp := panicking.(*componentNode)
scope := newScope(context.Background(), nil)
result := comp.renderFn(scope)
result := renderComponent(comp, scope)
node := result.viewNode()

if node.kind != viewKindText {
t.Fatal("expected fallback Text view")
}

// Re-rendering must not nest panic-recovery wrappers; the
// side-channel fallback should still fire cleanly on subsequent
// renders, with renderFn left untouched.
for i := 0; i < 3; i++ {
result = renderComponent(comp, scope)
if result.viewNode().kind != viewKindText {
t.Fatalf("rerender %d: expected fallback Text view", i)
}
}
}

func TestImageNode(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func toSpecWithScope(node *viewNode, scope *Scope) fyneBridge.ViewSpec {
// gets its own child scope so its State(...) keys do not
// collide with the caller's.
childScope := scope.childScopeFor("body")
rendered := node.componentRef.renderFn(childScope)
rendered := renderComponent(node.componentRef, childScope)
return toSpecWithScope(rendered.viewNode(), childScope)
}

Expand Down Expand Up @@ -357,7 +357,7 @@ func childSpecsWithScope(node *viewNode, scope *Scope) []fyneBridge.ViewSpec {
func renderSlot(node *viewNode, parentScope *Scope, slotID string) fyneBridge.ViewSpec {
if node.componentRef != nil && parentScope != nil {
childScope := parentScope.childScopeFor(slotID)
rendered := node.componentRef.renderFn(childScope)
rendered := renderComponent(node.componentRef, childScope)
return toSpecWithScope(rendered.viewNode(), childScope)
}
return toSpecWithScope(node, parentScope)
Expand Down Expand Up @@ -455,7 +455,7 @@ func navStackSpec(node *viewNode, scope *Scope, theme *Theme) fyneBridge.ViewSpe
func resolveNavModifiers(node *viewNode, scope *Scope) *viewNode {
cur := node
for cur != nil && cur.componentRef != nil && scope != nil {
rendered := cur.componentRef.renderFn(scope)
rendered := renderComponent(cur.componentRef, scope)
cur = rendered.viewNode()
}
return cur
Expand Down
6 changes: 3 additions & 3 deletions testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestRender(root View, opts ...TestOption) *RenderedTree {

var node *viewNode
if comp, ok := root.(*componentNode); ok {
rendered := comp.renderFn(scope)
rendered := renderComponent(comp, scope)
node = rendered.viewNode()
// Resolve nested components
node = resolveTree(node, scope)
Expand All @@ -30,7 +30,7 @@ func TestRender(root View, opts ...TestOption) *RenderedTree {

func resolveTree(node *viewNode, scope *Scope) *viewNode {
if node.componentRef != nil {
rendered := node.componentRef.renderFn(scope)
rendered := renderComponent(node.componentRef, scope)
return resolveTree(rendered.viewNode(), scope)
}
for i, child := range node.children {
Expand Down Expand Up @@ -75,7 +75,7 @@ func (rt *RenderedTree) Rerender() {
if rt.component == nil {
return
}
rendered := rt.component.renderFn(rt.scope)
rendered := renderComponent(rt.component, rt.scope)
rt.root = resolveTree(rendered.viewNode(), rt.scope)
}

Expand Down
Loading