Name every silent input rejection with WebSurfaceInputOutcome

bool returns on the five record_* surface inputs collapsed nine real
outcomes into one bit. The cascade we found in this round — silent
click drop because viewport_bounds wasn't measured yet, then
keyboard_focus stays None, then typing also "fails" — looked like
three independent symptoms but was one root cause hiding inside
that bit. Replace the bool with a #[must_use] WebSurfaceInputOutcome
enum so each rejection names itself at the call site.

Variants map 1:1 to real return points in web_surface.rs:
  Applied / NoChange / Buffered — three distinct success-ish states
  the controller already needed to disambiguate (only Applied notifies)
  DroppedInvalidBounds — geometry rejected zero/NaN viewport
  DroppedNoViewportBounds — input arrived before the viewport tracker
  DroppedOutOfBounds — window position outside the viewport rect
  DroppedZeroDelta — wheel rounded to zero device px
  DroppedEmptyText — empty record_typed_text
  DroppedNoKeyboardFocus — type without a prior click
  DroppedFocusMismatch — focus belongs to another tab/url

Behavior preserved: Applied is the only notify trigger, matching the
old `true` semantics. Three new negative-path tests (no_viewport_bounds,
zero_delta, no_keyboard_focus) lock the named drops so a future
regression surfaces as a wrong variant in tests instead of a missing
repaint. cargo test ely_app --bin ely_app web_surface: 17 passed.
This commit is contained in:
2026-05-10 02:00:52 -04:00
parent c20daf36b9
commit b8795bf903
5 changed files with 159 additions and 54 deletions
@@ -50,6 +50,46 @@ pub(super) struct WebSurfacePendingInput {
pub(super) typed_text: Option<String>,
}
/// Outcome of a `WebSurfaceStore::record_*` call.
///
/// Replaces the previous `-> bool` return so silent rejections name
/// themselves at the call site. Callers only branch on `Applied`
/// (every other variant means "do nothing, don't notify"), but each
/// `Dropped*` / non-`Applied` variant pins down *why* a coordinate
/// or keystroke never reached the runtime — so the next regression
/// shows up as a specific variant in tests instead of a missing
/// repaint. `#[must_use]` keeps a future caller from dropping the
/// outcome on the floor and reintroducing the silent-fail pattern.
#[must_use]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum WebSurfaceInputOutcome {
/// State changed and the renderer should re-notify.
Applied,
/// Same value as currently recorded — nothing to flush downstream.
NoChange,
/// First sighting of a new value; held back until a second
/// matching measurement confirms it (viewport-resize debounce).
Buffered,
/// Geometry constructor rejected the input (zero/NaN/negative
/// bounds). The viewport never measured cleanly.
DroppedInvalidBounds,
/// Viewport bounds have not been recorded yet, so window-relative
/// coordinates can't be translated into the page coordinate space.
DroppedNoViewportBounds,
/// Window position falls outside the viewport rect after scaling.
DroppedOutOfBounds,
/// Wheel delta rounded to zero device pixels in both axes.
DroppedZeroDelta,
/// Empty string passed to `record_typed_text` — nothing to buffer.
DroppedEmptyText,
/// `record_typed_text` ran before any click established
/// keyboard focus on this surface.
DroppedNoKeyboardFocus,
/// Keyboard focus belongs to a different tab or the URL drifted
/// (redirect / trailing-slash mismatch) since the focusing click.
DroppedFocusMismatch,
}
pub(super) enum WebSurfaceState {
Loading { requested_url: String, previous_frame: Option<WebSurfaceFrame> },
Ready(WebSurfaceFrame),