v0.2.0 6e0cddc

hooks.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close