1//go:build linux
2
3package ui
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "math"
10 "os"
11 "slices"
12
13 "github.com/bnema/nefergui/internal/css"
14 "github.com/bnema/nefergui/internal/edit"
15 "github.com/bnema/nefergui/internal/layout"
16 "github.com/bnema/nefergui/internal/presentation/session"
17 "github.com/bnema/nefergui/internal/presentation/vkdevice"
18 "github.com/bnema/nefergui/internal/text"
19)
20
21// Format is a DRM format and modifier the compositor accepts.
22type Format struct {
23 FourCC uint32
24 Modifier uint64
25}
26
27// Clipboard is the optional asynchronous clipboard; IME the optional input method.
28type (
29 Clipboard = edit.AsyncClipboard
30 IME = edit.IME
31)
32
33// Plane, Timeline and Retired are the transport-facing parts of Output.
34type (
35 Plane = session.Plane
36 Timeline = session.Timeline
37 Retired = session.Retired
38)
39
40// Cursor is the pointer shape the surface wants.
41type Cursor uint8
42
43const (
44 CursorDefault Cursor = iota
45 CursorPointer
46 CursorText
47 CursorNotAllowed
48)
49
50// RendererConfig configures a Wayland-free Renderer.
51type RendererConfig struct {
52 MainDevice uint64 // dev_t from linux-dmabuf feedback main_device
53 Formats []Format // compositor preference order, tranches flattened
54 Transparent bool // ARGB8888 instead of XRGB8888
55 Styles string // optional CSS file path
56 Clipboard Clipboard
57 IME IME
58}
59
60// Input is one platform input event. Text is read only during the call.
61type Input struct {
62 Kind InputKind
63 X, Y, DX, DY float64 // logical pixels; DX, DY are scroll deltas
64 Button uint32 // evdev code
65 Pressed bool
66 Repeat bool
67 Keysym uint32 // xkb keysym
68 Text []byte
69 Modifiers Modifiers
70}
71
72// Output describes one frame to present, in physical pixels. Every file
73// descriptor stays owned by the Renderer. Slices are reused and valid until the
74// next Render.
75type Output struct {
76 Buffer uint64
77 NewBuffer bool
78 Retired []Retired
79 Width, Height int32
80 FourCC uint32
81 Modifier uint64
82 Planes [4]Plane
83 PlaneCount int
84 Acquire, Release Timeline
85 NewTimelines bool
86 AcquirePoint uint64
87 ReleasePoint uint64
88 ReleaseFD int
89 Damage []Rect
90 Cursor Cursor
91 InputRects []Rect
92 InputRectsChanged bool
93}
94
95// target is the GPU seam of Renderer. *session.Target implements it.
96type target interface {
97 Resize(width, height int32)
98 Draw(display []layout.Command, scale float64, out *session.Output) (bool, error)
99 Released(buffer uint64) error
100 Waiting() bool
101 Close() error
102}
103
104// Renderer renders views into DMA-BUF buffers for a transport it does not
105// know. It owns no Wayland object and starts no goroutine. All methods except
106// Wake run on one owner goroutine.
107type Renderer struct {
108 rt *runtime
109 target target
110 tout session.Output
111 sized bool
112 unsent bool // a built frame still waits for a free buffer
113 w, h float64
114 scale float64
115 pw, ph int32
116 closed bool
117
118 // Last input region handed out, to report only changes.
119 region []Rect
120 regionNil bool
121 haveRegion bool
122}
123
124// asyncClipboard lets an AsyncClipboard satisfy the editor's Clipboard port;
125// synchronous reads are not supported, paste uses ReadTextAsync.
126type asyncClipboard struct{ edit.AsyncClipboard }
127
128func (asyncClipboard) ReadText(int) ([]byte, error) {
129 return nil, errors.New("nefergui: synchronous clipboard read not supported")
130}
131
132// NewRenderer opens the GPU for cfg.MainDevice and loads fonts and styles. It
133// allocates no images.
134func NewRenderer(cfg RendererConfig) (*Renderer, error) {
135 formats := make([]vkdevice.Format, len(cfg.Formats))
136 for i, f := range cfg.Formats {
137 formats[i] = vkdevice.Format{FourCC: f.FourCC, Modifier: f.Modifier}
138 }
139 t, err := session.NewTarget(session.TargetConfig{MainDevice: cfg.MainDevice, Formats: formats, Transparent: cfg.Transparent})
140 if err != nil {
141 return nil, fmt.Errorf("nefergui: open renderer: %w", err)
142 }
143 r, err := newRenderer(cfg, t, text.SystemSource{})
144 if err != nil {
145 _ = t.Close()
146 return nil, err
147 }
148 return r, nil
149}
150
151func newRenderer(cfg RendererConfig, t target, fonts text.FontSource) (*Renderer, error) {
152 rt := newRuntime()
153 if cfg.Styles != "" {
154 data, err := os.ReadFile(cfg.Styles)
155 if err != nil {
156 return nil, fmt.Errorf("nefergui: styles: %w", err)
157 }
158 rt.styles = css.Compile(css.UA(), css.Parse(string(data)))
159 }
160 catalog, err := text.Load(fonts)
161 if err != nil {
162 return nil, fmt.Errorf("nefergui: fonts: %w", err)
163 }
164 if len(catalog.Faces) == 0 {
165 return nil, errors.New("nefergui: no usable fonts")
166 }
167 rt.setTextEngine(text.NewEngine(catalog))
168 ctx, cancel := context.WithCancel(context.Background())
169 rt.pasteCtx, rt.pasteCancel = ctx, cancel
170 if cfg.Clipboard != nil {
171 rt.clipboard = asyncClipboard{cfg.Clipboard}
172 }
173 if cfg.IME != nil {
174 rt.ime = cfg.IME
175 }
176 return &Renderer{rt: rt, target: t, scale: 1}, nil
177}
178
179// Wake returns a channel signaled when work arrives from another goroutine
180// (for example a finished paste); call Render after receiving. Safe from any goroutine.
181func (r *Renderer) Wake() <-chan struct{} { return r.rt.wake }
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.
188func (r *Renderer) Pending() bool { return r.unsent && r.target.Waiting() }
189
190// Resize sets the logical surface size and scale. Invalid values are ignored.
191func (r *Renderer) Resize(width, height int, scale float64) {
192 if r.closed {
193 return
194 }
195 w, h := float64(width), float64(height)
196 if width <= 0 || height <= 0 || !(scale > 0) || math.IsInf(scale, 0) {
197 return
198 }
199 pw, ph := math.Ceil(w*scale-1e-9), math.Ceil(h*scale-1e-9)
200 if pw > math.MaxInt32 || ph > math.MaxInt32 {
201 return
202 }
203 if r.sized && r.w == w && r.h == h && r.scale == scale {
204 return
205 }
206 r.sized, r.w, r.h, r.scale = true, w, h, scale
207 r.pw, r.ph = int32(pw), int32(ph)
208 r.target.Resize(r.pw, r.ph)
209 r.rt.route(platformInput{Kind: "resize", Width: w, Height: h, Scale: scale})
210}
211
212// Invalidate requests a rebuild at the next Render.
213func (r *Renderer) Invalidate() { r.rt.Redraw() }
214
215// Input routes one event and reports whether a redraw is needed.
216func (r *Renderer) Input(ev *Input) bool {
217 if ev == nil || r.closed {
218 return false
219 }
220 switch ev.Kind {
221 case InputReset:
222 r.rt.route(platformInput{Kind: "leave"})
223 r.rt.route(platformInput{Kind: "focus-out"})
224 case InputPointerMotion:
225 r.rt.route(platformInput{Kind: "motion", X: ev.X, Y: ev.Y})
226 case InputPointerPress:
227 r.rt.route(platformInput{Kind: "press", X: ev.X, Y: ev.Y, Button: ev.Button, Shift: ev.Modifiers&ModShift != 0})
228 case InputPointerRelease:
229 r.rt.route(platformInput{Kind: "release", X: ev.X, Y: ev.Y, Button: ev.Button})
230 case InputPointerAxis:
231 if ev.DX == 0 && ev.DY == 0 {
232 return false
233 }
234 r.rt.route(platformInput{Kind: "axis", X: ev.X, Y: ev.Y, DX: ev.DX, DY: ev.DY})
235 case InputPointerLeave:
236 r.rt.route(platformInput{Kind: "leave"})
237 case InputFocusIn:
238 r.rt.route(platformInput{Kind: "focus-in"})
239 case InputFocusOut:
240 r.rt.route(platformInput{Kind: "focus-out"})
241 case InputKey:
242 shift, ctrl := ev.Modifiers&ModShift != 0, ev.Modifiers&ModCtrl != 0
243 key := keyEvent{Name: keysymName(ev.Keysym), Pressed: ev.Pressed, Repeat: ev.Repeat, Shift: shift, Ctrl: ctrl}
244 if len(ev.Text) > 0 {
245 key.Text = string(ev.Text)
246 }
247 r.rt.route(platformInput{Kind: "key", Key: key, Shift: shift, Ctrl: ctrl})
248 default:
249 return false
250 }
251 r.rt.mu.Lock()
252 need := r.rt.redraw || len(r.rt.pending) > 0
253 r.rt.mu.Unlock()
254 return need
255}
256
257// Render builds the view and, when something changed and a buffer is free,
258// draws it. It returns false without touching the GPU when nothing changed, no
259// buffer is available yet (call it again after Released) or before the first
260// Resize. When it returns true, out describes the frame to present.
261func (r *Renderer) Render[T any](out *Output, model *T, view func(*Frame, *T)) (bool, error) {
262 if r.closed {
263 return false, errClosed
264 }
265 if out == nil || model == nil || view == nil {
266 return false, errors.New("nefergui: nil output, model or view")
267 }
268 if !r.sized {
269 return false, nil
270 }
271 if r.rt.Build(func(f *Frame) { view(f, model) }) {
272 r.unsent = true
273 }
274 if !r.unsent {
275 return false, nil
276 }
277 ok, err := r.target.Draw(r.rt.output.Display, r.scale, &r.tout)
278 if err != nil || !ok {
279 return false, err
280 }
281 r.unsent = false
282 r.export(out)
283 return true, nil
284}
285
286// Measure returns the logical size, in pixels rounded up, that view needs for
287// model: its natural width, at most maxWidth, and the height the content takes
288// at that width (text wraps at maxWidth). Use it to size a surface before
289// creating it. The size includes the root's margins.
290//
291// The view is built in a temporary runtime that shares only this Renderer's
292// styles and text engine. Those caches only grow during Measure: it does not
293// age their entries, so a later Render finds its own entries intact. Hover,
294// focus, scroll offsets, editor state, queued input, the Wake channel and the
295// target are neither read nor changed, nothing is drawn, and the view sees no
296// events.
297//
298// There is no surface yet, so the height is indefinite: inside the view
299// Frame.Size reports an unbounded height (1<<20), and percentage height,
300// min-height and max-height of the root are treated as auto, as for nested
301// boxes, instead of resolving against that value. Lengths in pixels apply.
302//
303// Call it from the owner goroutine, before or after Resize, never from inside
304// a view callback. It allocates and shapes text: use it when opening a
305// surface, not on every frame. It returns an error when the Renderer is
306// closed, model or view is nil, maxWidth is not a finite number above zero, or
307// the view cannot be laid out (the layout error is wrapped). A view that
308// declares no root measures 0 by 0.
309func (r *Renderer) Measure[T any](model *T, view func(*Frame, *T), maxWidth float64) (width, height float64, err error) {
310 if r.closed {
311 return 0, 0, errClosed
312 }
313 if model == nil || view == nil {
314 return 0, 0, errors.New("nefergui: nil model or view")
315 }
316 if math.IsNaN(maxWidth) || math.IsInf(maxWidth, 0) || maxWidth <= 0 {
317 return 0, 0, errors.New("nefergui: Measure needs a finite maxWidth above zero")
318 }
319 const tall = 1 << 20
320 tmp := &runtime{
321 wake: make(chan struct{}, 1), styles: r.rt.styles, textEngine: r.rt.textEngine,
322 width: maxWidth, height: tall, scale: 1, redraw: true,
323 edits: make(map[string]*edit.State), measuring: true,
324 }
325 if !tmp.Build(func(f *Frame) { view(f, model) }) {
326 if tmp.layoutErr != nil {
327 return 0, 0, fmt.Errorf("nefergui: measure: %w", tmp.layoutErr)
328 }
329 return 0, 0, errors.New("nefergui: measure: could not build the view")
330 }
331 if tmp.committed == nil {
332 return 0, 0, nil
333 }
334 root := tmp.layoutTree(tmp.committed)
335 if root.Style != nil {
336 st := layout.IndefiniteHeights(*root.Style)
337 root.Style = &st
338 }
339 opts := layout.Options{Width: maxWidth, Height: tall, TextEngine: tmp.textEngine}
340 natural, err := layout.Natural(root, opts)
341 if err != nil {
342 return 0, 0, fmt.Errorf("nefergui: measure: %w", err)
343 }
344 // Natural is a border box without margins, while the surface holds the
345 // margin box: Layout subtracts the margins from the width it is given.
346 var m layout.Edges
347 if tmp.output.Tree != nil {
348 m = tmp.output.Tree.Margin
349 }
350 w := math.Min(natural.W+math.Max(0, m.Left)+math.Max(0, m.Right), maxWidth)
351 opts.Width = w
352 out, err := layout.Layout(root, opts)
353 if err != nil {
354 return 0, 0, fmt.Errorf("nefergui: measure: %w", err)
355 }
356 if out.Tree == nil {
357 return 0, 0, nil
358 }
359 tree := out.Tree
360 h := tree.Border.Y + tree.Border.H + math.Max(0, tree.Margin.Bottom)
361 return math.Ceil(w), math.Ceil(h), nil
362}
363
364// export copies the target's frame description into out and adds the parts
365// derived from the UI: damage, cursor and the staged input region.
366func (r *Renderer) export(out *Output) {
367 t := &r.tout
368 retired := append(out.Retired[:0], t.Retired...)
369 damage := append(out.Damage[:0], Rect{Width: t.Width, Height: t.Height})
370 rects := out.InputRects
371 *out = Output{
372 Buffer: t.Buffer, NewBuffer: t.NewBuffer, Retired: retired,
373 Width: t.Width, Height: t.Height, FourCC: t.FourCC, Modifier: t.Modifier,
374 Planes: t.Planes, PlaneCount: t.PlaneCount, Acquire: t.Acquire, Release: t.Release,
375 NewTimelines: t.NewTimelines, AcquirePoint: t.AcquirePoint, ReleasePoint: t.ReleasePoint, ReleaseFD: t.ReleaseFD,
376 Damage: damage, Cursor: cursorOf(r.rt.cursor()), InputRects: rects[:0],
377 }
378 rt := r.rt
379 if !rt.inputStaged {
380 return
381 }
382 rt.inputStaged = false
383 // A region equal to the last reported one costs nothing.
384 if r.haveRegion && r.regionNil == rt.inputNil && slices.Equal(r.region, rt.inputRects) {
385 return
386 }
387 r.haveRegion, r.regionNil = true, rt.inputNil
388 r.region = append(r.region[:0], rt.inputRects...)
389 out.InputRectsChanged = true
390 if rt.inputNil {
391 out.InputRects = nil
392 return
393 }
394 if out.InputRects == nil {
395 out.InputRects = []Rect{}
396 }
397 out.InputRects = append(out.InputRects, rt.inputRects...)
398}
399
400func cursorOf(k css.Keyword) Cursor {
401 switch k {
402 case css.KeywordPointer:
403 return CursorPointer
404 case css.KeywordText:
405 return CursorText
406 case css.KeywordNotAllowed:
407 return CursorNotAllowed
408 }
409 return CursorDefault
410}
411
412// Released must be called when the release eventfd of an Output buffer is
413// readable. It lets the buffer be reused.
414func (r *Renderer) Released(buffer uint64) error {
415 if r.closed {
416 return errClosed
417 }
418 return r.target.Released(buffer)
419}
420
421var errClosed = errors.New("nefergui: renderer closed")
422
423// Close cancels pending pastes and frees the GPU resources. The Renderer is
424// unusable afterwards.
425func (r *Renderer) Close() error {
426 if r.closed {
427 return nil
428 }
429 r.closed = true
430 if r.rt.pasteCancel != nil {
431 r.rt.pasteCancel()
432 }
433 return r.target.Close()
434}
435
436// xkb keysym values; internal/ui never imports an xkb library.
437const (
438 keysymSpace = 0x0020
439 keysymBackspace = 0xff08
440 keysymTab = 0xff09
441 keysymReturn = 0xff0d
442 keysymEscape = 0xff1b
443 keysymHome = 0xff50
444 keysymLeft = 0xff51
445 keysymUp = 0xff52
446 keysymRight = 0xff53
447 keysymDown = 0xff54
448 keysymPageUp = 0xff55
449 keysymPageDown = 0xff56
450 keysymEnd = 0xff57
451 keysymKPEnter = 0xff8d
452 keysymISOLeft = 0xfe20
453 keysymDelete = 0xffff
454)
455
456// keysymName maps a keysym to the name interaction.go dispatches on; other
457// keysyms have no name because only their text matters.
458func keysymName(sym uint32) string {
459 switch sym {
460 case keysymSpace:
461 return "space"
462 case keysymTab:
463 return "tab"
464 case keysymISOLeft:
465 return "iso_left_tab"
466 case keysymLeft:
467 return "left"
468 case keysymRight:
469 return "right"
470 case keysymUp:
471 return "up"
472 case keysymDown:
473 return "down"
474 case keysymHome:
475 return "home"
476 case keysymEnd:
477 return "end"
478 case keysymPageUp:
479 return "page_up"
480 case keysymPageDown:
481 return "page_down"
482 case keysymBackspace:
483 return "backspace"
484 case keysymDelete:
485 return "delete"
486 case 'a', 'A':
487 return "a"
488 case 'c', 'C':
489 return "c"
490 case 'x', 'X':
491 return "x"
492 case 'v', 'V':
493 return "v"
494 case keysymReturn:
495 return "return"
496 case keysymKPEnter:
497 return "kp_enter"
498 case keysymEscape:
499 return "escape"
500 }
501 return ""
502}