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// export copies the target's frame description into out and adds the parts
287// derived from the UI: damage, cursor and the staged input region.
288func (r *Renderer) export(out *Output) {
289 t := &r.tout
290 retired := append(out.Retired[:0], t.Retired...)
291 damage := append(out.Damage[:0], Rect{Width: t.Width, Height: t.Height})
292 rects := out.InputRects
293 *out = Output{
294 Buffer: t.Buffer, NewBuffer: t.NewBuffer, Retired: retired,
295 Width: t.Width, Height: t.Height, FourCC: t.FourCC, Modifier: t.Modifier,
296 Planes: t.Planes, PlaneCount: t.PlaneCount, Acquire: t.Acquire, Release: t.Release,
297 NewTimelines: t.NewTimelines, AcquirePoint: t.AcquirePoint, ReleasePoint: t.ReleasePoint, ReleaseFD: t.ReleaseFD,
298 Damage: damage, Cursor: cursorOf(r.rt.cursor()), InputRects: rects[:0],
299 }
300 rt := r.rt
301 if !rt.inputStaged {
302 return
303 }
304 rt.inputStaged = false
305 // A region equal to the last reported one costs nothing.
306 if r.haveRegion && r.regionNil == rt.inputNil && slices.Equal(r.region, rt.inputRects) {
307 return
308 }
309 r.haveRegion, r.regionNil = true, rt.inputNil
310 r.region = append(r.region[:0], rt.inputRects...)
311 out.InputRectsChanged = true
312 if rt.inputNil {
313 out.InputRects = nil
314 return
315 }
316 if out.InputRects == nil {
317 out.InputRects = []Rect{}
318 }
319 out.InputRects = append(out.InputRects, rt.inputRects...)
320}
321
322func cursorOf(k css.Keyword) Cursor {
323 switch k {
324 case css.KeywordPointer:
325 return CursorPointer
326 case css.KeywordText:
327 return CursorText
328 case css.KeywordNotAllowed:
329 return CursorNotAllowed
330 }
331 return CursorDefault
332}
333
334// Released must be called when the release eventfd of an Output buffer is
335// readable. It lets the buffer be reused.
336func (r *Renderer) Released(buffer uint64) error {
337 if r.closed {
338 return errClosed
339 }
340 return r.target.Released(buffer)
341}
342
343var errClosed = errors.New("nefergui: renderer closed")
344
345// Close cancels pending pastes and frees the GPU resources. The Renderer is
346// unusable afterwards.
347func (r *Renderer) Close() error {
348 if r.closed {
349 return nil
350 }
351 r.closed = true
352 if r.rt.pasteCancel != nil {
353 r.rt.pasteCancel()
354 }
355 return r.target.Close()
356}
357
358// xkb keysym values; internal/ui never imports an xkb library.
359const (
360 keysymSpace = 0x0020
361 keysymBackspace = 0xff08
362 keysymTab = 0xff09
363 keysymReturn = 0xff0d
364 keysymEscape = 0xff1b
365 keysymHome = 0xff50
366 keysymLeft = 0xff51
367 keysymUp = 0xff52
368 keysymRight = 0xff53
369 keysymDown = 0xff54
370 keysymPageUp = 0xff55
371 keysymPageDown = 0xff56
372 keysymEnd = 0xff57
373 keysymKPEnter = 0xff8d
374 keysymISOLeft = 0xfe20
375 keysymDelete = 0xffff
376)
377
378// keysymName maps a keysym to the name interaction.go dispatches on; other
379// keysyms have no name because only their text matters.
380func keysymName(sym uint32) string {
381 switch sym {
382 case keysymSpace:
383 return "space"
384 case keysymTab:
385 return "tab"
386 case keysymISOLeft:
387 return "iso_left_tab"
388 case keysymLeft:
389 return "left"
390 case keysymRight:
391 return "right"
392 case keysymUp:
393 return "up"
394 case keysymDown:
395 return "down"
396 case keysymHome:
397 return "home"
398 case keysymEnd:
399 return "end"
400 case keysymPageUp:
401 return "page_up"
402 case keysymPageDown:
403 return "page_down"
404 case keysymBackspace:
405 return "backspace"
406 case keysymDelete:
407 return "delete"
408 case 'a', 'A':
409 return "a"
410 case 'c', 'C':
411 return "c"
412 case 'x', 'X':
413 return "x"
414 case 'v', 'V':
415 return "v"
416 case keysymReturn:
417 return "return"
418 case keysymKPEnter:
419 return "kp_enter"
420 case keysymEscape:
421 return "escape"
422 }
423 return ""
424}