gitstack

grindlemire/go-tui code browser

4.4 KB Go 126 lines 2026-02-28 · e90bbf1 raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package tui

import "github.com/grindlemire/go-tui/internal/debug"

// mountKey identifies a component instance by its parent and position.
// Components at the same (parent, index) are considered the same instance
// across renders and are reused from cache.
type mountKey struct {
	parent Component
	index  int
}

// mountState is per-App state for component instance caching.
// Stored on the App struct, accessed via the explicit *App reference during render.
// Uses mark-and-sweep: each render marks active keys, then sweep
// cleans up unmounted components.
type mountState struct {
	cache       map[mountKey]Component
	cleanups    map[mountKey]func()
	activeKeys  map[mountKey]bool // Marked during render, swept after
	persistKeys map[mountKey]bool // keys that survive sweep even when not rendered
}

// newMountState creates a new mountState with initialized maps.
func newMountState() *mountState {
	return &mountState{
		cache:       make(map[mountKey]Component),
		cleanups:    make(map[mountKey]func()),
		activeKeys:  make(map[mountKey]bool),
		persistKeys: make(map[mountKey]bool),
	}
}

// PropsUpdater is an optional interface that components can implement
// to receive updated props when re-rendered from cache. Mount will call
// UpdateProps with a fresh instance containing the new props, allowing
// the cached instance to copy the relevant fields.
type PropsUpdater interface {
	UpdateProps(fresh Component)
}

// mount is the shared implementation for Mount and MountPersistent.
func (a *App) mount(parent Component, index int, factory func() Component) *Element {
	app := a
	ms := app.mounts
	key := mountKey{parent: parent, index: index}
	ms.activeKeys[key] = true // Mark as active this render

	instance, cached := ms.cache[key]
	if !cached {
		instance = factory()
		ms.cache[key] = instance
		debug.Log("Mount: NEW component at index %d, type %T", index, instance)

		// Bind app before Init so state/events are wired up
		if binder, ok := instance.(AppBinder); ok {
			binder.BindApp(app)
		}

		// Call Init() if component implements Initializer
		if init, ok := instance.(Initializer); ok {
			cleanup := init.Init()
			if cleanup != nil {
				ms.cleanups[key] = cleanup
			}
		}
	} else {
		// Component is cached - check if it can receive updated props
		if updater, ok := instance.(PropsUpdater); ok {
			fresh := factory()
			debug.Log("Mount: CACHED component at index %d, calling UpdateProps, type %T", index, instance)
			updater.UpdateProps(fresh)
		} else {
			debug.Log("Mount: CACHED component at index %d, NO UpdateProps, type %T", index, instance)
		}
		// Rebind after props update — fresh Events fields may be unbound
		if binder, ok := instance.(AppBinder); ok {
			binder.BindApp(app)
		}
	}

	// Render the component and tag the element for framework discovery
	el := instance.Render(a)
	el.component = instance
	return el
}

// Mount creates or retrieves a cached component instance and returns
// its rendered element tree. Called by generated code from @Component() syntax.
//
// On first call: executes factory, caches instance, calls Init() if Initializer.
// On subsequent calls: returns cached instance's Render() result.
// If the cached instance implements PropsUpdater, UpdateProps is called
// with a fresh instance to allow prop updates.
// Mark-and-sweep: marks the key as active. Sweep after render cleans stale entries.
func (a *App) Mount(parent Component, index int, factory func() Component) *Element {
	return a.mount(parent, index, factory)
}

// MountPersistent is like Mount but marks the component as persistent,
// preventing it from being cleaned up during sweep even when not active.
// Use this for components that must survive being hidden by conditionals.
func (a *App) MountPersistent(parent Component, index int, factory func() Component) *Element {
	key := mountKey{parent: parent, index: index}
	a.mounts.persistKeys[key] = true
	return a.mount(parent, index, factory)
}

// sweep removes cached instances that were not marked active during the last
// render pass. Calls cleanup functions for removed components.
func (ms *mountState) sweep() {
	for key := range ms.cache {
		if !ms.activeKeys[key] && !ms.persistKeys[key] {
			if unbinder, ok := ms.cache[key].(AppUnbinder); ok {
				unbinder.UnbindApp()
			}
			if cleanup, ok := ms.cleanups[key]; ok {
				cleanup()
				delete(ms.cleanups, key)
			}
			delete(ms.cache, key)
		}
	}
	// Reset active keys for next render
	ms.activeKeys = make(map[mountKey]bool)
}