Repository navigation
Make live view usable on touch devices - #448
masnwilliams wants to merge 9 commits into
Conversation
Add a gesture layer for touch input: tap clicks, one-finger swipes scroll with momentum, long-press right-clicks or drags, and two-finger pinch zooms and pans the video locally. Raise the soft keyboard from the tap when the remote cursor is a text cursor, type Android input and composition events, and send Shift for upper case letters from soft keyboards. Replace the small keyboard icon with a labeled button inside the safe area and fix the layout below 360px and under mobile browser toolbars.
…sor shapes Lift the keyboard button by the height the visual viewport loses to an overlaid soft keyboard, and only apply the classification of the newest cursor image.
The keyboard button is positioned inside the player, so lift it by the part of the player the visual viewport no longer shows.
Replace the fixed keyboard button with a draggable control that sits in a band outside the video: below it when there is vertical room, in a strip beside it when there is horizontal room, shrinking the video by up to 10% to make one. Only when there is no room does it overlay the stream as an edge-snapping handle that lets taps through while faded. The control fades when idle in every mode and its position is remembered.
Dragging kept a 44px assumption, so the wider bottom-band control jumped under the finger and landed away from the drop point. Measure the control, keep the finger's offset while dragging, and keep it out of the top and bottom safe areas.
Over a real network the cursor update for a tap usually arrives after the finger lifts, so the keyboard could only be offered through the chip. Track where the current cursor shape was seen and decide inside the tap when it lands on that spot again, such as a second tap on the same field.
A cursor image could be attributed to a newer pointer position while an update for an earlier one was still in flight, and the wait timeout used the latest pointer instead of the tap. Attribute images only when no earlier update can be pending, and pin a timed-out tap to its own point while the pointer is still there.
Pages often show another cursor before the I-beam after a click, and the first update after a tap was taken as the answer, so taps on such fields never offered the keyboard. Any text cursor within the window now offers it, only the end of the window means the tap was not on text, and the same-spot shortcut remembers where a text cursor was seen.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b978a25. Configure here.
Neko sends no cursor image when the shape does not change, so a tap on another text field while the I-beam was showing ended the wait with no update and dismissed the keyboard. Treat an unchanged I-beam at the end of the wait as text.
|
We ran into this with a real user on an iPhone (iOS Safari), on the live view that production serves today (bundle app.416afd11.js, which still has Tapping a field in the stream focuses the remote field, but the iOS keyboard never opens, and the user could not find any way to type. The small keyboard icon at the bottom-right does open it. In an emulated iPhone (Playwright WebKit, iPhone 15 Pro) the text then arrived wrong: "Hello" came through as "hello", "!" was lost, and text from autocorrect or predictive suggestions (input events only) was dropped. On the default 1920x1080 window the stream is about 393x221 px on a phone, so the fields are hard to hit too. CDP Our case: an agent drives the browser, and a person on a phone types a login and a 2FA code in the live view. This PR (tap-to-type, input/composition handling, Shift for capitals, the 16 px font) covers what we hit. Happy to test a build on a physical iPhone if that helps. |

Summary
Live view is hard to use from a phone or tablet: a one-finger swipe is sent as a mouse drag (it selects text instead of scrolling), a desktop-sized stream is too small to read or hit, and the soft keyboard only opens from a small icon. This PR adds a touch layer to the live view client. It is client-only: the remote browser still receives the same mouse, wheel and key messages it does today.
Changes
Gestures (
src/utils/touch-gestures.ts)src/utils/zoom-pan.ts), up to 3x the fitted size, or 1.5x the stream's native pixel size for large streams (at most 8x). The zoomed video can fill the letterbox bars. Double-tap resets the zoom, and a "2.5× · Reset" chip shows while zoomed. Pointer coordinates are mapped through the zoom, so taps land on the right remote pixel.Keyboard
touchendusing the remote cursor shape: neko already sends cursor images over the data channel, and Chromium shows a text (I-beam) cursor over editable fields. The pointer moves to the touch point ontouchstart, so the cursor update usually arrives before the finger lifts. If it arrives later, a "Tap to type" button appears at the tap point instead. Pages often show another cursor briefly before the I-beam after a click, so any text cursor within 800ms of the tap counts; only the end of that window means the tap was not on text. The client also remembers where a text cursor was seen, so a tap on that spot, such as a second tap on the same field, raises the keyboard inside the tap. Tapping a non-text area closes the keyboard. Seesrc/utils/cursor-shape.ts.inputand composition events, which the Guacamole keyboard ignores. These are now typed, including composition updates and backspace.src/components/touch-controls.vue) that stays off the stream whenever it can:localStorage, the control moves up above an overlaid soft keyboard (usingvisualViewport), and it shows on any touch device rather than only below 768px or with?embed.autocapitalize,autocorrectandautocompleteoff, and a 16px font so iOS does not zoom the page when it focuses.Layout
.neko-mainno longer hasmin-width: 360px, which pushed the controls off-screen in narrow embeds.100dvh, so the bottom controls are not hidden under mobile browser toolbars.viewport-fit=covermakes the safe-area insets apply.URL option
?tapKeyboard=auto(default): raise the keyboard when the tap lands on a text cursor.chip: show the "Tap to type" button there instead.always: raise it on every tap.off: keyboard button only.Desktop mouse and keyboard handling is unchanged. The new input paths only run for touch events, or on touch devices (Shift wrapping and
inputhandling). The touch-only branch infocusOverlayis removed because touch no longer synthesizes mouse events.Testing
bun test tests: 39 pass, including the newtests/touch-input.test.ts(gesture classification, zoom math and clamping, keysym and Shift mapping, composition diffing) andtests/touch-controls.test.ts(band, strip, shrink and overlay layout; drag and snap; stored position). Lint shows no new warnings compared withmain, andnpm run buildsucceeds.contextmenu.inputevents, IME composition with a keyCode 229 backspace, and key events with capitals all arrived correctly.input-event text was dropped, and capitals arrived in lower case.Not verified yet:
touchend, real Gboard behaviour, and timing over cellular networks.Known gaps
automode tapping plain paragraph text can open the keyboard.?tapKeyboard=chipis the less intrusive option.Future work
maxTouchPoints0 and a fine pointer, so pages would see touch events from a browser that says it has no touch support. Native touch should come with a consistent mobile device configuration, not on its own.Note
Medium Risk
Large client-only input refactor on the live view path; remote control still uses the same messages, but touch keyboard/cursor heuristics and new gesture code could mis-route clicks, scroll, or typing on edge cases.
Overview
Adds a touch-first live view layer in the chromium-headful client so phones/tablets can scroll, zoom, and type into the remote session without changing server wire protocol.
Gestures and zoom: Replaces touch-to-synthetic-mouse with
TouchGestures(tap click, swipe → wheel with fling, long-press right-click / drag, pinch zoom viaZoomPan). Taps map through local zoom; double-tap and a zoom chip reset zoom.Keyboard: Parses remote cursor images from the WebRTC data channel and classifies I-beam cursors to decide when to open the soft keyboard on
touchend(with?tapKeyboard=modes and a “Tap to type” chip). Handles Androidinput/IME text, auto-Shift for capitals, and a draggable keyboard control (touch-controls) with band/side/overlay layout andlocalStorageposition. Overlay textarea and layout tweaks (100dvh,viewport-fit=cover, removedmin-width: 360px) target mobile safe areas and browser chrome.Tests: New unit tests for gesture/zoom/text helpers and control layout math. Desktop mouse/keyboard paths stay as before for non-touch flows.
Reviewed by Cursor Bugbot for commit 4e938a0. Bugbot is set up for automated code reviews on this repo. Configure here.