Every time Mac repainted a region we set imageLayer.contents to a new
CGImage. CALayer's default action for .contents is a 0.25s crossfade, so
big repaints (like after a click — cursor + button + window-focus) looked
like a pulse. Tap seemed "flickery"; actually the whole view was doing
quarter-second crossfades constantly, most just weren't big enough to
notice until a chunky repaint hit.
Override imageLayer.actions with NSNull for contents/contentsRect/frame/
transform so blits are instantaneous, and wrap apply() in a
CATransaction.setDisableActions(true) for safety.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>