1package core
2
3import (
4 "context"
5 "strconv"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// handleInput dispatches one input event to its handler. A protected
11// session routes every event to protectedInput instead.
12func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
13 c.inputActive = true
14 defer func() { c.inputActive = false }()
15 if c.security.Protected {
16 if err := c.protectedInput(ctx, ev); err != nil {
17 return err
18 }
19 return nil
20 }
21 switch v := ev.(type) {
22 case ports.PointerMotion:
23 return c.pointerMotion(ctx, v)
24 case ports.PointerButton:
25 return c.pointerButton(ctx, v)
26 case ports.PointerAxis:
27 return c.pointerAxis(ctx, v)
28 case ports.SwipeBegin:
29 if c.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30 // The drag owns the pointer: no navigation under it.
31 return nil
32 }
33 if forwardsSwipe(v.Fingers) {
34 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
35 }
36 if err := c.gestureAbort(ctx, v.Time); err != nil {
37 return err
38 }
39 if c.swipeBegin(v) {
40 return c.slid(ctx, false)
41 }
42 return nil
43 case ports.SwipeUpdate:
44 if c.gesture.on {
45 return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time})
46 }
47 before := c.cur().mon.Current()
48 if c.swipeUpdate(v) {
49 return c.slid(ctx, c.cur().mon.Current() != before)
50 }
51 return nil
52 case ports.SwipeEnd:
53 if c.gesture.on && c.gesture.kind == ports.GestureSwipe {
54 return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time)
55 }
56 shown, spawn := c.swipeEnd(v)
57 if spawn != nil {
58 // As a bind: the terminal starts before the new state is published.
59 if err := c.runSpawn(ctx, spawn, Effect{Spawn: true, Stash: true}); err != nil {
60 return err
61 }
62 }
63 return c.slid(ctx, shown)
64 case ports.PinchBegin:
65 return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time)
66 case ports.PinchUpdate:
67 return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time})
68 case ports.PinchEnd:
69 return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time)
70 case ports.HoldBegin:
71 return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time)
72 case ports.HoldEnd:
73 return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time)
74 case ports.KeyEvent:
75 return c.keyEvent(ctx, v)
76 }
77 return nil
78}
79
80func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
81 c.motionTime = v.Time
82 // A locked pointer stays still; relative motion still flows.
83 if c.constraint.Mode != ports.ConstraintLock {
84 c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
85 }
86 // The focused screen follows the pointer, so new windows
87 // and launchers open where the user is.
88 // Only a pointer entering another output switches: keyboard
89 // moves to another screen stick until then. Not mid-drag of a
90 // client; a window drag (drag.go) follows the pointer.
91 if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
92 name := c.screens[i].name()
93 c.pointerOutput = name
94 if name != c.cur().name() {
95 c.focusScreen = i
96 if err := c.publish(ctx); err != nil {
97 return err
98 }
99 }
100 }
101 if c.drag != nil {
102 // The dragged window and the others get no pointer events.
103 return c.dragMotion(ctx, v.Time)
104 }
105 if c.dataDrag != nil {
106 // A data drag goes to the surface under the pointer, by its own
107 // commands.
108 return c.dataDragMotion(ctx, v.Time)
109 }
110 id, x, y := c.hit(c.cursorX, c.cursorY)
111 // Layout changes are intentionally re-hit-tested only on motion.
112 if id != c.pointer {
113 c.pointer = id
114 if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
115 return err
116 }
117 }
118 c.pointerAt = [2]float64{x, y}
119 if id != 0 {
120 if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
121 return err
122 }
123 }
124 return nil
125}
126
127// clickFocus focuses the window id and its output the way a click on it
128// does, unless it already has the keyboard.
129func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
130 s, w := c.screenOf(id)
131 if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
132 return nil
133 }
134 now := c.now()
135 before := c.snapshot(now)
136 w.Click(id)
137 c.focusScreen = c.screenIndex(s.name())
138 c.transition(before, now)
139 return c.publish(ctx)
140}
141
142func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
143 // Buttons a drag took stay in c.buttons until released, so the
144 // security gate admits their release; no client sees it.
145 if !v.Pressed && c.swallow[v.Button] {
146 delete(c.swallow, v.Button)
147 delete(c.buttons, v.Button)
148 return nil
149 }
150 if c.dataDrag != nil {
151 return c.dataDragButton(ctx, v)
152 }
153 if d := c.drag; d != nil {
154 if !v.Pressed && v.Button == d.button {
155 delete(c.buttons, v.Button)
156 return c.endDrag(ctx)
157 }
158 // Other buttons do nothing mid-drag; their releases neither.
159 if v.Pressed {
160 c.buttons[v.Button] = true
161 c.swallow[v.Button] = true
162 }
163 return nil
164 }
165 if v.Pressed && c.switching() {
166 if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
167 return err
168 }
169 }
170 if started, err := c.startDrag(ctx, v); err != nil || started {
171 return err
172 }
173 if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
174 if picked, err := c.overviewClick(ctx); err != nil {
175 return err
176 } else if picked {
177 return nil
178 }
179 }
180 id := c.pointer
181 if c.grab != 0 {
182 id = c.grab
183 }
184 if v.Pressed && c.held() == 0 {
185 // A click outside an open menu closes it.
186 if err := c.dismissGrabs(ctx, id); err != nil {
187 return err
188 }
189 }
190 grab := v.Pressed && c.held() == 0
191 if v.Pressed {
192 if grab {
193 c.grab = id
194 }
195 c.buttons[v.Button] = true
196 c.lastButton = v.Button
197 } else {
198 delete(c.buttons, v.Button)
199 }
200 if id != 0 {
201 if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
202 return err
203 }
204 // A click on an on-demand layer gives it the keyboard.
205 if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
206 if err := c.publish(ctx); err != nil {
207 return err
208 }
209 } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
210 // A click anywhere else takes the keyboard back.
211 c.keyboard.takeBack()
212 if err := c.publish(ctx); err != nil {
213 return err
214 }
215 }
216 // A click focuses the window and its output.
217 if v.Pressed {
218 if err := c.clickFocus(ctx, id); err != nil {
219 return err
220 }
221 }
222 }
223 if c.held() == 0 {
224 c.grab = 0
225 }
226 return nil
227}
228
229func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
230 c.scrollStop(v)
231 // Only wheel/continuous scrolling navigates the overview.
232 // Two-finger scrolling belongs to clients, never to navigation.
233 if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
234 // overviewScroll owns which scrolls step; the shot costs no
235 // allocation once warm.
236 now := c.now()
237 shots := c.snapshot(now)
238 if c.cur().mon.overviewScroll(v) {
239 c.transition(shots, now)
240 if err := c.workspaceVisible(ctx, true); err != nil {
241 return err
242 }
243 if err := c.publish(ctx); err != nil {
244 return err
245 }
246 }
247 return nil
248 }
249 // Scroll goes to the window under the pointer, which has the
250 // pointer focus even mid-drag. None during a window drag.
251 if c.pointer != 0 && c.drag == nil && c.dataDrag == nil {
252 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
253 return err
254 }
255 }
256 return nil
257}
258
259func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
260 c.mods = key.Mods
261 c.keyState = key.State
262 c.repeatKey(key)
263 // Releasing the command key commits the switcher; the release still
264 // goes on to the bind lookup and the clients.
265 if c.switching() && c.mods&c.cmdMod == 0 {
266 if err := c.releaseSwitcher(ctx); err != nil {
267 return err
268 }
269 }
270 if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
271 // Escape cancels the data drag first, even over the switcher;
272 // its release goes nowhere either.
273 c.pressed[heldKey(key)] = true
274 return c.cancelDataDrag(ctx)
275 }
276 if c.switching() && key.Pressed && key.Keysym == "Escape" {
277 // Escape cancels the switcher; its release goes nowhere either.
278 c.pressed[heldKey(key)] = true
279 return c.abortSwitcher(ctx)
280 }
281 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
282 // Escape cancels the drag; its release goes nowhere either.
283 c.cancelDrag()
284 c.pressed[heldKey(key)] = true
285 if err := c.publish(ctx); err != nil {
286 return err
287 }
288 return c.rehit(ctx)
289 }
290 // Hint mode takes every key press before binds and windows.
291 if handled, err := c.hintKey(ctx, key); handled || err != nil {
292 return err
293 }
294 // The overview takes its keys before any window; others still
295 // run binds, and are not forwarded.
296 // A launcher or a menu holding the keyboard gets them first.
297 if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
298 now := c.now()
299 var shots []viewShot
300 if key.Pressed {
301 shots = c.snapshot(now)
302 }
303 if key.Pressed && mon.overviewKey(key) {
304 if !mon.ov.open && keyName(key.Keysym) != "Escape" {
305 id, _ := mon.Focused()
306 if id != 0 {
307 c.requestFlash(flashWindow, id)
308 }
309 }
310 c.pressed[heldKey(key)] = true
311 if mon.ov.open && overviewRepeats(key) {
312 c.startRepeat(key, "")
313 }
314 c.transition(shots, now)
315 if err := c.workspaceVisible(ctx, true); err != nil {
316 return err
317 }
318 if err := c.publish(ctx); err != nil {
319 return err
320 }
321 return nil
322 }
323 }
324 // A focused window inhibiting shortcuts gets every key:
325 // no bind runs (emergency quit and VT switch are handled by
326 // input before core).
327 if c.keyboard.inhibited(c.keyboardFocus()) {
328 if held := heldKey(key); !key.Pressed && c.pressed[held] {
329 // Its press ran a bind before inhibiting began: the
330 // window never saw it.
331 delete(c.pressed, held)
332 return nil
333 }
334 if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
335 return err
336 }
337 return nil
338 }
339 name := keyName(key.Keysym)
340 // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
341 // prints exclam on US); then the physical key (code:N).
342 action, bound := c.binds[binding{key: name, mods: key.Mods}]
343 if !bound && key.Base != "" {
344 action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
345 }
346 if !bound && key.Keycode != 0 {
347 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
348 }
349 // Track presses by physical key: Shift may be released before the key.
350 held := heldKey(key)
351 if !key.Pressed {
352 consumed := c.pressed[held]
353 delete(c.pressed, held)
354 if consumed {
355 return nil
356 }
357 }
358 if key.Pressed {
359 if bound {
360 c.pressed[held] = true
361 if repeats(action) {
362 c.startRepeat(key, action)
363 }
364 return c.runBind(ctx, action)
365 }
366 c.pressed[held] = false
367 }
368 if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
369 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
370 return err
371 }
372 }
373 return nil
374}
375
376// runBind runs the action of a bound key press.
377func (c *Core) runBind(ctx context.Context, action Action) error {
378 if action == ActionScaleUp || action == ActionScaleDown {
379 return c.scaleBind(ctx, action)
380 }
381 if dir := switchDir(action); dir != 0 {
382 return c.switchBind(ctx, dir)
383 }
384 before := c.cur().mon.Current()
385 swiped := c.swipedWorkspace()
386 now := c.now()
387 // Snapshot before the switcher closes: its cards fly back to the layout.
388 shots := c.snapshot(now)
389 if c.switching() {
390 c.cancelSwitcher()
391 }
392 c.keyboard.takeBack() // a bind acts on the windows
393 effect := c.applyAction(action)
394 if effect.Quit {
395 return ErrQuit
396 }
397 if effect.Spawn {
398 if err := c.runSpawn(ctx, before, effect); err != nil {
399 return err
400 }
401 }
402 if c.swipedWorkspace() != swiped {
403 c.dropSwipe()
404 }
405 c.transition(shots, now)
406 if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
407 return err
408 }
409 if effect.Close != 0 {
410 if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
411 return err
412 }
413 }
414 if err := c.publish(ctx); err != nil {
415 return err
416 }
417 if c.dataDrag != nil {
418 // A data drag follows the layout under the still cursor.
419 return c.rehit(ctx)
420 }
421 return nil
422}
423
424// runSpawn starts the programs of a spawn effect of an action applied to w:
425// the given commands, else the configured terminal. A stash effect opens the
426// windows in the stash of w. It reports errSecurityChanged when the input
427// epoch changed meanwhile.
428func (c *Core) runSpawn(ctx context.Context, w *Workspace, effect Effect) error {
429 commands := effect.Commands
430 if commands == nil {
431 commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
432 }
433 if effect.Stash {
434 if err := c.spawnStash(ctx, w, commands, effect.Commands == nil); err != nil {
435 return err
436 }
437 } else {
438 // Explicit actions are suppressed on a full launcher queue;
439 // never keep the input owner waiting across an epoch change.
440 for _, argv := range commands {
441 if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
442 break
443 }
444 }
445 }
446 if c.inputEpochChanged() {
447 return errSecurityChanged
448 }
449 return nil
450}
451
452// spawnStash runs commands whose windows open in the stash of w, the
453// workspace the bind acted on. Wayland reads the claim tokens only while it
454// knows slots are pending, so that is published before the spawn, as
455// spawnEmpty does. A send the gate suppresses leaves no claim behind. With
456// terminal set (the terminal of an empty stash), nothing starts while the
457// previous one is still awaited.
458func (c *Core) spawnStash(ctx context.Context, w *Workspace, commands [][]string, terminal bool) error {
459 if c.protectionRequested() {
460 return nil
461 }
462 now := c.now()
463 c.placement.expireWorkspaceClaims(now)
464 if terminal && c.placement.workspacePending(w, spawnStash) {
465 return nil
466 }
467 reqs := make([]ports.SpawnRequest, len(commands))
468 for i, argv := range commands {
469 reqs[i] = c.placement.request(argv, spawnTarget{kind: spawnStash, ws: w, at: now})
470 }
471 _, err := c.dispatch(ctx, reqs)
472 return err
473}
474
475// scaleBind steps the focused output's scale and asks for it to be saved.
476func (c *Core) scaleBind(ctx context.Context, action Action) error {
477 dir := 1
478 if action == ActionScaleDown {
479 dir = -1
480 }
481 sc := c.cur()
482 prev := sc.scale
483 sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
484 c.order()
485 if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
486 // Never wait on persistence: a stalled save drops the change.
487 select {
488 case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
489 default:
490 }
491 }
492 return c.publish(ctx)
493}