Skip to content

Commit 617bedd

Browse files
author
Roy Lin
committed
feat: schedule self-drawn long press
1 parent f644ded commit 617bedd

18 files changed

Lines changed: 868 additions & 54 deletions

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,10 @@ independently.
294294
exposure, published only after host commit, and discarded on rejection
295295
- retained resize, fractional-scale, damage, occlusion, redraw, delayed
296296
acknowledgement, and surface-loss replay without changing semantic identity
297-
- raw pointer, keyboard, Tab-focus, hover, press, cancellation, wheel, stable
298-
action bubbling, and reducer dispatch over `PlatformElementId`; failed
299-
reducers restore the complete staged interaction session
297+
- raw pointer, keyboard, Tab-focus, hover, press, cancellation, wheel, and
298+
scheduled long-press routing over `PlatformElementId`; event-loop deadlines,
299+
release-time fallback, stable action bubbling, and reducer rollback share one
300+
staged interaction session
300301
- an identical-frame fast path that performs no layout, scene, host, or
301302
presentation work, plus semantic-only commits that skip pixel presentation
302303
- a software Graphics presenter and interactive shared
@@ -314,7 +315,7 @@ independently.
314315
| M3 · Layout and Scene | Current | Generic calculator rectangle slice landed; full flex, stacking, redraw scheduling, cross-platform fingerprints, and thin-host presentation remain |
315316
| M4 · Text and interaction cutover | Planned | Shaping, glyphs, GUI-owned input, IME, accessibility bridges, overlays, and complete calculator scenarios |
316317
| M5 · Default cutover | Planned | Make self-drawn content the default, then delete the three legacy widget renderers |
317-
| H0-H5 · Thin platform hosts | H0 complete; H1 in progress | Atomic frames, lifecycle recovery, stable-id raw input/reducer routing, zero-toolkit firewalls, and an interactive calculator landed; a real raw-surface presenter remains |
318+
| H0-H5 · Thin platform hosts | H0 complete; H1 in progress | Atomic frames, lifecycle recovery, stable-id raw input/reducers, scheduled long press, zero-toolkit firewalls, and an interactive calculator landed; the Graphics raw-surface edge remains |
318319
| T0-T5 · TSX native authoring | Proposed | Automatic JSX runtime, versioned Node-to-host session, state/event runtime, self-drawn native window, packages, and stable SDK |
319320
| M6-M8 · React Aria components | Catalog pinned; conformance planned | 51/51 families mapped, eight public parts explicitly missing, Button scene smoke only; full software and three-OS self-drawn evidence required |
320321

docs/platform-hosts.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ src/platform_runtime/
212212
|- interaction.rs portable state, actions, changes, and event context
213213
|- input.rs pointer capture, hover, press, and cancellation
214214
|- keyboard_input.rs keyboard activation, Tab focus, and wheel routing
215+
|- long_press_input.rs event-loop deadlines and terminal hold recognition
215216
|- presenter.rs raw-surface prepare/publish contract and recorder
216217
|- reference_presenter.rs
217218
| transactional software Graphics evidence
@@ -241,6 +242,17 @@ The new modules are created beside the legacy directories rather than by
241242
renaming a control backend. This makes accidental content-widget reuse visible
242243
and lets each old backend be deleted after its replacement evidence exists.
243244

245+
### Graphics surface capability gate
246+
247+
The pinned Graphics commit `8748fab` currently renders to a
248+
surface-independent texture and supports deterministic readback; it does not
249+
yet expose its planned safe window-surface attachment API. GUI therefore does
250+
not import `wgpu`, create a second device/queue owner, or place raw handles in
251+
`PlatformHost` records. H1's remaining presenter can land only after Graphics
252+
provides host-owned attachment, configure/acquire/present, resize/suspend, and
253+
surface/device recovery while retaining Graphics resource identity. The H1
254+
firewall pins that dependency and rejects a direct GUI `wgpu` dependency.
255+
244256
## H0 Transaction and Thread Contract
245257

246258
`PlatformHost` is intentionally thread-affine. The trait does not require
@@ -355,10 +367,14 @@ Landed evidence:
355367
route through committed hit regions and stable `PlatformElementId` paths;
356368
ordered action batches retain frame revision, event sequence, bubbling
357369
target, input context, and static action payload
370+
- callback-driven and style-only long press exposes the next monotonic host
371+
deadline, cancels and restarts across pointer boundaries, falls back to
372+
release-time recognition, and routes the terminal action through the same
373+
rollback-aware reducer path
358374
- reducer errors restore the staged interaction state and sequence before the
359375
event is exposed as successful; successful frame reconciliation preserves
360376
focused stable ids, while rejected frames do not touch them
361-
- 20 focused runtime/software tests and three recursive feature/source
377+
- 26 focused runtime/software tests and four recursive feature/source
362378
firewall tests pass without any legacy renderer or OS toolkit dependency
363379
- `self_drawn_calculator` reproduces layout fingerprint
364380
`16529597026056060935`, scene fingerprint `2100550662756266801`, and
@@ -369,7 +385,8 @@ Remaining before H1 is complete:
369385

370386
- implement the Graphics raw-surface presenter used by H2-H4; the landed
371387
presenter contract and software implementation deliberately expose no raw
372-
handle to components or common host records
388+
handle to components or common host records, and pinned Graphics commit
389+
`8748fab` must first supply its planned safe surface attachment API
373390

374391
Gates:
375392

docs/react-aria-native.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,18 @@ The first shared interaction milestone is available in the portable runtime:
123123

124124
- `SelfDrawnWindowRuntime` builds its hit/focus/action index from the atomically
125125
committed Native IR and layout snapshot. Raw pointer, keyboard, Tab-focus,
126-
hover, press, cancellation, wheel, bubbling, and reducer dispatch retain
127-
stable `PlatformElementId` identity without importing a widget blueprint or
128-
platform content-control runtime.
126+
hover, press, cancellation, wheel, scheduled long press, bubbling, and
127+
reducer dispatch retain stable `PlatformElementId` identity without
128+
importing a widget blueprint or platform content-control runtime.
129129
- `SelfDrawnActionInvocation` carries the committed frame revision, monotonic
130130
event sequence, target/current-target pair, modality and input context, plus
131131
static action payloads. Reducer failure restores the staged interaction
132132
session and sequence; keyed successful frames reconcile focus and transient
133133
state by stable id.
134+
- `next_interaction_deadline_micros` lets a thin OS host schedule its native
135+
event-loop timer without exporting a toolkit timer into portable state.
136+
Deadline recognition and release-time fallback share the same ordering and
137+
reducer rollback, while pointer leave/re-entry cancels and restarts the hold.
134138

135139
- `NativeInputModality` represents keyboard, mouse, touch, pen, virtual, and
136140
unknown input.
@@ -564,7 +568,7 @@ props:
564568
| Priority | Area | Required outcome |
565569
| --- | --- | --- |
566570
| P0 | Self-drawn component accounting | Keep all 51 React Aria 1.19.0 families in the executable matrix, implement the eight recorded public-part gaps, and require every upstream catalog delta to update code, matrix, tests, and milestones together. |
567-
| P0 | Shared self-drawn interaction | Extend the landed stable-id pointer, keyboard, Tab-focus, hover, press, cancellation, wheel, bubbling, and reducer path with long press, move/drag, focus-scope restoration, overlay gestures, text editing/IME, and accessibility activation. |
571+
| P0 | Shared self-drawn interaction | Extend the landed stable-id pointer, keyboard, Tab-focus, hover, press, scheduled generic long press, cancellation, wheel, bubbling, and reducer path with move/drag, collection long-press selection mode, focus-scope restoration, overlay gestures, text editing/IME, and accessibility activation. |
568572
| P0 | Native input conformance | WinUI's complete 98-case V1 manifest passes real OS automation. Populate the AppKit and GTK4 manifests with platform-run mouse, pen, touch where applicable, keyboard, assistive activation, disabled, cancellation, and keyed-rerender fixtures for every role currently marked native; then close or retain evidence-backed menu/item exceptions. |
569573
| P1 | Event propagation | Add platform-run conformance fixtures for conditional `Stop`/`Continue` across nested native controls. |
570574
| P1 | Focus management | Add platform-run conformance fixtures for post-mount `autoFocus`, nested containment, and restoration. |

docs/roadmap.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,17 +486,24 @@ Landed evidence:
486486
- ordered action batches carry the hit-tested frame revision and monotonic
487487
event sequence, preserve bubbling current targets and static payloads, and
488488
restore staged interaction state if an application reducer fails
489+
- long press exposes a monotonic event-loop deadline, tracks style-only and
490+
callback-driven targets, resets across pointer leave/re-entry, recognizes at
491+
the deadline or on release as a scheduling fallback, and atomically emits
492+
`LongPressEnd`, `PressCancel`, then terminal `LongPress`
489493
- the shared 410x620 calculator preserves its reviewed layout and scene
490494
fingerprints, routes eight fake-host events through four reducer actions,
491495
commits the resulting frames, and reaches display value `10`
492-
- 20 focused runtime/software tests plus three recursive H1 firewall tests are
496+
- 26 focused runtime/software tests plus four recursive H1 firewall tests are
493497
included in `just verify`
494498

495499
Remaining H1 work:
496500

497501
- a Graphics raw-surface presenter implementation for the H2-H4 OS shells;
498-
richer text editing, IME, overlay gestures, and component-specific
499-
interaction conformance remain explicit M4 and M6-M8 work
502+
pinned Graphics commit `8748fab` owns only a surface-independent texture and
503+
readback today, so its safe host-owned surface attachment/recovery contract
504+
must land before GUI can implement this edge without duplicating `wgpu`
505+
- richer move/drag, text editing, IME, overlay gestures, and
506+
component-specific interaction conformance remain explicit M4 and M6-M8 work
500507

501508
Gates:
502509

src/event/press.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,13 +1115,7 @@ fn is_number_field_stepper(blueprint: &NativeWidgetBlueprint) -> bool {
11151115
}
11161116

11171117
fn long_press_threshold(metadata: &BTreeMap<String, String>) -> Duration {
1118-
["threshold", "data-long-press-threshold"]
1119-
.into_iter()
1120-
.find_map(|name| metadata.get(name))
1121-
.and_then(|value| value.trim().parse::<u64>().ok())
1122-
.filter(|value| *value > 0)
1123-
.map(|value| Duration::from_millis(value.min(60_000)))
1124-
.unwrap_or(DEFAULT_LONG_PRESS_THRESHOLD)
1118+
Duration::from_micros(crate::semantic_event::long_press_threshold_micros(metadata))
11251119
}
11261120

11271121
#[cfg(test)]

src/platform_runtime/events.rs

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,31 +111,73 @@ where
111111
let SelfDrawnHostEventOutcome::Input(dispatch) = &mut outcome else {
112112
return Ok(outcome);
113113
};
114-
let mut stopped_at = None;
115-
for invocation in &dispatch.invocations {
116-
if stopped_at
117-
.as_ref()
118-
.is_some_and(|target| invocation.current_target() != target)
119-
{
120-
continue;
121-
}
122-
match reducer(invocation) {
123-
Ok(SelfDrawnActionPropagation::Continue) => {}
124-
Ok(SelfDrawnActionPropagation::Stop) => {
125-
stopped_at = Some(invocation.current_target().clone());
126-
}
127-
Err(error) => {
128-
self.interaction = interaction;
129-
self.stats = stats;
130-
self.stats.reducer_failures = self.stats.reducer_failures.saturating_add(1);
131-
return Err(error);
132-
}
133-
}
114+
if let Err(error) = reduce_dispatch(dispatch, &mut reducer) {
115+
self.interaction = interaction;
116+
self.stats = stats;
117+
self.stats.reducer_failures = self.stats.reducer_failures.saturating_add(1);
118+
return Err(error);
134119
}
135-
dispatch.propagation_stopped_at = stopped_at;
136120
Ok(outcome)
137121
}
138122

123+
/// Earliest monotonic host-clock timestamp at which portable interaction
124+
/// state needs another event-loop callback.
125+
pub fn next_interaction_deadline_micros(&self) -> Option<u64> {
126+
self.interaction.next_interaction_deadline_micros()
127+
}
128+
129+
/// Advances scheduled portable interaction state without synthesizing an
130+
/// operating-system input event. Hosts call this at or after the deadline
131+
/// returned by [`Self::next_interaction_deadline_micros`]. Each call drains
132+
/// one stable pointer deadline; call again while the next deadline is not
133+
/// later than the current host timestamp.
134+
pub fn advance_interaction_time(
135+
&mut self,
136+
timestamp_micros: u64,
137+
) -> GuiResult<Option<super::SelfDrawnInputDispatch>> {
138+
self.ensure_running()?;
139+
let Some(snapshot) = self.committed.as_ref() else {
140+
return Ok(None);
141+
};
142+
let revision = snapshot.revision();
143+
let tree = Arc::clone(snapshot.interaction_tree());
144+
let dispatch =
145+
self.interaction
146+
.route_interaction_time(timestamp_micros, revision, &tree)?;
147+
if let Some(dispatch) = &dispatch {
148+
self.stats.interaction_ticks = self.stats.interaction_ticks.saturating_add(1);
149+
self.stats.action_invocations = self
150+
.stats
151+
.action_invocations
152+
.saturating_add(dispatch.invocations.len() as u64);
153+
}
154+
Ok(dispatch)
155+
}
156+
157+
/// Advances a scheduled interaction deadline and applies its ordered
158+
/// action batch transactionally to the portable interaction session.
159+
pub fn advance_interaction_time_with_reducer<R>(
160+
&mut self,
161+
timestamp_micros: u64,
162+
mut reducer: R,
163+
) -> GuiResult<Option<super::SelfDrawnInputDispatch>>
164+
where
165+
R: FnMut(&SelfDrawnActionInvocation) -> GuiResult<SelfDrawnActionPropagation>,
166+
{
167+
let interaction = self.interaction.clone();
168+
let stats = self.stats;
169+
let Some(mut dispatch) = self.advance_interaction_time(timestamp_micros)? else {
170+
return Ok(None);
171+
};
172+
if let Err(error) = reduce_dispatch(&mut dispatch, &mut reducer) {
173+
self.interaction = interaction;
174+
self.stats = stats;
175+
self.stats.reducer_failures = self.stats.reducer_failures.saturating_add(1);
176+
return Err(error);
177+
}
178+
Ok(Some(dispatch))
179+
}
180+
139181
pub fn poll_event(&mut self) -> GuiResult<Option<SelfDrawnHostEventOutcome>> {
140182
let Some(event) = self.host.poll_event()? else {
141183
return Ok(None);
@@ -283,3 +325,29 @@ fn input_window(event: &PlatformInputEvent) -> PlatformWindowId {
283325
PlatformInputEvent::ModifiersChanged { window, .. } => *window,
284326
}
285327
}
328+
329+
fn reduce_dispatch<R>(
330+
dispatch: &mut super::SelfDrawnInputDispatch,
331+
reducer: &mut R,
332+
) -> GuiResult<()>
333+
where
334+
R: FnMut(&SelfDrawnActionInvocation) -> GuiResult<SelfDrawnActionPropagation>,
335+
{
336+
let mut stopped_at = None;
337+
for invocation in &dispatch.invocations {
338+
if stopped_at
339+
.as_ref()
340+
.is_some_and(|target| invocation.current_target() != target)
341+
{
342+
continue;
343+
}
344+
match reducer(invocation)? {
345+
SelfDrawnActionPropagation::Continue => {}
346+
SelfDrawnActionPropagation::Stop => {
347+
stopped_at = Some(invocation.current_target().clone());
348+
}
349+
}
350+
}
351+
dispatch.propagation_stopped_at = stopped_at;
352+
Ok(())
353+
}

0 commit comments

Comments
 (0)