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 forwardsSwipe(v.Fingers) {
30 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
31 }
32 if err := c.gestureAbort(ctx, v.Time); err != nil {
33 return err
34 }
35 if c.swipeBegin(v) {
36 return c.slid(ctx, false)
37 }
38 return nil
39 case ports.SwipeUpdate:
40 if c.gesture.on {
41 return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time})
42 }
43 before := c.cur().mon.Current()
44 if c.swipeUpdate(v) {
45 return c.slid(ctx, c.cur().mon.Current() != before)
46 }
47 return nil
48 case ports.SwipeEnd:
49 if c.gesture.on && c.gesture.kind == ports.GestureSwipe {
50 return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time)
51 }
52 return c.slid(ctx, c.swipeEnd(v))
53 case ports.PinchBegin:
54 return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time)
55 case ports.PinchUpdate:
56 return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time})
57 case ports.PinchEnd:
58 return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time)
59 case ports.HoldBegin:
60 return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time)
61 case ports.HoldEnd:
62 return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time)
63 case ports.KeyEvent:
64 return c.keyEvent(ctx, v)
65 }
66 return nil
67}
68
69func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
70 c.motionTime = v.Time
71 // A locked pointer stays still; relative motion still flows.
72 if c.constraint.Mode != ports.ConstraintLock {
73 c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
74 }
75 // The focused screen follows the pointer, so new windows
76 // and launchers open where the user is.
77 // Only a pointer entering another output switches: keyboard
78 // moves to another screen stick until then. Not mid-drag of a
79 // client; a window drag (drag.go) follows the pointer.
80 if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
81 name := c.screens[i].name()
82 c.pointerOutput = name
83 if name != c.cur().name() {
84 c.focusScreen = i
85 if err := c.publish(ctx); err != nil {
86 return err
87 }
88 }
89 }
90 if c.drag != nil {
91 // The dragged window and the others get no pointer events.
92 return c.dragMotion(ctx, v.Time)
93 }
94 id, x, y := c.hit(c.cursorX, c.cursorY)
95 // Layout changes are intentionally re-hit-tested only on motion.
96 if id != c.pointer {
97 c.pointer = id
98 if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
99 return err
100 }
101 }
102 c.pointerAt = [2]float64{x, y}
103 if id != 0 {
104 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 {
105 return err
106 }
107 }
108 return nil
109}
110
111func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
112 // Buttons a drag took stay in c.buttons until released, so the
113 // security gate admits their release; no client sees it.
114 if !v.Pressed && c.swallow[v.Button] {
115 delete(c.swallow, v.Button)
116 delete(c.buttons, v.Button)
117 return nil
118 }
119 if d := c.drag; d != nil {
120 if !v.Pressed && v.Button == d.button {
121 delete(c.buttons, v.Button)
122 return c.endDrag(ctx)
123 }
124 // Other buttons do nothing mid-drag; their releases neither.
125 if v.Pressed {
126 c.buttons[v.Button] = true
127 c.swallow[v.Button] = true
128 }
129 return nil
130 }
131 if v.Pressed && c.switching() {
132 if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
133 return err
134 }
135 }
136 if started, err := c.startDrag(ctx, v); err != nil || started {
137 return err
138 }
139 if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
140 if picked, err := c.overviewClick(ctx); err != nil {
141 return err
142 } else if picked {
143 return nil
144 }
145 }
146 id := c.pointer
147 if c.grab != 0 {
148 id = c.grab
149 }
150 if v.Pressed && c.held() == 0 {
151 // A click outside an open menu closes it.
152 if err := c.dismissGrabs(ctx, id); err != nil {
153 return err
154 }
155 }
156 if v.Pressed {
157 if c.held() == 0 {
158 c.grab = id
159 }
160 c.buttons[v.Button] = true
161 c.lastButton = v.Button
162 } else {
163 delete(c.buttons, v.Button)
164 }
165 if id != 0 {
166 if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
167 return err
168 }
169 // A click on an on-demand layer gives it the keyboard.
170 if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
171 if err := c.publish(ctx); err != nil {
172 return err
173 }
174 } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
175 // A click anywhere else takes the keyboard back.
176 c.keyboard.takeBack()
177 if err := c.publish(ctx); err != nil {
178 return err
179 }
180 }
181 // A click focuses the window and its output.
182 s, w := c.screenOf(id)
183 if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
184 now := c.now()
185 before := c.snapshot(now)
186 w.Click(id)
187 c.focusScreen = c.screenIndex(s.name())
188 c.transition(before, now)
189 if err := c.publish(ctx); err != nil {
190 return err
191 }
192 }
193 }
194 if c.held() == 0 {
195 c.grab = 0
196 }
197 return nil
198}
199
200func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
201 c.scrollStop(v)
202 // Only wheel/continuous scrolling navigates the overview.
203 // Two-finger scrolling belongs to clients, never to navigation.
204 if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
205 // overviewScroll owns which scrolls step; the shot costs no
206 // allocation once warm.
207 now := c.now()
208 shots := c.snapshot(now)
209 if c.cur().mon.overviewScroll(v) {
210 c.transition(shots, now)
211 if err := c.workspaceVisible(ctx, true); err != nil {
212 return err
213 }
214 if err := c.publish(ctx); err != nil {
215 return err
216 }
217 }
218 return nil
219 }
220 // Scroll goes to the window under the pointer, which has the
221 // pointer focus even mid-drag. None during a window drag.
222 if c.pointer != 0 && c.drag == nil {
223 if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
224 return err
225 }
226 }
227 return nil
228}
229
230func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
231 c.mods = key.Mods
232 // Releasing the command key commits the switcher; the release still
233 // goes on to the bind lookup and the clients.
234 if c.switching() && c.mods&c.cmdMod == 0 {
235 if err := c.releaseSwitcher(ctx); err != nil {
236 return err
237 }
238 }
239 if c.switching() && key.Pressed && key.Keysym == "Escape" {
240 // Escape cancels the switcher; its release goes nowhere either.
241 c.pressed[heldKey(key)] = true
242 return c.abortSwitcher(ctx)
243 }
244 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
245 // Escape cancels the drag; its release goes nowhere either.
246 c.cancelDrag()
247 c.pressed[heldKey(key)] = true
248 if err := c.publish(ctx); err != nil {
249 return err
250 }
251 return c.rehit(ctx)
252 }
253 // The overview takes its keys before any window; others still
254 // run binds, and are not forwarded.
255 // A launcher or a menu holding the keyboard gets them first.
256 if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
257 now := c.now()
258 var shots []viewShot
259 if key.Pressed {
260 shots = c.snapshot(now)
261 }
262 if key.Pressed && mon.overviewKey(key) {
263 c.pressed[heldKey(key)] = true
264 c.transition(shots, now)
265 if err := c.workspaceVisible(ctx, true); err != nil {
266 return err
267 }
268 if err := c.publish(ctx); err != nil {
269 return err
270 }
271 return nil
272 }
273 }
274 // A focused window inhibiting shortcuts gets every key:
275 // no bind runs (emergency quit and VT switch are handled by
276 // input before core).
277 if c.keyboard.inhibited(c.keyboardFocus()) {
278 if held := heldKey(key); !key.Pressed && c.pressed[held] {
279 // Its press ran a bind before inhibiting began: the
280 // window never saw it.
281 delete(c.pressed, held)
282 return nil
283 }
284 if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
285 return err
286 }
287 return nil
288 }
289 name := keyName(key.Keysym)
290 // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
291 // prints exclam on US); then the physical key (code:N).
292 action, bound := c.binds[binding{key: name, mods: key.Mods}]
293 if !bound && key.Base != "" {
294 action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
295 }
296 if !bound && key.Keycode != 0 {
297 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
298 }
299 // Track presses by physical key: Shift may be released before the key.
300 held := heldKey(key)
301 if !key.Pressed {
302 consumed := c.pressed[held]
303 delete(c.pressed, held)
304 if consumed {
305 return nil
306 }
307 }
308 if key.Pressed {
309 if bound {
310 c.pressed[held] = true
311 return c.runBind(ctx, action)
312 }
313 c.pressed[held] = false
314 }
315 if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
316 if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
317 return err
318 }
319 }
320 return nil
321}
322
323// runBind runs the action of a bound key press.
324func (c *Core) runBind(ctx context.Context, action Action) error {
325 if action == ActionScaleUp || action == ActionScaleDown {
326 return c.scaleBind(ctx, action)
327 }
328 if dir := switchDir(action); dir != 0 {
329 return c.switchBind(ctx, dir)
330 }
331 before := c.cur().mon.Current()
332 swiped := c.swipedWorkspace()
333 now := c.now()
334 // Snapshot before the switcher closes: its cards fly back to the layout.
335 shots := c.snapshot(now)
336 if c.switching() {
337 c.cancelSwitcher()
338 }
339 c.keyboard.takeBack() // a bind acts on the windows
340 effect := c.applyAction(action)
341 if effect.Quit {
342 return ErrQuit
343 }
344 if effect.Spawn {
345 commands := effect.Commands
346 if commands == nil {
347 commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
348 }
349 // Explicit actions are suppressed on a full launcher queue;
350 // never keep the input owner waiting across an epoch change.
351 for _, argv := range commands {
352 if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
353 break
354 }
355 }
356 if c.inputEpochChanged() {
357 return errSecurityChanged
358 }
359 }
360 if c.swipedWorkspace() != swiped {
361 c.dropSwipe()
362 }
363 c.transition(shots, now)
364 if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
365 return err
366 }
367 if effect.Close != 0 {
368 if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
369 return err
370 }
371 }
372 return c.publish(ctx)
373}
374
375// scaleBind steps the focused output's scale and asks for it to be saved.
376func (c *Core) scaleBind(ctx context.Context, action Action) error {
377 dir := 1
378 if action == ActionScaleDown {
379 dir = -1
380 }
381 sc := c.cur()
382 prev := sc.scale
383 sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
384 c.order()
385 if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
386 // Never wait on persistence: a stalled save drops the change.
387 select {
388 case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
389 default:
390 }
391 }
392 return c.publish(ctx)
393}