v0.1.0 d9218d6

outputs.go RawHistoryFolder

1package wayland
2
3import (
4	"math"
5	"strings"
6
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/bnema/purego-libwayland/protocol/wayland"
9	"github.com/bnema/purego-libwayland/protocol/xdgoutput"
10	"github.com/bnema/purego-libwayland/server"
11)
12
13// Outputs. Core sends the global layout (ports.SetOutputs); each output is
14// one wl_output global, removed when the display is unplugged. Every size
15// sent to clients (configures, xdg-output, layer configures, pointer
16// coordinates) is logical. Clients that bind wp_fractional_scale_v1 get the
17// exact scale of their output in 1/120 steps and draw at physical size
18// through wp_viewporter; others get wl_output.scale rounded up and the
19// renderer downscales.
20
21// output is one advertised display and the resources clients bound to it.
22type output struct {
23	place      ports.OutputPlacement
24	global     *server.Global
25	resources  []*wayland.Output
26	xdgOutputs []*xdgoutput.ZxdgOutputV1
27}
28
29func (o *output) name() string { return o.place.Info.Name }
30
31// integerScale is wl_output.scale: the fractional scale rounded up.
32func integerScale(scale float64) int32 { return int32(math.Ceil(max(scale, 1) - 1e-9)) }
33
34func (o *output) description() string {
35	i := o.place.Info
36	return strings.Join(strings.Fields(i.Make+" "+i.Model+" "+i.Serial+" ("+i.Name+")"), " ")
37}
38
39// sendAll sends the full wl_output state, then done (v2+).
40func (o *output) sendAll(r *wayland.Output) {
41	i := o.place.Info
42	make, model := i.Make, i.Model
43	if make == "" {
44		make, model = "neferwl", "headless"
45	}
46	refresh := i.RefreshMilli
47	if refresh == 0 {
48		refresh = 60000
49	}
50	r.SendGeometry(int32(o.place.X), int32(o.place.Y), int32(i.PhysicalW), int32(i.PhysicalH), 0, make, model, 0)
51	// The mode stays physical; the scale tells clients how to divide it.
52	r.SendMode(3, int32(i.Width), int32(i.Height), int32(refresh))
53	if r.Version() >= 2 {
54		r.SendScale(integerScale(o.place.Scale))
55	}
56	if r.Version() >= 4 {
57		r.SendName(i.Name)
58		r.SendDescription(o.description())
59	}
60	if r.Version() >= 2 {
61		r.SendDone()
62	}
63}
64
65func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
66	x.SendLogicalPosition(int32(o.place.X), int32(o.place.Y))
67	x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
68	if withName && x.Version() >= 2 {
69		x.SendName(o.name())
70		x.SendDescription(o.description())
71	}
72}
73
74// outputOf returns the output a wl_output resource belongs to, or nil for a
75// removed one.
76func (s *Server) outputOf(r *wayland.Output) *output {
77	if r == nil {
78		return nil
79	}
80	for _, o := range s.outputs {
81		for _, v := range o.resources {
82			if v.Resource == r.Resource {
83				return o
84			}
85		}
86	}
87	return nil
88}
89
90// outputByName returns the output with that connector, else the focused one,
91// else the first; nil when there is none.
92func (s *Server) outputByName(name string) *output {
93	var first, focused *output
94	for _, o := range s.outputs {
95		if o.name() == name {
96			return o
97		}
98		if first == nil {
99			first = o
100		}
101		if o.name() == s.focusedOutput {
102			focused = o
103		}
104	}
105	if focused != nil {
106		return focused
107	}
108	return first
109}
110
111// addOutput advertises a new wl_output global.
112func (s *Server) addOutput(p ports.OutputPlacement) {
113	o := &output{place: p}
114	g, err := s.display.AddGlobal(wayland.OutputInterface, 4, func(c server.Client, v, id uint32) {
115		r, err := wayland.NewOutput(c, int32(v), id, outputHandler{})
116		if err != nil {
117			return
118		}
119		// A client may bind a global just removed: it gets the last state
120		// and no updates.
121		o.resources = append(o.resources, r)
122		r.OnDestroy = func() { o.resources = removeItem(o.resources, r) }
123		o.sendAll(r)
124	})
125	if err != nil {
126		s.log.Warn().Err(err).Str("output", p.Info.Name).Msg("add wl_output")
127		return
128	}
129	o.global = g
130	s.outputs = append(s.outputs, o)
131	s.log.Info().Str("output", p.Info.Name).Int("x", p.X).Int("w", p.Width).Int("h", p.Height).Float64("scale", p.Scale).Msg("output added")
132}
133
134// setOutputs applies the global layout from core: new outputs are
135// advertised, gone ones withdrawn (their layer surfaces are closed), and
136// changed ones resent.
137func (s *Server) setOutputs(c ports.SetOutputs) {
138	s.focusedOutput = c.Focused
139	want := map[string]ports.OutputPlacement{}
140	for _, p := range c.Outputs {
141		want[p.Info.Name] = p
142	}
143	kept := s.outputs[:0]
144	for _, o := range s.outputs {
145		if _, ok := want[o.name()]; ok {
146			kept = append(kept, o)
147			continue
148		}
149		o.global.Remove()
150		s.log.Info().Str("output", o.name()).Msg("output removed")
151		for _, l := range s.layers {
152			if l.output == o {
153				l.close()
154			}
155		}
156		for _, surf := range s.surfaces {
157			surf.leave(o)
158		}
159	}
160	s.outputs = kept
161	for _, p := range c.Outputs {
162		o := s.outputByNameExact(p.Info.Name)
163		if o == nil {
164			s.addOutput(p)
165			continue
166		}
167		if o.place == p {
168			continue
169		}
170		scaleChanged := o.place.Scale != p.Scale
171		o.place = p
172		for _, r := range o.resources {
173			o.sendAll(r)
174		}
175		for _, x := range o.xdgOutputs {
176			o.sendXDG(x, false)
177			if x.Version() < 3 {
178				x.SendDone()
179			}
180		}
181		if scaleChanged {
182			s.log.Info().Str("output", p.Info.Name).Float64("scale", p.Scale).Int("w", p.Width).Int("h", p.Height).Msg("output scale")
183		}
184	}
185	// Surfaces follow the scale of their output.
186	for _, surf := range s.surfaces {
187		surf.sendScale()
188	}
189	// Layer surfaces sized from the output follow its new logical size.
190	adopted := false
191	for _, l := range s.layers {
192		if l.output == nil && !l.closed {
193			// Created before any output: it takes the focused one.
194			if l.output = s.outputByName(""); l.output != nil && l.configured {
195				l.sendConfigure()
196				adopted = adopted || l.mapped
197				continue
198			}
199		}
200		// Uses committed state only: pending requests wait for the client's commit.
201		if l.configured && (l.current.width == 0 || l.current.height == 0) {
202			l.sendConfigure()
203		}
204	}
205	if adopted {
206		s.layerChanged()
207	}
208}
209
210func (s *Server) outputByNameExact(name string) *output {
211	for _, o := range s.outputs {
212		if o.name() == name {
213			return o
214		}
215	}
216	return nil
217}
218
219type outputHandler struct{}
220
221func (outputHandler) Release(*wayland.Output) {}
222
223func registerXDGOutput(d *server.Display, s *Server) error {
224	return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
225		_, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
226	})
227}
228
229type xdgOutputManager struct{ server *Server }
230
231func (xdgOutputManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
232func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
233	x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
234	if err != nil {
235		return
236	}
237	o := m.server.outputOf(wl)
238	if o == nil {
239		// Removed output: the object stays inert.
240		return
241	}
242	o.xdgOutputs = append(o.xdgOutputs, x)
243	x.OnDestroy = func() { o.xdgOutputs = removeItem(o.xdgOutputs, x) }
244	o.sendXDG(x, true)
245	if r.Version() < 3 || wl.Version() < 2 {
246		x.SendDone()
247	} else {
248		wl.SendDone()
249	}
250}
251
252type xdgOutput struct{}
253
254func (xdgOutput) Destroy(*xdgoutput.ZxdgOutputV1) {}

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 23

Tags 10

↑↓ moveenter openesc close