# Buttons and gestures

What every button and every touch gesture does on every kodeOS screen, the buzzes the interface plays, and the recognizer that reads a touch.

## What it is

The Kode Dot has six buttons. Four form the **pad**: up, down, left and right. Two round buttons sit to the right of it on the front: the blue **TOP** button and, lower, the red **BOT** button. Together they are the side buttons. This page lists what each button and each touch gesture does on each screen. Every row is an action or **nothing (by design)**, which means the system chooses to do nothing for that input.

The short version:

- **The face is home.** A tap anywhere, TOP or the pad's right opens Apps. The pad's up, TOP held or a finger that drags down pulls Settings over it. BOT pressed and let go sends the face to Rest. The pad's down does nothing there.
- A pad button moves the cursor and acts when it goes down.
- **TOP goes in or confirms** and acts when you let go. Held for 0.6 s on an app, it opens the app's details. Held on the Wi-Fi or Bluetooth circle in Settings, it opens their page.
- **BOT goes back** and acts when you let go. In a folder it goes back to Apps' root, at the root to the face. On the face it sends the face to Rest. On Settings and on a page it goes back one level.
- Hold BOT for 0.6 s to go home, to Apps' root. A hold never locks: the face comes back when you go back from Apps' root, or by itself after the auto-lock time.
- On the face, hold BOT instead. From 2 s a red arc fills along the top, and at 5 s the card "Turn off?" rises.
- At Rest, TOP pressed twice wakes the face. The first press shows "Press ● again", where the blue dot is TOP and beats once a second. BOT and the pad show "Press ● twice". On a cable any button or touch wakes the face at once.
- A finger that moves more than 16 px is not a tap and acts on nothing.
- The interface buzzes lightly when something acts, refuses or arms. The Settings list sets how much.

## How it is connected

```mermaid
flowchart LR
  btn["AO_Buttons"] -- "BUTTON_EVT_SIG<br/>BUTTON_HOLD_SIG" --> gui["AO_Gui"]
  boot["AO_Boot"] -- "HOME_SIG at 0.6 s" --> gui
  touch["AO_Touch"] -- "TOUCH_EVT_SIG" --> rec["recognizer<br/>kui_gesture"]
  rec -- "tap, claim, rejection" --> gui
  console["console: btn, tap, touch, swipe"] -. "injected" .-> gui
  gui -- "kode_haptic_play()" --> hap["AO_Haptic"]
  class btn ao
  class boot ao
  class touch ao
  class gui ao
  class hap ao
  class console ext
```

*From a finger or a button to an action*

### Where inputs come from

- AO_Buttons publishes each press, release and hold of the six buttons. `sdk/kaos/buttons` has the debounce and the hold.
- AO_Boot turns a held BOT into `HOME_SIG`.
- AO_Touch publishes touch samples on change only. Every sample goes through the recognizer of the kit before LVGL reads it, and AO_Gui creates an LVGL input device in event mode so each sample triggers exactly one read.
- `gui_face_navigate()`, `gui_navigate()`, `gui_settings_navigate()` and `gui_sheet_navigate()` in `kgui_ao.c` read the buttons of their screens.
- The console's `btn`, `tap`, `touch` and `swipe` (`internals/command-set`) drive the same inputs.

An injected press starts no hold, so `btn id=bot state=press` on the face begins the count of the red arc but never produces `HOME_SIG`, and `btn id=bot state=hold` is home alone.

### A gesture only records an intent

LVGL calls a click callback inside its input processing, and a repaint there would delete objects the input device is still walking. A callback first ends its press with `lv_indev_wait_release()` where it must, then records an intent: a back (`pending_back`), a door, or a tap. The next tick applies it.

### The arc counts edges, never the hold

On the face AO_Gui times BOT itself, from its press (`bot_hold_ms`, on `khal_time_ms()`, read by `gui_off_arc_tick()`). `BUTTON_HOLD_SIG` is not an edge and leaves the count running, although it disarms every armed press. From 2,000 ms (`GUI_OFF_ARC_FROM_MS`) the red arc fills, and at 5,000 ms (`GUI_OFF_ARC_TO_MS`) with BOT still down the card "Turn off?" rises. No hold on any screen locks the board.

5 s sits far past the 0.6 s home hold and 11 s below the shortest full reset the charger can make. BOT is the BQ25896's `QON` pin, and holding it for `tQON_RST`, 16 to 23 s, with no USB cuts and closes the battery path.

## Specifications

### Words

| Word | Meaning |
| --- | --- |
| Press, release | The edges of a button going down and coming up |
| Hold | A side button down for 0.6 s (`KODE_BUTTON_HOLD_MS`); `BUTTON_HOLD_SIG` |
| Armed | A side button whose press AO_Gui has seen: the action waits for the release |
| Swallow | Receive an input and do nothing with it |
| Home | Apps' root, asked for by `HOME_SIG` |
| Rest | The dimmed look of the face (`internals/gui/face`) |
| Tap | A short touch of one finger that never goes farther than 16 px from where it landed |
| Drag, swipe | The finger moves and an object follows it, or the recognizer reads a quick move in one direction |
| Flick | A quick drag; the list goes on after the finger lifts |
| Settings pull | A drag down on Apps from the top of the glass, or on the list when it rests at its top; Settings comes down over Apps with the finger |
| Folder | A group of apps on the rail, filed by their first category |
| Details | The page of one app, opened by TOP held or a finger held on its row |
| Top band | The first 60 rows of the glass (`KGUI_TOP_BAND_PX`), where the hint stands |
| Hint | A few words in the top band, for 2 s, that say why an input did nothing |
| Claim | The moment the recognizer decides a press is a drag |
| Curtain | A panel that comes down over a screen as a picture on a screen of its own |
| Busy sheet | The write screen and the Removing card, which answer no input. The log calls them locked |
| System card | A card the system raises. It ignores every input for 700 ms and takes no drag, except Turn off |
| Door | A request to change screen (`internals/gui/overview`) |

### The face

`gui_face_navigate()` reads the buttons in `Gui_awake`, the awake look of `Gui_face`.

| Input | What happens |
| --- | --- |
| A tap anywhere, Kody and the top band included | Apps opens at its root |
| TOP, pressed and released | Apps opens at its root |
| The pad's right, pressed | Apps opens at its root |
| BOT, pressed and released before 2 s | The face goes to Rest |
| The pad's up, pressed | Settings comes down over the face |
| A drag down, from anywhere | Settings follows the finger down as a curtain |
| The pad's down, the pad's left | Nothing (by design) |
| TOP held | Settings comes down over the face. The release that follows does nothing |
| BOT held | Nothing at 0.6 s (`HOME_SIG`). From 2 s a red arc fills along the top |
| BOT held for 5 s | The arc is full and goes, and the card "Turn off?" rises |
| BOT let go between 2 s and 5 s | The arc empties in 160 ms. Nothing else happens |
| TOP and BOT together | Nothing. Two side buttons down arm neither |
| A swipe left | Apps opens at its root, sliding in from the right |
| A swipe right | Apps opens at its root, sliding in from the left |
| A swipe up | Nothing (by design). It is no tap either |
| No input for the `kody_sleep` time | The face goes to Rest |

- A tap is the recognizer's (`gui_face_tap()`): a press that travelled more than 16 px, or that the recognizer claimed, opens nothing. A tap on Kody does not make him look.
- The press of a side button arms it and the release acts. The hold disarms the press it grew out of.
- BOT posts the door to Rest on the release, because the answer refuses while a button is held. The face goes to Rest only when it is bare: no card, no curtain, no finger, no held button and no write.
- The recognizer claims a drag down that starts anywhere on the face. A swipe left or right opens Apps, and a swipe up is a `zone` rejection and no tap.
- `gui_off_arc_tick()` counts the hold from the press: the arc fills toward 5 s, the hold is spent with a `mark` buzz when the card rises, and the hold disarms the press, so the release opens nothing.
- Kody looks when the face comes back from Apps or from Settings, not on an input of the face.
- The scene of Kody plays on while the face is seen. When the `kody_sleep` time has gone by, the face goes to Rest. A finger on the screen, a held button, a sheet, a moving screen, a running write and a look of Kody hold the count.

### The face at Rest

At Rest the face is dimmed. The state is `Gui_rest`. Nothing a person does reaches a screen from it. On the battery only TOP, pressed twice, wakes it. On a cable any button or touch wakes it at once.

| Input | What happens |
| --- | --- |
| TOP, pressed | The knock: the glass lights and "Press ● again" stands in the date's place. Its dot is Kody Blue, the colour of TOP, and beats once a second (`KGUI_KNOCK_BEAT_HALF_MS`, 495 ms each way). Left alone for 5.94 s (`KGUI_PEEK_MS`), the face rests again |
| TOP, pressed again during the knock | The face wakes on the press. The log says `door: rest -> face (top pressed twice)` |
| BOT or a pad button, pressed, on the battery | "Press ● twice" in the date's place, for 5.94 s. Nothing wakes |
| A touch on the battery: a tap, a swipe or a drag | Nothing, and nothing shows. The touch counts as `blocked` |
| Any button, on a cable | The face wakes on the press, and its release is swallowed. The log says `door: rest -> face (button on a cable)` |
| A touch on a cable | The face wakes on the press. The log says `door: rest -> face (touch on a cable)` |
| A system card: a pairing, a write, its failure, a critical battery | The card rises over the face and its arrival lights the screen |
| No input after a wake | The face rests again after 60 s (`KGUI_WOKEN_REST_MS`) |

`Gui_rest` records the waking button in `wake_btn`, and `Gui_awake` swallows the rest of that press (`gui_wake_swallow()`). `Gui_rest` takes every touch sample, so LVGL and the recognizer never see it, and the log writes no `gesture:` line. A touch that is down when the face wakes stays swallowed until the finger lifts.

### Apps

The launcher is the rail of folders and apps (`internals/gui/launcher`). `gui_navigate()` reads the buttons, in `Gui_browsing`.

| Input | What happens |
| --- | --- |
| The pad's up or down | The selection moves one row and the rows roll under the focus line. No wrap; at an end the list gives a little and comes back. On an empty list, nothing |
| The pad's right | Goes in, as TOP does |
| The pad's left | Goes back: out of the open folder to the root, and from the root to the face |
| TOP, pressed and released | Goes in on the selected row: a folder opens, an installed app launches, a file is written and then launched. On an empty list, Kody's door works |
| TOP held | The details of the selected app open. A folder's row has none. The release that follows launches nothing |
| BOT, pressed and released | Goes back, as the pad's left does |
| BOT held | Home at 0.6 s: an open folder closes and Apps stays at its root. Nothing fills and nothing locks |
| A tap on a row | Goes in on the row, as TOP does. A tap on a row whose icon is not wholly inside the rail only moves the focus to it |
| A finger held on a row for 0.6 s | The details of an app open. A folder's row has none, and its lift is no tap either |
| A tap on Kody, on the empty launcher | Kody's door |
| A vertical drag or flick | The list follows the finger one to one and glides to the row the finger's speed reaches, at most five rows on. The row on the focus line is the selection |
| A press on a list that moves | The list stops where it is. The click of that press acts on nothing |
| A drag down from the top 44 rows of the glass, or on a list at rest at its top (the Settings pull) | Settings comes down over Apps with the finger. Letting go past halfway opens it, and closing it goes back to Apps |
| A swipe right | Goes back, as BOT does |
| A swipe left at the root | Goes to the face, which comes in from the right. In a folder, nothing (by design) |
| 20 s with no input, with the QR code showing | Kody comes back |
| No input for the auto-lock time (1 min by default) | The auto-lock: the face comes down over Apps |

- A pad button that stays down repeats: nothing for 400 ms (`KGUI_PAD_HOLD_MS`), then one row every 115 ms, and after 8 repeats two rows each time. At an end the repeat stops.
- Kody's door is the empty launcher's switch between Kody and the QR code. It changes no screen and does nothing while "Looking for apps" shows or Kody slides.
- `HOME_SIG` closes an open folder (`gui_level_home()`) and leaves Apps at its root. The release goes back nowhere, because the hold disarmed the press. The hint `already home` shows only for a hold that counts for nothing, such as `btn id=bot state=hold` with no press.
- A swipe right is claimed 12 px from the landing; the level goes back when the finger lifts, if it went 40 px along the claim (`KGUI_SIDE_STEP_PX`). A shorter slide is no tap and goes nowhere. It works on the list and on the empty box alike.
- A drag down that starts in the top 44 rows (`KGUI_LIST_PULL_BAND`) always pulls Settings, wherever the list stands. Lower down it pulls Settings only when the press landed on the list, the first row is the selection, the list is at rest there and nothing has moved it for 500 ms (`gui_zone_lock_pull()`). If one fails, the drag moves the list.
- The auto-lock does not happen while a sheet is up, a finger rests, a button is held, a write runs or a screen moves.

**A launch is one gesture.** `gui_launch_selection()` has two callers: `gui_level_enter()` on an app row, and **Run again** on the Stopped card. TOP and a tap on an app row launch it at once, with no card between. Remove sits behind the details and behind a question of its own, so the one act that cannot be undone asks first.

### The settings screen

The control centre. `gui_settings_navigate()` reads the buttons, in `Gui_settings`.

| Input | What happens |
| --- | --- |
| The pad's left or right | The ring walks the top row and stops at its ends, and on the second row goes between the battery and Turn off. On a level, it steps the brightness or the volume by 5 % |
| The pad's up or down | The ring walks the controls: down from Wi-Fi to the battery, from Settings to Turn off, from Bluetooth, the battery or Turn off to the brightness level, and from the brightness to the volume. Up is the same walk back. Nothing wraps |
| TOP, pressed and released | The control with the ring acts: Wi-Fi and Bluetooth switch the radio, Settings opens the Settings page, the battery opens the Battery page, Turn off asks "Turn off?". A level does nothing |
| TOP held, on Wi-Fi or Bluetooth | The page of the radio opens. Elsewhere, nothing |
| BOT, pressed and released | Back to the face. The screen goes up as a curtain |
| BOT held | Home: Apps' root, with no rescan |
| A tap on a circle | The ring moves there and the circle acts as TOP does |
| A finger held on the Wi-Fi or Bluetooth circle | The page of the radio opens. The lift does not switch the radio |
| A press or a drag on a level | The level follows the finger and is stored when the finger lifts |
| A swipe up, anywhere but on a level | Back to the face. The screen follows the finger up |
| A drag or a flick in any other direction | Nothing (by design) |
| No input for the auto-lock time | The auto-lock: the face comes down over Settings |

### A page

Wi-Fi, Bluetooth, Settings, Screen, Battery, About, Kody, Self-test and the two lists of choices. `gui_sheet_navigate()` and `gui_sheet_body()` read them.

| Input | What happens |
| --- | --- |
| The pad's up or down | The ring moves one row, wrapping |
| The pad's left or right | The value of the row under the ring changes by one step. A row with no value does nothing |
| TOP, pressed and released | The row under the ring acts |
| TOP held | On the Wi-Fi page: forgets the network under the ring, when the board remembers it. Elsewhere nothing |
| BOT, pressed and released | The page goes back one level: a page to Settings, Settings, Wi-Fi, Bluetooth and the battery's Battery page to the control centre, a list of choices to its page |
| BOT held | Home. The settings screen goes back to Apps' root and the page goes with it. A busy sheet ignores the hold |
| A tap on a row | The ring moves to the row and the row acts |
| A press or a drag on a value row | The ring moves to the row and the level follows the finger |
| A swipe right on a page | The page goes back one level. A swipe that starts on the brightness bar is the bar's |
| A drag or a flick on the body | A page longer than the card scrolls and settles on a whole row. On Wi-Fi and Bluetooth the switch stays at the top |
| A touch on the blocker while the card slides | Nothing (by design) |

Rows with no value, where left and right do nothing: every row of the Wi-Fi, Bluetooth, Battery and About pages, the Auto-lock row, and every choice in a list. On the Settings list, Sleep after, Kody's scene and Vibration step through their values and wrap, and the switches While charging, Header 3.3 V and Header 5 V have no step. TOP or a tap changes any of them where it stands.

### The keypad

The pad moves the focus over the controls of the card, and TOP presses the control with the focus. The ring marks the focus.

| Input | What happens |
| --- | --- |
| The pad's up or down | The focus moves one row: the yellow pill, the Show password row, and four rows of keys. It wraps |
| The pad's left or right | The focus moves one key along a row, wrapping. On the pill and the Show password row, nothing |
| TOP, pressed and released | The control with the focus is pressed |
| TOP held | Nothing (by design) |
| BOT, pressed and released | The keypad closes back to the Wi-Fi page. What was typed is wiped |
| BOT held | Home, as for any card. What was typed is wiped |
| A tap on a control | The focus moves to the control and the control is pressed |
| A drag down on the header, the top 92 rows of the card | The card follows the finger and closes. What was typed is wiped |
| A drag on the field or on the keys | Nothing (by design) |

Down from the pill or from the Show password row lands on the column where the focus last stood. The Show password row is skipped for a hidden network's name. While the join is out, the focus still moves, the pill is dimmed and no control acts.

### A system card

The pad does nothing on a system card. One table serves them all, and the last column says where a card differs.

| Input | What happens |
| --- | --- |
| Any button or touch in the first 700 ms after the card began to rise | Nothing (by design). A press that began under the gate answers nothing even if its lift comes later |
| TOP released, on Didn't open or Stopped | The answer button acts: Try again or Run again |
| TOP released, or a tap, on Turn off | The red button "Turn off" confirms. On USB the button is grey and the board does not stop |
| BOT, pressed and released | The card closes. On Turn off it answers no. On the pairing code it refuses the passkey. On the write screen it does nothing |
| BOT held | Home, as for any card. Home closes a card that can be closed, over the face too, and the face stays the face. It is refused inside the gate, for a press that began before the card, and over the write screen |
| A tap on the button of Didn't open or Stopped | The answer button acts |
| A swipe down on Turn off | Answers no, at the lift |
| A drag, a swipe or a tap anywhere else | Nothing (by design). The recognizer has no zone on a system card |

A hold that reached its end acts once; the Ring stays full and takes no second hold until the card is closed.

### The details

The details of an app open over Apps. `gui_details_navigate()` reads the buttons while they are up.

| Input | What happens |
| --- | --- |
| The pad | Nothing (by design) |
| TOP released | The Remove pill acts, for an installed app: it asks its question. On the question, the red **Remove** erases the app |
| TOP held | Nothing (by design). It opens no Settings |
| BOT released | The card closes. On the question of Remove it answers no and closes both cards |
| BOT held | Home: the card closes and Apps stays at its root |
| A tap on the pill | The pill acts, as TOP does |
| A drag down from anywhere on the card | The card follows the finger down and closes. It closes when the finger lifts past halfway, or faster than 800 px/s |
| Any touch on the Removing card | Nothing (by design). It is locked |

The hold that opened the details disarmed the press it grew out of, so its release is no answer. The question takes no answer for 400 ms after it is written.

### The guided tests and the self-test

During a guided factory test the operator presses what a script asks for. `Gui_btn_test` and `Gui_tp_test` swallow `BUTTON_EVT_SIG`, `BUTTON_HOLD_SIG`, `HOME_SIG` and every door, because AO_Buttons, AO_Touch, AO_LedMgr and AO_Display score those very presses. Every input does nothing (by design).

While a run of the Self-test page lasts, AO_Gui is in `Gui_selftest` on the loading screen.

| Input | What happens |
| --- | --- |
| TOP at the hold | After "Now we need you", the run goes on to the rows that need hands |
| BOT before the hold or at it | The run is asked to stop after the check in flight and the line reads "Stopping after this check" |
| BOT after the hold | Nothing from AO_Gui. The row in flight reads the press as its answer |
| Any other press, a hold of either button, HOME, a door | Nothing (by design) |

### Haptics

The interface buzzes at the moments that act, refuse and arm. The buzz is the LRA motor on the antenna board, which AO_Haptic drives. `kgui_haptic_play(effect)` in `kgui_haptic.c` is the one call every moment makes. It applies the level, keeps silent while a recording would pick the buzz up, spaces the step, and asks AO_Haptic by event with `kode_haptic_play()`.

The setting `haptic_ui` sets how much of the interface buzzes: `off`, `light` or `normal`, default `light`. The Vibration row of the Settings list sets it too.

| Level | What plays |
| --- | --- |
| `off` | Nothing |
| `light` | click, thud, tick and mark |
| `normal` | Light, and a step each time the focus moves, at most 14 a second (`KGUI_HAPTIC_STEPS_PER_S`) |

| Effect | Length | Strength | Means |
| --- | --- | --- | --- |
| click | 12 ms | 60 % | Something acted |
| thud | 25 ms | 80 % | A limit was reached, or an input was refused |
| tick | 6 ms | 40 % | A pull passed the point where letting go commits, or a hold began to fill its Ring |
| mark | 18 ms | 70 % | A hold reached its end and did something |
| step | 6 ms | 40 % | The focus moved one place (level `normal` only, at least 72 ms apart) |

The moments:

- **click:** a circle acts; Apps goes in on a row; the one answer of a card; a row of a page that acts; the Auto-lock row and each change of a Screen setting; a key of the keypad that typed, deleted or changed the mode; Kody leaves for the QR code and comes back.
- **thud:** the pad past the first or last row, and a finger that takes the list past an end, once for each pull; a key the full text refuses; BOT held where home is refused.
- **tick:** a settings pull passes the point where letting go commits; a card taken by its header passes the point where letting go closes it; a hold begins on a hold card, and the red arc starts to fill at 2 s.
- **mark:** BOT held for 0.6 s where home does something; a hold fills its Ring; the arc is full at 5 s; TOP held opens the details or the page of the Wi-Fi or Bluetooth circle.
- **step:** the pad moves the selection of Apps, the controls of Settings or the ring between the rows of a page; a tap on a row not yet selected; left or right steps the brightness bar.

A buzz that is not played is counted and said at debug level with the reason: `haptic: <effect> held back (<reason>)`. The reasons are `level` (the level does not play this effect), `recording` (the microphone or the IMU records), `pace` (a step came less than 72 ms after the last) and `AO_Haptic is busy`, `no event in the pool` or `AO_Haptic refused it`. The silence for a recording follows the rows of AO_Autotest alone: `AUTOTEST_ROW_SIG` raises it at the start of the audio, IMU or haptic row and the verdict lowers it. A recording nobody started through a row is not seen.

`config key="haptic"` is a percentage AO_Haptic applies to every gain it is asked for, the interface's included, so `haptic=0` silences the interface as well. `haptic_ui=off` is the setting meant for the interface alone.

`gui_info` ends its interface line with `ui: haptics <level> (silenced by <nothing|the microphone|the IMU|both>), motion <word>, awake on USB <on|off>; <n> buzzes asked, <m> held back`. The bench board 6c:0a has no AW86233, so its evidence is the count. While a secret is typed, kgui writes no `haptic:` line for a buzz, but the counts still count.

### The four rules

The rules behind every table above.

| Rule | Meaning |
| --- | --- |
| 1. The pad acts at the press, the side buttons at the release | Seeing a side button's press arms it. A hold disarms the press it grew from. A side button acts only when it has seen both edges, so a stray edge, such as electrical noise on GPIO35 that the 30 ms debounce confirmed, never acts alone. AO_Gui times no press; AO_Buttons declares the hold |
| 2. A swallowed input disarms the press behind it | Pressing the other button, a finger landing, a hold, a screen leaving the glass, a curtain starting, a sheet opening, a rescan and a busy sheet's refusal all call `gui_press_disarm()`. On the face and Apps a pad button and the other side button disarm; on Settings only the other side button; on a sheet no button |
| 3. A cursor that cannot move changes nothing | Apps refuses at its ends and stops the repeat. A ring that would wrap onto itself stays |
| 4. Only a tap acts, and a press that travels is not a tap | Every click handler that acts asks `gui_click_is_tap()`: an Apps row, a settings circle, a page row, a keypad control, the answer button of a card, and Kody on the empty launcher. A press is a tap when the finger never went farther than 16 px from where it landed and the recognizer did not claim it. LVGL's own distance plays no part |

A press that is not a tap acts on nothing. It logs `not a tap: the press on <what> was <outcome>; nothing acts` at debug level, with the recognizer's word: `claim`, `diagonal`, `axis`, `zone` or `blocked`.

A held button counts as input for as long as it is down. Nothing publishes while a hand rests on one, so the face, Apps, Settings and any card over them track which buttons are down (`btn_held`, `gui_btn_track()`), and the auto-lock and the rest hold their counts meanwhile.

### The recognizer

Every touch sample goes through the recognizer of the kit (`kui_gesture`, `sdk/kui`) before LVGL reads it. The recognizer decides what a press is, from where the finger has gone, and it owns every door and every tap: the curtains a finger pulls or catches, the card a finger takes down by its header, the way back a swipe right makes, the Settings pull over Apps, and the tap on the face and on every row, circle and button. LVGL keeps one thing: the scroll of the list page of a card. The rail of Apps is the interface's own, so no LVGL scroll is involved.

Each sample is measured from the point where the finger landed, never from the sample before, so the answer does not depend on how often the touch sensor reports.

| Outcome | Rule |
| --- | --- |
| Tap | The finger never went farther than 16 px (`KUI_GESTURE_TAP_PX`) from the landing, on either axis, and then lifted |
| Claim | The first sample 12 px or more from the landing (`KUI_GESTURE_CLAIM_PX`) decides the press. The farther axis must lead the nearer one by at least 1.5:1, and the zone must take that direction. The claim keeps its axis |
| `diagonal` | The axes are under 1.5:1 |
| `axis` | The zone takes other directions only |
| `zone` | The press landed where no drag is born |
| `blocked` | Input was shut when the finger landed. A touch at Rest on the battery counts here too |

A rejection is noted at first and becomes the state only when the finger goes farther than 16 px, so a sloppy tap still works where nothing drags. The input device of AO_Gui starts a scroll one pixel after the claim distance, at 13 px, so the recognizer has decided before a scroll can begin.

`gui_gesture_zone()` says what a press may become. It reads the screen, the card over it and the landing point on the press edge, once.

| Where the press lands | Directions it takes | What a claim is for |
| --- | --- | --- |
| A guided test, or the locked card of a running write | None: blocked | - |
| A curtain, while its spring settles | Up and down | A catch: close, settings or unlock |
| A curtain a finger holds, or one that has landed and waits | None: blocked | - |
| The Turn off card | Down | `dismiss`: a swipe down answers no, at the lift |
| Any other system card | None | - |
| A card that a finger or a spring has, or that slides | None: blocked | - |
| A settings page | Right | `back`: one level. A press on the brightness bar takes no drag |
| A card's header (top of the card to the foot of the title disc; the keypad's 92 rows; all of the details) | Down | `sheet`: the card follows the finger |
| A card, anywhere else | None | A list page scrolls under LVGL |
| The face | Down | `settings`: the curtain. A swipe up or sideways is `zone` |
| The face at Rest | None: `Gui_rest` swallows the touch first | The press counts as `blocked` |
| Apps, with rows | Up and down; right, and left at the root | `scroll`, the Settings pull for down from the top 44 rows or when its conditions hold, and `level` sideways: back |
| Apps, while the rows slide, or with no rows | None; right only | Back to the face |
| The settings screen | Up | `close` |

**The top band.** The first 60 rows of the glass are the top band. The corners are cut away, so at each row the band starts where the glass does. The band draws nothing of its own and takes no press: the content may stand in it, and a tap there is the content's tap. The hint stands in it. The face's drag down starts anywhere on it, and over a card a drag down from the band is the card's.

At the end of every press AO_Gui logs one line at debug level (`gui_gesture_end()`), such as `gesture: claim up scroll at 205,400, 0,-180 px` or `gesture: tap at 205,300, 0,0 px`. While a secret is typed, every gesture line is written without coordinates. The list logs its own lines at INFO level: `gesture: caught the list at <p>`, `gesture: list released at <v> px/s -> row <r>`. A curtain logs `gesture: caught the curtain at <p>%` and `gesture: release at <p>% <v> px/s -> open|back`, and a card logs `gesture: card release at <p>% <v> px/s -> stay|close`.

**The counters.** `gui_info` reports `>|gesture`. The counters count presses since boot and stop at 65535:

- `taps`, `claims` and the four rejections `diagonal`, `axis`, `zone` and `blocked`.
- `commits`: curtains and cards a finger followed that landed on the far side. `shorts` counts the ones that went back to where they started. `commits` + `shorts` never exceeds `claims`.

## Gotchas

> **WARNING**
>
> **A swipe that starts on a button fires it.** LVGL keeps the object a press landed on pressed while the finger slides off (`LV_OBJ_FLAG_PRESS_LOCK`) and sends a click at the release unless something scrolled. A card, a circle and a list that cannot scroll scroll nothing. Without rule 4, a sideways swipe across Remove would remove the app, across a zone would change the zone, and across Turn off would turn the board off. `lv_indev_get_press_moved()` is no substitute: LVGL sets it only when one sample moves past the limit, so a slow drag never sets it.

> **NOTE**
>
> **One press does nothing and the log says "two side buttons down".** A swallowed input left a press armed. The refusal of a busy sheet returns before `gui_navigate()`, so nothing clears `bot_armed` or `top_armed`, and the next TOP press reads the stale flag as two side buttons down. Any code that swallows a button calls `gui_press_disarm()`.

> **NOTE**
>
> **Holding BOT over a card goes home.** At 0.6 s HOME closes the card or returns to Apps' root, and holding on does nothing more. A busy write screen swallows HOME.

> **NOTE**
>
> **`btn` from the console is not a thumb.** An injected press starts no hold, so `btn id=bot state=press` on the face begins the count of the red arc but never produces HOME, and `btn id=bot state=hold` is home alone. Send the press, wait, and send the release in one session.

## Example code

Walk the screens with the buttons:

1. Boot the Kode Dot. The face shows. Press TOP and let go. Apps slides in from the right.
2. Press BOT and let go. The face slides back in from the left. Press the pad's up button. Settings comes down over it. Press BOT to close it.
3. Press the pad's right button. Apps opens. Move the pad's down button until an app is in focus, then hold TOP for a moment. The details of the app open. Press BOT to close them.
4. On the face, press BOT and let go. The face dims. Press BOT: "Press ● twice" shows. Press TOP, then TOP again, to wake the face.
5. On the face, press and hold BOT. From 2 s a red arc fills along the top, and at 5 s "Turn off?" rises. Press BOT to answer no.

## In the SDK

- [`kgui_ao.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_ao.c): the button maps, `gui_gesture_zone()`, `gui_off_arc_tick()` and `gui_click_is_tap()`.
- [`kgui_haptic.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_haptic.c): the level, the silences and the pace of the buzzes.
- [`kui_gesture.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kui/drag/include/kui_gesture.h): the recognizer.
- [`kqp_config.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kqp/include/kqp_config.h): `KODE_BUTTON_HOLD_MS` and the other timing constants.
- [`components/kaos/buttons`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/buttons): AO_Buttons, the owner of the six buttons.
