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.pgrab != nil {
102 return c.pgrab.motion(ctx, v.Time)
103 }
104 if c.drag != nil {
105 // The dragged window and the others get no pointer events.
106 return c.dragMotion(ctx, v.Time)
107 }
108 if c.dataDrag != nil {
109 // A data drag goes to the surface under the pointer, by its own
110 // commands.
111 return c.dataDragMotion(ctx, v.Time)
112 }
113 id, x, y := c.hit(c.cursorX, c.cursorY)
114 // Layout changes are intentionally re-hit-tested only on motion.
115 if id != c.pointer {
116 c.pointer = id
117 if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
118 return err
119 }
120 }
121 c.pointerAt = [2]float64{x, y}
122 if id != 0 {
123 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 {
124 return err
125 }
126 }
127 return nil
128}
129
130// clickFocus focuses the window id and its output the way a click on it
131// does, unless it already has the keyboard.
132func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
133 s, w := c.screenOf(id)
134 if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
135 return nil
136 }
137 now := c.now()
138 before := c.snapshot(now)
139 c.focusClick(s, w, id)
140 c.transition(before, now)
141 return c.publish(ctx)
142}
143
144// focusClick raises the clicked window and focuses its output. Modal dialogs
145// can take the parent's focus, so flash the resolved focus, not the click target.
146func (c *Core) focusClick(s *screen, w *Workspace, id WindowID) {
147 before, _ := c.cur().mon.Focused()
148 w.Click(id)
149 c.focusScreen = c.screenIndex(s.name())
150 after, _ := c.cur().mon.Focused()
151 if after != 0 && after != before {
152 c.requestFlash(flashWindow, after)
153 }
154}
155
156func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
157 // Buttons a drag took stay in c.buttons until released, so the
158 // security gate admits their release; no client sees it.
159 if !v.Pressed && c.swallow[v.Button] {
160 delete(c.swallow, v.Button)
161 delete(c.buttons, v.Button)
162 return nil
163 }
164 if c.dataDrag != nil {
165 return c.dataDragButton(ctx, v)
166 }
167 if g := c.pgrab; g != nil {
168 if !v.Pressed && v.Button == g.button() {
169 delete(c.buttons, v.Button)
170 return c.endPointerGrab(ctx)
171 }
172 if v.Pressed {
173 c.buttons[v.Button], c.swallow[v.Button] = true, true
174 }
175 return nil
176 }
177 if d := c.drag; d != nil {
178 if !v.Pressed && v.Button == d.button {
179 delete(c.buttons, v.Button)
180 return c.endDrag(ctx)
181 }
182 // Other buttons do nothing mid-drag; their releases neither.
183 if v.Pressed {
184 c.buttons[v.Button] = true
185 c.swallow[v.Button] = true
186 }
187 return nil
188 }
189 if v.Pressed && c.switching() {
190 if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
191 return err
192 }
193 }
194 if started, err := c.startPointerGrab(ctx, v); err != nil || started {
195 return err
196 }
197 if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
198 if picked, err := c.overviewClick(ctx); err != nil {
199 return err
200 } else if picked {
201 return nil
202 }
203 }
204 id := c.pointer
205 if c.grab != 0 {
206 id = c.grab
207 }
208 if v.Pressed && c.held() == 0 {
209 // A click outside an open menu closes it.
210 if err := c.dismissGrabs(ctx, id); err != nil {
211 return err
212 }
213 }
214 grab := v.Pressed && c.held() == 0
215 if v.Pressed {
216 if grab {
217 c.grab = id
218 }
219 c.buttons[v.Button] = true
220 c.lastButton = v.Button
221 } else {
222 delete(c.buttons, v.Button)
223 }
224 if id != 0 {
225 if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
226 return err
227 }
228 // A click on an on-demand layer gives it the keyboard.
229 if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
230 if err := c.publish(ctx); err != nil {
231 return err
232 }
233 } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
234 // A click anywhere else takes the keyboard back.
235 c.keyboard.takeBack()
236 if err := c.publish(ctx); err != nil {
237 return err
238 }
239 }
240 // A click focuses the window and its output.
241 if v.Pressed {
242 if err := c.clickFocus(ctx, id); err != nil {
243 return err
244 }
245 }
246 }
247 if c.held() == 0 {
248 c.grab = 0
249 }
250 return nil
251}
252
253func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
254 c.scrollStop(v)
255 // Only wheel/continuous scrolling navigates the overview.
256 // Two-finger scrolling belongs to clients, never to navigation.
257 if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
258 // overviewScroll owns which scrolls step; the shot costs no
259 // allocation once warm.
260 now := c.now()
261 shots := c.snapshot(now)
262 if c.cur().mon.overviewScroll(v) {
263 c.transition(shots, now)
264 if err := c.workspaceVisible(ctx, true); err != nil {
265 return err
266 }
267 if err := c.publish(ctx); err != nil {
268 return err
269 }
270 }
271 return nil
272 }
273 // Scroll goes to the window under the pointer, which has the
274 // pointer focus even mid-drag. None during a window drag.
275 if c.pointer != 0 && !c.pointerBusy() && c.dataDrag == nil {
276 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
277 return err
278 }
279 }
280 return nil
281}
282
283func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
284 c.mods = key.Mods
285 c.keyState = key.State
286 c.repeatKey(key)
287 // Releasing the command key commits the switcher; the release still
288 // goes on to the bind lookup and the clients.
289 if c.switching() && c.mods&c.cmdMod == 0 {
290 if err := c.releaseSwitcher(ctx); err != nil {
291 return err
292 }
293 }
294 if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
295 // Escape cancels the data drag first, even over the switcher;
296 // its release goes nowhere either.
297 c.pressed[heldKey(key)] = true
298 return c.cancelDataDrag(ctx)
299 }
300 if c.switching() && key.Pressed && key.Keysym == "Escape" {
301 // Escape cancels the switcher; its release goes nowhere either.
302 c.pressed[heldKey(key)] = true
303 return c.abortSwitcher(ctx)
304 }
305 if c.pointerBusy() && key.Pressed && key.Keysym == "Escape" {
306 // Escape cancels the grab; its release goes nowhere either.
307 if c.pgrab != nil {
308 c.pgrab.cancel()
309 if err := c.abortPointerGrab(ctx); err != nil {
310 return err
311 }
312 } else {
313 c.cancelDrag()
314 }
315 c.pressed[heldKey(key)] = true
316 if err := c.publish(ctx); err != nil {
317 return err
318 }
319 return c.rehit(ctx)
320 }
321 // Hint mode takes every key press before binds and windows.
322 if handled, err := c.hintKey(ctx, key); handled || err != nil {
323 return err
324 }
325 // The overview takes its keys before any window; others still
326 // run binds, and are not forwarded.
327 // A launcher or a menu holding the keyboard gets them first.
328 if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
329 now := c.now()
330 var shots []viewShot
331 if key.Pressed {
332 shots = c.snapshot(now)
333 }
334 if key.Pressed && mon.overviewKey(key) {
335 if !mon.ov.open && keyName(key.Keysym) != "Escape" {
336 id, _ := mon.Focused()
337 if id != 0 {
338 c.requestFlash(flashWindow, id)
339 }
340 }
341 c.pressed[heldKey(key)] = true
342 if mon.ov.open && overviewRepeats(key) {
343 c.startRepeat(key, "")
344 }
345 c.transition(shots, now)
346 if err := c.workspaceVisible(ctx, true); err != nil {
347 return err
348 }
349 if err := c.publish(ctx); err != nil {
350 return err
351 }
352 return nil
353 }
354 }
355 // A focused window inhibiting shortcuts gets every key:
356 // no bind runs (emergency quit and VT switch are handled by
357 // input before core).
358 if c.keyboard.inhibited(c.keyboardFocus()) {
359 if held := heldKey(key); !key.Pressed && c.pressed[held] {
360 // Its press ran a bind before inhibiting began: the
361 // window never saw it.
362 delete(c.pressed, held)
363 return nil
364 }
365 if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
366 return err
367 }
368 return nil
369 }
370 name := keyName(key.Keysym)
371 // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
372 // prints exclam on US); then the physical key (code:N).
373 action, bound := c.binds[binding{key: name, mods: key.Mods}]
374 if !bound && key.Base != "" {
375 action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
376 }
377 if !bound && key.Keycode != 0 {
378 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
379 }
380 // Track presses by physical key: Shift may be released before the key.
381 held := heldKey(key)
382 if !key.Pressed {
383 consumed := c.pressed[held]
384 delete(c.pressed, held)
385 if consumed {
386 return nil
387 }
388 }
389 if key.Pressed {
390 if bound {
391 c.pressed[held] = true
392 if repeats(action) {
393 c.startRepeat(key, action)
394 }
395 return c.runBind(ctx, action)
396 }
397 c.pressed[held] = false
398 }
399 if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
400 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
401 return err
402 }
403 }
404 return nil
405}
406
407// runBind runs the action of a bound key press.
408func (c *Core) runBind(ctx context.Context, action Action) error {
409 if action == ActionScaleUp || action == ActionScaleDown {
410 return c.scaleBind(ctx, action)
411 }
412 if dir := switchDir(action); dir != 0 {
413 return c.switchBind(ctx, dir)
414 }
415 before := c.cur().mon.Current()
416 swiped := c.swipedWorkspace()
417 now := c.now()
418 // Snapshot before the switcher closes: its cards fly back to the layout.
419 shots := c.snapshot(now)
420 if c.switching() {
421 c.cancelSwitcher()
422 }
423 c.keyboard.takeBack() // a bind acts on the windows
424 effect := c.applyAction(action)
425 if effect.Quit {
426 return ErrQuit
427 }
428 if effect.Spawn {
429 if err := c.runSpawn(ctx, before, effect); err != nil {
430 return err
431 }
432 }
433 if c.swipedWorkspace() != swiped {
434 c.dropSwipe()
435 }
436 c.transition(shots, now)
437 if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
438 return err
439 }
440 if effect.Close != 0 {
441 if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
442 return err
443 }
444 }
445 if err := c.publish(ctx); err != nil {
446 return err
447 }
448 if c.dataDrag != nil {
449 // A data drag follows the layout under the still cursor.
450 return c.rehit(ctx)
451 }
452 return nil
453}
454
455// runSpawn starts the programs of a spawn effect of an action applied to w:
456// the given commands, else the configured terminal. A stash effect opens the
457// windows in the stash of w. It reports errSecurityChanged when the input
458// epoch changed meanwhile.
459func (c *Core) runSpawn(ctx context.Context, w *Workspace, effect Effect) error {
460 commands := effect.Commands
461 if commands == nil {
462 commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
463 }
464 if effect.Stash {
465 if err := c.spawnStash(ctx, w, commands, effect.Commands == nil); err != nil {
466 return err
467 }
468 } else {
469 // Explicit actions are suppressed on a full launcher queue;
470 // never keep the input owner waiting across an epoch change.
471 for _, argv := range commands {
472 if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
473 break
474 }
475 }
476 }
477 if c.inputEpochChanged() {
478 return errSecurityChanged
479 }
480 return nil
481}
482
483// spawnStash runs commands whose windows open in the stash of w, the
484// workspace the bind acted on. Wayland reads the claim tokens only while it
485// knows slots are pending, so that is published before the spawn, as
486// spawnEmpty does. A send the gate suppresses leaves no claim behind. With
487// terminal set (the terminal of an empty stash), nothing starts while the
488// previous one is still awaited.
489func (c *Core) spawnStash(ctx context.Context, w *Workspace, commands [][]string, terminal bool) error {
490 if c.protectionRequested() {
491 return nil
492 }
493 now := c.now()
494 c.placement.expireWorkspaceClaims(now)
495 if terminal && c.placement.workspacePending(w, spawnStash) {
496 return nil
497 }
498 reqs := make([]ports.SpawnRequest, len(commands))
499 for i, argv := range commands {
500 reqs[i] = c.placement.request(argv, spawnTarget{kind: spawnStash, ws: w, at: now})
501 }
502 _, err := c.dispatch(ctx, reqs)
503 return err
504}
505
506// scaleBind steps the focused output's scale and asks for it to be saved.
507func (c *Core) scaleBind(ctx context.Context, action Action) error {
508 dir := 1
509 if action == ActionScaleDown {
510 dir = -1
511 }
512 sc := c.cur()
513 prev := sc.scale
514 sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
515 c.order()
516 if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
517 // Never wait on persistence: a stalled save drops the change.
518 select {
519 case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
520 default:
521 }
522 }
523 return c.publish(ctx)
524}