interactive_render.go RawHistoryFolder
1package modes
2
3import (
4 "sync"
5
6 "github.com/bnema/zut/packages/tui"
7)
8
9// renderRequest is deliberately a signal, not a frame. The render owner
10// reads the newest synchronized interactive state when it wakes, so a burst
11// replaces the one pending request instead of queueing stale snapshots.
12type renderRequest struct {
13 clear bool
14 invalidate bool
15 theme *tui.Theme
16}
17
18type latestFrameScheduler struct {
19 wake chan struct{}
20 done chan struct{}
21
22 mu sync.Mutex
23 clear bool
24 invalidate bool
25 theme *tui.Theme
26 stopped bool
27}
28
29func newLatestFrameScheduler() *latestFrameScheduler {
30 return &latestFrameScheduler{
31 wake: make(chan struct{}, 1),
32 done: make(chan struct{}),
33 }
34}
35
36func (s *latestFrameScheduler) request(clear, invalidate bool) bool {
37 s.mu.Lock()
38 defer s.mu.Unlock()
39 if s.stopped {
40 return false
41 }
42 s.clear = s.clear || clear
43 s.invalidate = s.invalidate || invalidate
44 select {
45 case s.wake <- struct{}{}:
46 default:
47 }
48 return true
49}
50
51func (s *latestFrameScheduler) requestTheme(theme tui.Theme) bool {
52 s.mu.Lock()
53 defer s.mu.Unlock()
54 if s.stopped {
55 return false
56 }
57 s.clear = true
58 s.theme = &theme
59 select {
60 case s.wake <- struct{}{}:
61 default:
62 }
63 return true
64}
65
66func (s *latestFrameScheduler) next() (renderRequest, bool) {
67 <-s.wake
68 s.mu.Lock()
69 defer s.mu.Unlock()
70 if s.stopped {
71 return renderRequest{}, false
72 }
73 req := renderRequest{clear: s.clear, invalidate: s.invalidate, theme: s.theme}
74 s.clear = false
75 s.invalidate = false
76 s.theme = nil
77 return req, true
78}
79
80func (s *latestFrameScheduler) run(render func(renderRequest)) {
81 defer close(s.done)
82 for {
83 req, ok := s.next()
84 if !ok {
85 return
86 }
87 render(req)
88 }
89}
90
91// stop waits for the in-flight render callback to finish. Callers must not
92// invoke it while holding i.mu: the callback may need to acquire i.mu.
93func (s *latestFrameScheduler) stop() {
94 s.mu.Lock()
95 if !s.stopped {
96 s.stopped = true
97 close(s.wake)
98 }
99 s.mu.Unlock()
100 <-s.done
101}
102
103func (i *Interactive) requestRendererClear() {
104 if scheduler := i.renderScheduler.Load(); scheduler != nil {
105 scheduler.request(true, false)
106 return
107 }
108 if i.rend != nil {
109 i.rend.Clear()
110 }
111}
112
113func (i *Interactive) requestRendererInvalidate() {
114 if scheduler := i.renderScheduler.Load(); scheduler != nil {
115 scheduler.request(false, true)
116 return
117 }
118 if i.rend != nil {
119 i.rend.Invalidate()
120 }
121}
122
123func (i *Interactive) requestRendererTheme(theme tui.Theme) {
124 if scheduler := i.renderScheduler.Load(); scheduler != nil {
125 scheduler.requestTheme(theme)
126 return
127 }
128 if i.rend != nil {
129 i.rend.SetTheme(theme)
130 }
131}
132
133func (i *Interactive) invalidate() {
134 i.renderRevision.Add(1)
135 // Keep ordinary state changes on the main-loop throttle. The scheduler
136 // owns terminal output, but scheduling it here would bypass
137 // redrawMinInterval and paint every intermediate tool-event frame.
138 select {
139 case i.dirty <- struct{}{}:
140 default:
141 }
142}