feat: add Renderer.Pending, make closed Renderer inert, size TextInt buffer
Pending reports a built frame waiting for the GPU so the app can retry on a short timer. Resize, Input and Released return early after Close.
- commit
- 84a151f3e511ef9c0e90c454f9ef9c24e514370a
- author
- brice · · 2026-10-01 17:40 UTC
- parent
- eba94e9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +73 -7
M internal/ui/controls.go +5 -4
| @@ -233,15 +233,16 @@ func (n Node) Heading(text string, options ...HeadingOption) { | |||
| 233 | 233 | } | |
| 234 | 234 | } | |
| 235 | 235 | ||
| 236 | - | // TextInt shows prefix followed by v in decimal without fmt boxing. It builds | |
| 237 | - | // the text in a stack buffer, so the only allocation is the final string. | |
| 236 | + | // TextInt shows prefix followed by v in decimal without fmt boxing. A prefix | |
| 237 | + | // up to 76 bytes is built in a stack buffer, so the only allocation is the | |
| 238 | + | // final string; a longer prefix costs one more. | |
| 238 | 239 | func (n Node) TextInt(prefix string, v int64, options ...ContainerOption) { | |
| 239 | 240 | child := n.child("text", "", options) | |
| 240 | 241 | if !child.valid() { | |
| 241 | 242 | return | |
| 242 | 243 | } | |
| 243 | - | var stack [64]byte | |
| 244 | - | buf := append(stack[:0], prefix...) | |
| 244 | + | var stack [96]byte | |
| 245 | + | buf := append(stack[:0], prefix...) // grows on the heap for a long prefix | |
| 245 | 246 | buf = strconv.AppendInt(buf, v, 10) | |
| 246 | 247 | child.element.text = string(buf) | |
| 247 | 248 | } | |
| @@ -233,15 +233,16 @@ func (n Node) Heading(text string, options ...HeadingOption) { | |||||
| 233 | } | 233 | } | ||
| 234 | } | 234 | } | ||
| 235 | 235 | ||||
| 236 | - | // TextInt shows prefix followed by v in decimal without fmt boxing. It builds | 236 | + | // TextInt shows prefix followed by v in decimal without fmt boxing. A prefix |
| 237 | - | // the text in a stack buffer, so the only allocation is the final string. | 237 | + | // up to 76 bytes is built in a stack buffer, so the only allocation is the |
| 238 | + | // final string; a longer prefix costs one more. | |||
| 238 | func (n Node) TextInt(prefix string, v int64, options ...ContainerOption) { | 239 | func (n Node) TextInt(prefix string, v int64, options ...ContainerOption) { | ||
| 239 | child := n.child("text", "", options) | 240 | child := n.child("text", "", options) | ||
| 240 | if !child.valid() { | 241 | if !child.valid() { | ||
| 241 | return | 242 | return | ||
| 242 | } | 243 | } | ||
| 243 | - | var stack [64]byte | 244 | + | var stack [96]byte |
| 244 | - | buf := append(stack[:0], prefix...) | 245 | + | buf := append(stack[:0], prefix...) // grows on the heap for a long prefix |
| 245 | buf = strconv.AppendInt(buf, v, 10) | 246 | buf = strconv.AppendInt(buf, v, 10) | ||
| 246 | child.element.text = string(buf) | 247 | child.element.text = string(buf) | ||
| 247 | } | 248 | } | ||
M internal/ui/renderer.go +21 -3
| @@ -97,6 +97,7 @@ type target interface { | |||
| 97 | 97 | Resize(width, height int32) | |
| 98 | 98 | Draw(display []layout.Command, scale float64, out *session.Output) (bool, error) | |
| 99 | 99 | Released(buffer uint64) error | |
| 100 | + | Waiting() bool | |
| 100 | 101 | Close() error | |
| 101 | 102 | } | |
| 102 | 103 | ||
| @@ -179,8 +180,18 @@ func newRenderer(cfg RendererConfig, t target, fonts text.FontSource) (*Renderer | |||
| 179 | 180 | // (for example a finished paste); call Render after receiving. Safe from any goroutine. | |
| 180 | 181 | func (r *Renderer) Wake() <-chan struct{} { return r.rt.wake } | |
| 181 | 182 | ||
| 183 | + | // Pending reports that a built frame is waiting for the GPU (a buffer still | |
| 184 | + | // being drawn, or a release wait not yet installed) rather than for the | |
| 185 | + | // compositor. No event announces this: arm a short timer (for example 2 ms) | |
| 186 | + | // and call Render again. It is false while every buffer belongs to the | |
| 187 | + | // compositor; Released is the wake-up for that case. | |
| 188 | + | func (r *Renderer) Pending() bool { return r.unsent && r.target.Waiting() } | |
| 189 | + | ||
| 182 | 190 | // Resize sets the logical surface size and scale. Invalid values are ignored. | |
| 183 | 191 | func (r *Renderer) Resize(width, height int, scale float64) { | |
| 192 | + | if r.closed { | |
| 193 | + | return | |
| 194 | + | } | |
| 184 | 195 | w, h := float64(width), float64(height) | |
| 185 | 196 | if width <= 0 || height <= 0 || !(scale > 0) || math.IsInf(scale, 0) { | |
| 186 | 197 | return | |
| @@ -203,7 +214,7 @@ func (r *Renderer) Invalidate() { r.rt.Redraw() } | |||
| 203 | 214 | ||
| 204 | 215 | // Input routes one event and reports whether a redraw is needed. | |
| 205 | 216 | func (r *Renderer) Input(ev *Input) bool { | |
| 206 | - | if ev == nil { | |
| 217 | + | if ev == nil || r.closed { | |
| 207 | 218 | return false | |
| 208 | 219 | } | |
| 209 | 220 | switch ev.Kind { | |
| @@ -249,7 +260,7 @@ func (r *Renderer) Input(ev *Input) bool { | |||
| 249 | 260 | // Resize. When it returns true, out describes the frame to present. | |
| 250 | 261 | func (r *Renderer) Render[T any](out *Output, model *T, view func(*Frame, *T)) (bool, error) { | |
| 251 | 262 | if r.closed { | |
| 252 | - | return false, errors.New("nefergui: renderer closed") | |
| 263 | + | return false, errClosed | |
| 253 | 264 | } | |
| 254 | 265 | if out == nil || model == nil || view == nil { | |
| 255 | 266 | return false, errors.New("nefergui: nil output, model or view") | |
| @@ -322,7 +333,14 @@ func cursorOf(k css.Keyword) Cursor { | |||
| 322 | 333 | ||
| 323 | 334 | // Released must be called when the release eventfd of an Output buffer is | |
| 324 | 335 | // readable. It lets the buffer be reused. | |
| 325 | - | func (r *Renderer) Released(buffer uint64) error { return r.target.Released(buffer) } | |
| 336 | + | func (r *Renderer) Released(buffer uint64) error { | |
| 337 | + | if r.closed { | |
| 338 | + | return errClosed | |
| 339 | + | } | |
| 340 | + | return r.target.Released(buffer) | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | var errClosed = errors.New("nefergui: renderer closed") | |
| 326 | 344 | ||
| 327 | 345 | // Close cancels pending pastes and frees the GPU resources. The Renderer is | |
| 328 | 346 | // unusable afterwards. | |
| @@ -97,6 +97,7 @@ type target interface { | |||||
| 97 | Resize(width, height int32) | 97 | Resize(width, height int32) | ||
| 98 | Draw(display []layout.Command, scale float64, out *session.Output) (bool, error) | 98 | Draw(display []layout.Command, scale float64, out *session.Output) (bool, error) | ||
| 99 | Released(buffer uint64) error | 99 | Released(buffer uint64) error | ||
| 100 | + | Waiting() bool | |||
| 100 | Close() error | 101 | Close() error | ||
| 101 | } | 102 | } | ||
| 102 | 103 | ||||
| @@ -179,8 +180,18 @@ func newRenderer(cfg RendererConfig, t target, fonts text.FontSource) (*Renderer | |||||
| 179 | // (for example a finished paste); call Render after receiving. Safe from any goroutine. | 180 | // (for example a finished paste); call Render after receiving. Safe from any goroutine. | ||
| 180 | func (r *Renderer) Wake() <-chan struct{} { return r.rt.wake } | 181 | func (r *Renderer) Wake() <-chan struct{} { return r.rt.wake } | ||
| 181 | 182 | ||||
| 183 | + | // Pending reports that a built frame is waiting for the GPU (a buffer still | |||
| 184 | + | // being drawn, or a release wait not yet installed) rather than for the | |||
| 185 | + | // compositor. No event announces this: arm a short timer (for example 2 ms) | |||
| 186 | + | // and call Render again. It is false while every buffer belongs to the | |||
| 187 | + | // compositor; Released is the wake-up for that case. | |||
| 188 | + | func (r *Renderer) Pending() bool { return r.unsent && r.target.Waiting() } | |||
| 189 | + | ||||
| 182 | // Resize sets the logical surface size and scale. Invalid values are ignored. | 190 | // Resize sets the logical surface size and scale. Invalid values are ignored. | ||
| 183 | func (r *Renderer) Resize(width, height int, scale float64) { | 191 | func (r *Renderer) Resize(width, height int, scale float64) { | ||
| 192 | + | if r.closed { | |||
| 193 | + | return | |||
| 194 | + | } | |||
| 184 | w, h := float64(width), float64(height) | 195 | w, h := float64(width), float64(height) | ||
| 185 | if width <= 0 || height <= 0 || !(scale > 0) || math.IsInf(scale, 0) { | 196 | if width <= 0 || height <= 0 || !(scale > 0) || math.IsInf(scale, 0) { | ||
| 186 | return | 197 | return | ||
| @@ -203,7 +214,7 @@ func (r *Renderer) Invalidate() { r.rt.Redraw() } | |||||
| 203 | 214 | ||||
| 204 | // Input routes one event and reports whether a redraw is needed. | 215 | // Input routes one event and reports whether a redraw is needed. | ||
| 205 | func (r *Renderer) Input(ev *Input) bool { | 216 | func (r *Renderer) Input(ev *Input) bool { | ||
| 206 | - | if ev == nil { | 217 | + | if ev == nil || r.closed { |
| 207 | return false | 218 | return false | ||
| 208 | } | 219 | } | ||
| 209 | switch ev.Kind { | 220 | switch ev.Kind { | ||
| @@ -249,7 +260,7 @@ func (r *Renderer) Input(ev *Input) bool { | |||||
| 249 | // Resize. When it returns true, out describes the frame to present. | 260 | // Resize. When it returns true, out describes the frame to present. | ||
| 250 | func (r *Renderer) Render[T any](out *Output, model *T, view func(*Frame, *T)) (bool, error) { | 261 | func (r *Renderer) Render[T any](out *Output, model *T, view func(*Frame, *T)) (bool, error) { | ||
| 251 | if r.closed { | 262 | if r.closed { | ||
| 252 | - | return false, errors.New("nefergui: renderer closed") | 263 | + | return false, errClosed |
| 253 | } | 264 | } | ||
| 254 | if out == nil || model == nil || view == nil { | 265 | if out == nil || model == nil || view == nil { | ||
| 255 | return false, errors.New("nefergui: nil output, model or view") | 266 | return false, errors.New("nefergui: nil output, model or view") | ||
| @@ -322,7 +333,14 @@ func cursorOf(k css.Keyword) Cursor { | |||||
| 322 | 333 | ||||
| 323 | // Released must be called when the release eventfd of an Output buffer is | 334 | // Released must be called when the release eventfd of an Output buffer is | ||
| 324 | // readable. It lets the buffer be reused. | 335 | // readable. It lets the buffer be reused. | ||
| 325 | - | func (r *Renderer) Released(buffer uint64) error { return r.target.Released(buffer) } | 336 | + | func (r *Renderer) Released(buffer uint64) error { |
| 337 | + | if r.closed { | |||
| 338 | + | return errClosed | |||
| 339 | + | } | |||
| 340 | + | return r.target.Released(buffer) | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | var errClosed = errors.New("nefergui: renderer closed") | |||
| 326 | 344 | ||||
| 327 | // Close cancels pending pastes and frees the GPU resources. The Renderer is | 345 | // Close cancels pending pastes and frees the GPU resources. The Renderer is | ||
| 328 | // unusable afterwards. | 346 | // unusable afterwards. | ||
M internal/ui/target_mock_test.go +44 -0
| @@ -258,3 +258,47 @@ func (_c *mocktarget_Resize_Call) RunAndReturn(run func(width int32, height int3 | |||
| 258 | 258 | _c.Run(run) | |
| 259 | 259 | return _c | |
| 260 | 260 | } | |
| 261 | + | ||
| 262 | + | // Waiting provides a mock function for the type mocktarget | |
| 263 | + | func (_mock *mocktarget) Waiting() bool { | |
| 264 | + | ret := _mock.Called() | |
| 265 | + | ||
| 266 | + | if len(ret) == 0 { | |
| 267 | + | panic("no return value specified for Waiting") | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | var r0 bool | |
| 271 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 272 | + | r0 = returnFunc() | |
| 273 | + | } else { | |
| 274 | + | r0 = ret.Get(0).(bool) | |
| 275 | + | } | |
| 276 | + | return r0 | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | // mocktarget_Waiting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Waiting' | |
| 280 | + | type mocktarget_Waiting_Call struct { | |
| 281 | + | *mock.Call | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // Waiting is a helper method to define mock.On call | |
| 285 | + | func (_e *mocktarget_Expecter) Waiting() *mocktarget_Waiting_Call { | |
| 286 | + | return &mocktarget_Waiting_Call{Call: _e.mock.On("Waiting")} | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | func (_c *mocktarget_Waiting_Call) Run(run func()) *mocktarget_Waiting_Call { | |
| 290 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 291 | + | run() | |
| 292 | + | }) | |
| 293 | + | return _c | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | func (_c *mocktarget_Waiting_Call) Return(b bool) *mocktarget_Waiting_Call { | |
| 297 | + | _c.Call.Return(b) | |
| 298 | + | return _c | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | func (_c *mocktarget_Waiting_Call) RunAndReturn(run func() bool) *mocktarget_Waiting_Call { | |
| 302 | + | _c.Call.Return(run) | |
| 303 | + | return _c | |
| 304 | + | } | |
| @@ -258,3 +258,47 @@ func (_c *mocktarget_Resize_Call) RunAndReturn(run func(width int32, height int3 | |||||
| 258 | _c.Run(run) | 258 | _c.Run(run) | ||
| 259 | return _c | 259 | return _c | ||
| 260 | } | 260 | } | ||
| 261 | + | ||||
| 262 | + | // Waiting provides a mock function for the type mocktarget | |||
| 263 | + | func (_mock *mocktarget) Waiting() bool { | |||
| 264 | + | ret := _mock.Called() | |||
| 265 | + | ||||
| 266 | + | if len(ret) == 0 { | |||
| 267 | + | panic("no return value specified for Waiting") | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | var r0 bool | |||
| 271 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |||
| 272 | + | r0 = returnFunc() | |||
| 273 | + | } else { | |||
| 274 | + | r0 = ret.Get(0).(bool) | |||
| 275 | + | } | |||
| 276 | + | return r0 | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | // mocktarget_Waiting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Waiting' | |||
| 280 | + | type mocktarget_Waiting_Call struct { | |||
| 281 | + | *mock.Call | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // Waiting is a helper method to define mock.On call | |||
| 285 | + | func (_e *mocktarget_Expecter) Waiting() *mocktarget_Waiting_Call { | |||
| 286 | + | return &mocktarget_Waiting_Call{Call: _e.mock.On("Waiting")} | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | func (_c *mocktarget_Waiting_Call) Run(run func()) *mocktarget_Waiting_Call { | |||
| 290 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 291 | + | run() | |||
| 292 | + | }) | |||
| 293 | + | return _c | |||
| 294 | + | } | |||
| 295 | + | ||||
| 296 | + | func (_c *mocktarget_Waiting_Call) Return(b bool) *mocktarget_Waiting_Call { | |||
| 297 | + | _c.Call.Return(b) | |||
| 298 | + | return _c | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | func (_c *mocktarget_Waiting_Call) RunAndReturn(run func() bool) *mocktarget_Waiting_Call { | |||
| 302 | + | _c.Call.Return(run) | |||
| 303 | + | return _c | |||
| 304 | + | } | |||
M renderer.go +3 -0
| @@ -10,6 +10,9 @@ import "github.com/bnema/nefergui/internal/ui" | |||
| 10 | 10 | // Input and Resize, call Render when a redraw is needed, import and present | |
| 11 | 11 | // the Output buffer, and call Released when its ReleaseFD becomes readable. | |
| 12 | 12 | // | |
| 13 | + | // Pending() reports a built frame waiting for the GPU rather than the | |
| 14 | + | // compositor; arm a short timer (about 2 ms) and call Render again. | |
| 15 | + | // | |
| 13 | 16 | // Render is generic over the model type, so it is a generic method: | |
| 14 | 17 | // | |
| 15 | 18 | // ok, err := r.Render(&out, &model, view) | |
| @@ -10,6 +10,9 @@ import "github.com/bnema/nefergui/internal/ui" | |||||
| 10 | // Input and Resize, call Render when a redraw is needed, import and present | 10 | // Input and Resize, call Render when a redraw is needed, import and present | ||
| 11 | // the Output buffer, and call Released when its ReleaseFD becomes readable. | 11 | // the Output buffer, and call Released when its ReleaseFD becomes readable. | ||
| 12 | // | 12 | // | ||
| 13 | + | // Pending() reports a built frame waiting for the GPU rather than the | |||
| 14 | + | // compositor; arm a short timer (about 2 ms) and call Render again. | |||
| 15 | + | // | |||
| 13 | // Render is generic over the model type, so it is a generic method: | 16 | // Render is generic over the model type, so it is a generic method: | ||
| 14 | // | 17 | // | ||
| 15 | // ok, err := r.Render(&out, &model, view) | 18 | // ok, err := r.Render(&out, &model, view) | ||