Skip to content

Fill imperative consumption gaps: event payloads, focus, dialogs (waves P–T) - #6

Merged
Marc-André Moreau (mamoreau-devolutions) merged 6 commits into
masterfrom
copilot/rustolonia-gap-analysis
Sep 11, 2026
Merged

Fill imperative consumption gaps: event payloads, focus, dialogs (waves P–T)#6
Marc-André Moreau (mamoreau-devolutions) merged 6 commits into
masterfrom
copilot/rustolonia-gap-analysis

Conversation

@mamoreau-devolutions

Copy link
Copy Markdown
Contributor

Summary

An audit of the projection against the four apps showed every consumer uses AXAML + view-model interop and the 104-class imperative IAvn* surface had zero consumers. This PR fills the gaps that made the imperative model unusable, in five verified waves:

  • Wave P — first imperative consumer: rust/avalonia/examples/imperative_counter.rs builds a full UI with no AXAML and no view-model IR. Selection events now carry AddedItems/RemovedItems on host-implemented args interfaces (handlers v2); RangeBase.ValueChanged carries old/new fields; Control gains KeyUp (with settable Handled), GotFocus, LostFocus. Ships the missing wave tooling: avalonia-bindgen --write-baseline regenerates the frozen abi-baseline.json, wired as regenerate-and-build.ps1 -UpdateAbiBaseline.
  • Wave Q — pointer payloads: PointerEntered/PointerExited/Tapped carry KeyModifiers; DoubleTapped is a notification; PointerWheelChanged carries the scroll Delta as AvnVector.
  • Wave R — event-parameter marshalling overrides (EventParameterProjection.Override + ProjectedParameter.StringConverterTypeName); NumericUpDown.ValueChanged carries the old/new decimal pair as invariant strings through the existing AvnDecimal converter (handler v2).
  • Wave SControl.Focus(NavigationMethod, KeyModifiers) returning success, plus Focusable.
  • Wave T — modal dialog activation: new IAvnApplication5 capability with StartShowDialog riding the shared async operation registry (exactly-one completion, cancellable via dialog close, never blocks the UI thread). Safe-side AppScope.show_dialog returns AsyncOperation<Option<String>>.

ABI policy

All changes follow the nano-COM policy in rust/COMPATIBILITY.md: no slot is ever inserted into a published vtable; widened interfaces republish under fresh version-bumped IIDs (AbiVersions), and stale identities are pinned as retired in the sys ABI tests so reuse fails loudly (E_NOINTERFACE) rather than calling through a stale contract. App-level capabilities are separately versioned IAvnApplicationN interfaces (stage 32 for dialogs).

Verification

  • Every wave verified live in the running example over UI Automation: button clicks, text echo, list mutation, selection args (+1 item(s)), wheel delta (Wheel: (0, -1)), decimal change (Number: 3.2 -> 3.25), focus on mount (Focus: true), and a modal accept/decline dialog (Dialog: accepted).
  • Test suites green at every wave: 129 IR + 92 Generator + 375 Host managed tests; 45 Rust test suites including the ABI contract against the regenerated baseline.
  • Both generator --check determinism gates pass; cargo fmt --check clean; regen produces no diff.

Deferred (documented)

Pointer position payloads (GetPosition(relativeTo) is a method — needs a coordinate-space design decision), DataGrid/NativeMenu inventory, arm64 native CI execution. The dormant MarshallingKind.DialogCompletion scaffolding can be removed in a follow-up — wave T used the proven IAvnAsyncCompletion path instead.

Fill the top consumption gaps found by auditing the projection against
the four apps (all AXAML + view-model interop; zero imperative consumers):

- Control gains KeyUp (Fields parity with KeyDown), GotFocus
  (NavigationMethod/KeyModifiers) and LostFocus, so keyboard and focus
  interaction is consumable from Rust. Control and every descendant
  republish under fresh IIDs.
- SelectingItemsControl.SelectionChanged and TreeView.SelectionChanged
  now carry the added/removed item lists on host-implemented args
  interfaces (handler identities minted at version 2);
  RangeBase.ValueChanged carries the old/new value pair as fields.
- avalonia-bindgen gains --write-baseline, the reviewed way to advance
  the frozen ABI snapshot after an intentional wave, wired into
  regenerate-and-build.ps1 as -UpdateAbiBaseline.
- New avalonia example imperative_counter builds a full UI with no
  AXAML and no view-model IR, exercising composition, brushes, commands
  and the new event payloads end to end.
Extend the imperative surface with the pointer interactions the dogfood
sample proved were missing from the event model:

- Control.PointerEntered/PointerExited/Tapped now carry KeyModifiers as
  a Fields payload; DoubleTapped crosses as a notification (no payload
  members); PointerWheelChanged carries the scroll Delta as an AvnVector
  plus the modifiers. The host-typed members (IPointer, relative
  position) stay off the wire by design.
- The advise slots change shape, so Control and every descendant
  republish under fresh IIDs (version bump via AbiVersions); the three
  new handler interfaces mint at version 1. The stale wave-P IIDs are
  recorded as retired in the sys ABI tests.
- The imperative_counter example grows a wheel zone that renders the
  live delta, verified end to end over UI Automation.
…lling overrides

The last payload-less input event a form UI reaches for. The decimal?
old/new pair has no numeric ABI shape, so it crosses as invariant UTF-16
through the AvnDecimal converter the Value property already uses:

- EventParameterProjection gains an optional MarshallingOverride, so a
  Fields payload can carry a CLR type the default extractor mapping
  rejects (decimal? here) through an explicitly named host converter.
- ProjectedParameter gains StringConverterTypeName and the C# event
  read/write expressions route StringUtf16 event fields through it.
- IAvnNumericUpDownValueChangedHandler republishes at version 2 with the
  payload; the control itself is untouched. Rust bindgen emits
  Option<String> slots natively, so no bindgen change was needed.
- imperative_counter grows a NumericUpDown whose label renders the
  old/new pair; verified live over UIA ('Number: 3.2 -> 3.25').
Input focus was entirely off the projected surface, so no Rust consumer
could move focus programmatically. Focus lives on InputElement and is
inherited by every projected Control; the projection names it explicitly
and carries the navigation context:

- Control.FocusWithNavigationMethodAndKeyModifiers crosses with the
  NavigationMethod and KeyModifiers enums and returns whether focus
  took; Control.Focusable crosses as a Bool property.
- The new method slots republish Control and every descendant under
  fresh IIDs (version bump via AbiVersions); the stale wave-Q IIDs are
  recorded as retired in the sys ABI tests.
- imperative_counter focuses its text box after mount and renders the
  result; verified live over UIA ('Focus: true').
Window.ShowDialog returns a Task<TResult> and never completes
synchronously, so it composes onto the shared async operation registry
the same way the storage pickers and clipboard do — exactly one
completion, cancellable, never blocking the UI thread:

- New IAvnApplication5 capability (stage 32) queried from IAvnApplication
  with StartShowDialog(owner, dialog, completion); the dialog result
  converts to its ABI string form on completion (null = no result).
- AvnApplication.Dialogs.cs (partial class) resolves the window tokens
  up front and awaits ShowDialog; a cancellation request closes the
  dialog instead of abandoning the task, because an abandoned
  ShowDialog still holds the owner.
- avalonia-sys gains the dialogs module; the safe crate exposes
  AppScope.show_dialog returning an AsyncOperation<Option<String>>.
- imperative_counter grows an 'Ask' button that opens a modal
  accept/decline dialog and renders its result; verified live over UIA
  ('Dialog: accepted').
The workflow's push trigger referenced main, which is the fork's
default branch; this repository's default branch is master, so the
release gate never ran on upstream pushes. PRs are unaffected (the
pull_request trigger has no branch filter).
@mamoreau-devolutions

Copy link
Copy Markdown
Contributor Author

CI: the "Avalonia Rust" workflow fails at startup — needs an org/repo admin

Every pull_request run of this workflow on this repo completes in 0s with conclusion: startup_failure and zero jobs (this PR: run 34557457255; same on every recent PR: archive-zip-tar-explorer, devtools-integration, hash-calculator, fix-avalonia-submodule, pdf-viewer). The workflow YAML is valid and unchanged since the initial commit.

What I verified:

  • The identical workflow runs green on the origin fork (mamoreau-devolutions/rustolonia) — this is not a YAML or code problem.
  • The repo's default branch is master, and the workflow's push trigger referenced main, so the upstream never ran this gate on its own default branch. Fixed in d5164fb (branches: [master]).
  • I cannot rerun or inspect the failure: POST /actions/runs/{id}/rerun → 403 This workflow run cannot be retried, and every Actions policy endpoint (/actions/permissions, /orgs/Devolutions/actions/permissions, allowed-actions) returns 403 for my token. Actions itself is enabled on the repo (secrets/variables endpoints respond).

Most likely cause: an org-level GitHub Actions policy on Devolutions (allowed actions / runner groups / "Fork pull request workflows" or a billing/spending limit) that rejects this workflow before any job is scheduled. It needs someone with admin:org on Devolutions to check Organization settings → Actions → General and this repo's Settings → Actions.

Until then: this PR's verification was done locally — 129 IR + 92 Generator + 375 Host managed tests green, 45 Rust suites green (including the ABI contract against the regenerated baseline), both generator --check determinism gates pass, and every wave (P/Q/R/S/T) verified live in the running imperative_counter example over UI Automation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant