Three-finger tap still works as a power-user shortcut, but now there's a
small glass chevron pill at the top center that flips between chevron.up
(hide toolbar) and chevron.down (show toolbar) based on chrome state.
Discoverable and reachable from one hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Class-level UIKeyInput conformance without UITextInputTraits means iOS
falls back to default traits — autocorrect on, predictive on, smart
quotes/dashes on. The suggestion engine was swallowing most keystrokes
before insertText() could forward them (the "1 in 6 chars" symptom).
Declaring all the traits as @objc stored properties with permissive
(autocorrect=.no, etc.) values turns every suggestion layer off so each
key tap produces exactly one insertText() and hits the remote.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VNCConnection.Settings.inputMode looks like it configures keyboard-shortcut
forwarding on macOS, but in RoyalVNCKit's enqueue path it's a master gate:
// VNCConnection+Queue.swift
guard settings.inputMode != .none else { return } // every path
So every PointerEvent and KeyEvent we enqueued was discarded before hitting
the wire. The Mac received zero input even though the framebuffer was live.
Frames streamed because that queue is server→client, not gated by inputMode.
Fix: pass .forwardKeyboardShortcutsEvenIfInUseLocally. On iOS we have no
local keyboard shortcuts to steal from, so the most permissive value is
safe and it unblocks the input queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a connection is configured with viewOnly=true, the controller silently
drops every key, pointer, and scroll event. Without a visible indicator the
behavior looks like the keyboard is broken. Yellow capsule next to the
connection label makes it obvious.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier UIKeyInput conformance was declared in a separate extension. ObjC
protocol conformance via Swift extension is fragile when the protocol
inherits another @objc protocol (UIKeyInput inherits UITextInputTraits) — the
runtime didn't always pick up insertText:, so the on-screen keyboard came
up but characters never reached controller.type(_:).
Fix: declare UIKeyInput conformance directly on FramebufferUIView's class
declaration, with insertText / deleteBackward / hasText as native members.
Also caught and fixed by the new UI test:
- The toolbar's keyboard-icon button had a 20×13 hit region (SF Symbol size)
even though the visual frame was 34×34 — XCUI taps couldn't land on it
reliably. .contentShape(Rectangle()) widens the hit area to the frame.
- accessibilityValue is reserved by iOS for UIKeyInput-classed views (treats
them as TextView), so a separate hidden "fb-diag" accessibility probe
records keyboard plumbing events for the test to verify.
Tests:
- KeyboardInputTests (5): pure mapping from String → X11 keysym down/up pairs
- ScreensUITests.testSoftwareKeyboardSendsCharactersToFramebuffer:
opens a session, taps the keyboard toggle, types "hi" via the system
keyboard, and asserts the framebuffer's diagnostic probe records
[ins:h] and [ins:i] — proving the chars reach controller.type(_:)
- A SwiftUI state probe (sessionview-state) verifies the binding flips,
which guards against future tap-routing regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toolbar's keyboard icon used to toggle a custom function-key bar — it
never opened the iOS system keyboard, so users couldn't type into the remote.
Fix: FramebufferUIView now conforms to UIKeyInput + UITextInputTraits, so
becoming first responder presents the iOS keyboard. Tapping the keyboard
button toggles software-keyboard visibility on the framebuffer view via a
SwiftUI binding. While the keyboard is up, an inputAccessoryView toolbar
(esc / tab / ctrl / ⌘ / ⌥ / ←↓↑→ / dismiss) sits directly above it, with
each button forwarded to the existing controller.send… APIs.
The standalone SoftKeyboardBar overlay is no longer used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RoyalVNCKit prioritizes .diffieHellman (ARD) over .vnc during handshake when
both are offered. My delegate adapter was passing an empty username to
VNCUsernamePasswordCredential, so any Mac with user-account screen sharing
enabled rejected the credential before .vnc fallback could happen.
Fix: persist a username on SavedConnection and pipe it through to the
credential callback. Leave blank to use the VNC-only password path.
AddConnection footer now explains the two Mac paths:
• User account (ARD) — macOS short name + full account password
• VNC-only password — blank username + ≤8 char password
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS 26's NavigationStack reserves space for its floating Liquid Glass nav bar
even with `.toolbar(.hidden, for: .navigationBar)` — that's why "Screens"
landed ~180pt down from the dynamic island with a wedge of black above it.
Removed the NavigationStack from the list entirely. Layout is now a plain
top-anchored VStack: top chrome (gear + Screens + plus + search) flush with
the safe-area top, then a ScrollView with LazyVStack of cards filling the
rest of the screen. Sessions present via fullScreenCover instead of
NavigationLink, so we don't need NavigationStack here at all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The system nav bar floated low on iOS 26 and left a wedge of black above the
title. Replaced it with a custom top row pinned to the safe-area top: gear ⟶
big "Screens" wordmark ⟶ +. Search bar lives directly beneath, sections start
right after — no centered-in-the-void layout. Background gets a subtle blue
radial bloom so the floating glass buttons have something to anchor to.
Saved-empty state is now a glass card with an icon and a gradient CTA button.
Connection rows are full-width glass cards with rounded corners; long-press
gives Edit / Open in New Window / Delete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Edit reuses AddConnectionView with an `editing:` parameter that prefills the
form and updates in-place; password field becomes optional ("leave blank to
keep current"). Surfaced via context menu and a leading-edge swipe action on
each saved row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>