1package ui
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "math"
8
9 "github.com/bnema/wlturbo/protocol/core"
10 "github.com/bnema/wlturbo/wl"
11
12 "github.com/bnema/nefergui/internal/platform/wayland"
13)
14
15// InputKind classifies an InputEvent.
16type InputKind uint8
17
18const (
19 InputPointerMotion InputKind = iota + 1
20 InputPointerPress
21 InputPointerRelease
22 InputPointerAxis
23 InputPointerLeave
24 InputKey
25 InputFocusIn
26 InputFocusOut
27 InputReset // platform dropped queued input; treat held state as released
28)
29
30// Modifiers is a bit set of keyboard modifiers held during a key event.
31type Modifiers uint8
32
33const (
34 ModShift Modifiers = 1 << iota
35 ModCtrl
36)
37
38// InputEvent is a raw platform event, delivered on the owner loop before the
39// normal control routing. Coordinates are logical, surface-local pixels.
40// Size changes are not input; use OnResize and Frame.Size. Which fields are set
41// depends on Kind:
42//
43// pointer motion/press/release/axis: X, Y; press/release: Button (evdev code)
44// and Pressed; axis: DX, DY
45// key: KeyName, Text, Modifiers, Pressed, Repeat
46type InputEvent struct {
47 Kind InputKind
48 X, Y float64
49 DX, DY float64
50 Button uint32
51 Pressed bool
52 Repeat bool
53 KeyName string
54 Text string
55 Modifiers Modifiers
56}
57
58// OnInput delivers every raw input event to fn on the session owner loop,
59// before the event reaches NeferGUI controls; controls still receive it. fn may
60// mutate the model the view reads. It returns true when it changed state the
61// view shows, which schedules a redraw; a false return costs no frame, so
62// pointer motion does not force a build. A nil fn is ignored. fn must not block.
63func OnInput(fn func(InputEvent) bool) WindowOption {
64 return windowOption(func(w *windowConfig) {
65 if fn != nil {
66 w.onInput = fn
67 }
68 })
69}
70
71// OnResize reports the logical size and scale once at start and again only
72// when one of them changes; a change always schedules a redraw. It runs on the
73// owner loop before the frame is built. A nil fn is ignored.
74func OnResize(fn func(width, height int, scale float64)) WindowOption {
75 return windowOption(func(w *windowConfig) {
76 if fn != nil {
77 w.onResize = fn
78 }
79 })
80}
81
82// Wake requests a redraw whenever a value arrives on ch, so a view can react
83// to state changed by other goroutines. The caller synchronizes such state;
84// the view still runs only on the owner loop. Closing ch stops forwarding. A nil ch is ignored.
85func Wake(ch <-chan struct{}) WindowOption {
86 return windowOption(func(w *windowConfig) {
87 if ch != nil {
88 w.wake = ch
89 }
90 })
91}
92
93// inputHooks holds the optional callbacks; all methods run on the owner loop.
94type inputHooks struct {
95 onInput func(InputEvent) bool
96 onResize func(int, int, float64)
97 haveSize bool
98 width, height int
99 scale float64
100}
101
102func toInputEvent(in wayland.Input) (InputEvent, bool) {
103 ev := InputEvent{X: in.X, Y: in.Y, DX: in.DX, DY: in.DY, Button: in.Button}
104 switch in.Kind {
105 case "motion":
106 ev.Kind = InputPointerMotion
107 case "press":
108 ev.Kind, ev.Pressed = InputPointerPress, true
109 case "release":
110 ev.Kind = InputPointerRelease
111 case "axis":
112 ev.Kind = InputPointerAxis
113 case "leave":
114 ev.Kind = InputPointerLeave
115 case "key":
116 ev.Kind = InputKey
117 ev.KeyName, ev.Text = in.Key.Name, in.Key.Text
118 ev.Pressed, ev.Repeat = in.Key.Pressed, in.Key.Repeat
119 if in.Key.Shift {
120 ev.Modifiers |= ModShift
121 }
122 if in.Key.Ctrl {
123 ev.Modifiers |= ModCtrl
124 }
125 case "focus-in":
126 ev.Kind = InputFocusIn
127 case "focus-out":
128 ev.Kind = InputFocusOut
129 case wayland.InputReset:
130 ev.Kind = InputReset
131 default:
132 return InputEvent{}, false
133 }
134 return ev, true
135}
136
137// input reports one platform input and whether the hook changed view state;
138// it never allocates.
139func (h *inputHooks) input(in wayland.Input) bool {
140 if h.onInput == nil {
141 return false
142 }
143 if ev, ok := toInputEvent(in); ok {
144 return h.onInput(ev)
145 }
146 return false
147}
148
149// resize reports a size or scale only when it differs from the last report and
150// reports whether it did.
151func (h *inputHooks) resize(width, height int, scale float64) bool {
152 if h.haveSize && h.width == width && h.height == height && h.scale == scale {
153 return false
154 }
155 h.haveSize, h.width, h.height, h.scale = true, width, height, scale
156 if h.onResize != nil {
157 h.onResize(width, height, scale)
158 }
159 return true
160}
161
162// forwardWake turns external wake tokens into redraw requests. Redraw records
163// the request before signalling the owner loop, so the next Build is not idle.
164func (r *runtime) forwardWake(ctx context.Context, ch <-chan struct{}) {
165 for {
166 select {
167 case <-ctx.Done():
168 return
169 case _, ok := <-ch:
170 if !ok {
171 return
172 }
173 r.Redraw()
174 }
175 }
176}
177
178// WaylandSurface exposes the window's Wayland connection and wl_surface so an
179// integrator can register extra protocol objects on the same client, such as
180// clipboard or surface-marking extensions. It is the escape hatch to the
181// underlying wlturbo objects; it does not add any protocol itself.
182//
183// The values are owned by NeferGUI and valid until Run returns. Do not close
184// the Display, destroy the Surface or attach buffers. Events for objects the
185// callback creates are dispatched on the owner goroutine while requests such as
186// roundtrips run before the event reader starts, and on NeferGUI's reader
187// goroutine afterwards, so handlers must synchronize any state they share.
188type WaylandSurface struct {
189 Display *wl.Display
190 Surface *core.Surface
191}
192
193// OnSurface calls fn once on the owner goroutine, after the surface has its
194// role (xdg toplevel or layer surface) and its first configure, and before the
195// first buffer is attached, so the window is not yet visible. ctx is the Run
196// context: if it is cancelled while fn runs, NeferGUI closes the display so
197// blocking protocol calls fail, and Run returns ctx.Err() (joined with any
198// other error fn returned). A non-nil error aborts Run and closes the session.
199// A nil fn is ignored.
200func OnSurface(fn func(context.Context, WaylandSurface) error) WindowOption {
201 return windowOption(func(w *windowConfig) {
202 if fn != nil {
203 w.onSurface = fn
204 }
205 })
206}
207
208// SetInputRects stages the input region for the surface: pointer input outside
209// the rectangles passes through. It is applied after the view returns and
210// and committed together with the frame's buffer, with no extra commit. nil restores the whole surface, and a non-nil
211// empty slice makes the surface click-through. Rects need positive size. The
212// slice is copied, so callers may reuse it; an unchanged region costs nothing.
213// If the view never calls it, the Layer config's InputRects stay in effect.
214func (f *Frame) SetInputRects(rects []Rect) error {
215 if !f.active {
216 panic("nefergui: SetInputRects outside frame")
217 }
218 for i, r := range rects {
219 if r.Width <= 0 || r.Height <= 0 || int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
220 return fmt.Errorf("nefergui: input rect %d %+v is empty or overflows", i, r)
221 }
222 }
223 o := f.owner
224 o.inputNil = rects == nil
225 o.inputRects = append(o.inputRects[:0], rects...)
226 o.inputStaged = true
227 return nil
228}
229
230// applyInputRects hands a staged region to the platform's stage function, which
231// must not commit: the region rides the frame's own buffer commit. The region
232// stays staged until stage succeeds, so a failure is retried on the next frame.
233func (r *runtime) applyInputRects(stage func([]wayland.Rect) error) error {
234 if !r.inputStaged {
235 return nil
236 }
237 if r.inputNil {
238 if err := stage(nil); err != nil {
239 return err
240 }
241 r.inputStaged = false
242 return nil
243 }
244 out := r.inputOut[:0]
245 if out == nil {
246 out = []wayland.Rect{}
247 }
248 for _, q := range r.inputRects {
249 out = append(out, wayland.Rect{X: q.X, Y: q.Y, W: q.Width, H: q.Height})
250 }
251 r.inputOut = out
252 if err := stage(out); err != nil {
253 return err
254 }
255 r.inputStaged = false
256 return nil
257}
258
259// startupHook runs the OnSurface callback. When ctx can be cancelled, the
260// display is closed on cancellation so blocking calls in fn return; a
261// cancelled ctx is reported joined with fn's own error.
262func startupHook(ctx context.Context, fn func(context.Context, WaylandSurface) error, s WaylandSurface) error {
263 if ctx.Done() != nil {
264 closed := make(chan struct{})
265 stop := context.AfterFunc(ctx, func() { _ = s.Display.Close(); close(closed) })
266 defer func() {
267 if !stop() {
268 <-closed // the close callback already started; let it finish
269 }
270 }()
271 }
272 err := fn(ctx, s)
273 if cerr := ctx.Err(); cerr != nil {
274 return errors.Join(cerr, err)
275 }
276 return err
277}