Skip to content

Latest commit

 

History

History
215 lines (185 loc) · 15.5 KB

File metadata and controls

215 lines (185 loc) · 15.5 KB

Roadmap

SwiftDotNet is early-stage and an active design space. Detailed design docs live in plans/; this page indexes them and the open questions.

Cross-cutting milestone: per-view reconciliation

Several features are gated on the same milestone — per-view local state ownership, i.e. child composite views keeping local state across renders via view-instance reconciliation. It unblocks:

  • child ctor injection for DI,
  • enter/leave transitions,
  • inline-Body-child lifecycle hooks,
  • keyed ForEach for animated list insert/remove/move.

Plan index: plans/README.md lists every design doc with its real status and what's left in each.

Open workstreams

Linux / Wayland backend — scaffolded, unproven

See Linux / Wayland. The Skia backend now has a host that talks xdg-shell directly: client-side decorations, an shm swapchain, xkb input with compose and key repeat, fractional scaling, clipboard and IME. The protocol layer is shared with the .NET MAUI Wayland backend in the sibling maui-wayland repo.

Everything compiles and the protocol tables are unit-tested (23 tests covering signature arity, cross-interface references and the native wl_interface struct layout), but nothing has been run against a live compositor — it was written on macOS. Remaining, in order:

  1. First run on GNOME/Mutter, KDE/KWin and a wlroots compositor. These three diverge on decorations, fractional scale and layer-shell availability, so all three matter.
  2. Route zwp_text_input_v3 into the Skia text controls — the platform layer surfaces preedit/commit already, but SkiaBridge still only receives committed text from xkb.
  3. wl_subsurface overlays for WebView / Map, which no Skia host can paint.
  4. A GPU path (EGL or Vulkan + zwp_linux_dmabuf_v1); today it is CPU raster into shared memory.
  5. AT-SPI2 accessibility — the genuine cost of self-drawing on Linux, and the reason to keep the GTK4 backend alongside it rather than replacing it.

Dependency injection — Phase 1 shipped

See Hosting & Dependency Injection; design in plans/dependency-injection-proposal.md. MAUI-style SwiftProgram.CreateSwiftApp() + SwiftDotNetApp.CreateBuilder(), [Inject] partial properties filled by a reflection-free source generator, View.Service<T>() for inline children, IViewLifecycle observers + view hooks, and ISwiftInitializer. MS.DI is referenced by the root library (the add-on idea was rejected — hosting is the front door).

Remaining: per-page IServiceScope (needs the paused navigation service); child-view injection (waits on per-view reconciliation); ISwiftDispatcher — mostly moot, since SwiftApp already marshals via the captured SynchronizationContext, so the work is auditing backends that lack one.

Animation

Implicit .Animation(spec, on:) shipped, and multi-track .Keyframes(…) timelines shipped on every backend except the TUI, which has no animation clock at all (WinUI's path is written but 🧩 scaffolded — never compiled). Remaining: explicit Animate.Run(...) transactions (needs render batching in SwiftAppState.Value currently renders immediately per set) and enter/leave .Transition(...) (gated on reconciliation). Phases: 1 = implicit ✅, 1b = keyframes ✅, 2 = batching + explicit, 3 = transitions.

Gestures & transforms

.ScaleEffect shipped. Tap/long-press/swipe (one-shot) shipped. Remaining: continuous pan/pinch (need a new throttled/committed event channel + a Transformable container — native-owned live transform, C# syncs on end); .Rotation/.Offset siblings; and the .Tag(name) native-view-access seam.

Page / view lifecycle — Phase 1 shipped

plans/page-lifecycle-plan.md (reconciled 2026-07-19). Shipped with DI Phase 1: OnCreated/OnAppearing/OnDisappearing/OnDestroyed on View, plus IViewLifecycle observers registered in the container and a dispatcher with a defined ordering (observers → view on setup, view → observers on teardown). See Hosting & Dependency Injection.

Caveat worth knowing: visibility is not real yet — OnAppearing is currently raised by the host code path (app start / ViewScope), not by actual platform visibility. The next slice is the native appear/disappear emitters per backend (SwiftUI .onAppear, Compose DisposableEffect, WinUI Loaded, GTK map, Blazor OnAfterRenderAsync) plus node-id→view routing in SwiftApp. Then IAppLifecycle, .OnChange(state, handler), and OnAppearAsync(ct). Per-page lifecycle is blocked on the paused navigation service; inline-child lifecycle waits on reconciliation.

Native view access

plans/native-view-access-plan.md — tag-based access to a control's underlying native view (.Tag + per-backend Customize registries).

MAUI interop — implemented, MAUI half unrun

plans/maui-interop-plan.md · docs: MAUI Interop. The platform-view seam (IPlatformViewHost) is in the engine with CI coverage, MauiView embeds a real MAUI control, and WebView finally punches through inside a MAUI app. What's left: drive the sample's MAUI tab by hand on the simulator and emulator — scroll tracking, per-control clipping, the overlay-suppression rule, and the IME hand-off between the shadow Entry and an embedded text control are all unverified; a Map platform-view renderer; an IPlatformViewHost for the non-MAUI Skia hosts (AppKit, Silk, WPF, WinForms) so WebView/Map work there too; and the guest direction (a MAUI control inside a SwiftUI or Compose tree) compiles on every platform but has never run — the WinUI third of it has never compiled, along with the rest of that backend.

Accessibility & screen readers

plans/accessibility-plan.md — nothing is built. Ten SwiftUI-style modifiers (.AccessibilityLabel/Hint/Value/Hidden/traits/grouping/actions), an Accessibility settings channel over a reserved $a11y event id (screen-reader-running, reduce motion, text scale, high contrast) in the shape of SafeArea, and — the real gap — a Skia accessibility tree with UIAccessibilityContainer / ExploreByTouchHelper host adapters, since the Skia canvas is a single unlabelled rectangle to VoiceOver and TalkBack today.

Game surface & real-time rendering

plans/game-engine-plan.md — draft, nothing built. Everything the framework does today is declarative and native-owned: .Animation and .Keyframes hand interpolation to SwiftUI/CSS/Compose and never report back, so there is no frame clock, no update(dt), no immediate-mode drawing surface, and no keyboard/multi-touch input anywhere.

The plan adds a Canvas node that is diffed once and thereafter bypasses IBridge entirely — necessary because IBridge.Render takes a JSON string on every backend, including the pure-C# in-process ones, so no per-frame content can travel that path. Frames are recorded into a binary DisplayList in C# and replayed per backend (the same shape as Flutter's Dart→engine display list), which works across both the shim and interpreter routes. Phases: 1 = surface, 2 = .OnFrame(dt) + raw input, 3 = paths/blend modes/sprite atlases, 4 = a separate SwiftDotNet.Game library, 5 = the remaining backends. Phases 1–3 are justified by charts, signature pads and custom gauges on their own; they also supersede F8 in plans/controls-missing-features-plan.md. Audio and a portable shader language are explicitly out of scope.

System-rendered surfaces — Live Activities, notifications, widgets — implemented, unrun

plans/live-activities-plan.md and plans/widgets-plan.md — both built; reference at Live surfaces.

The two pure-C# libraries (SwiftDotNet.Live, SwiftDotNet.Widgets) are covered by 41 headless tests. The two platform drivers and the Swift shim compile and have never been run — no device, no emulator, no simulator. Remaining, in order: an emulator run of the RemoteViews interpreter; a simulator run of a widget extension against the App Group; the five factual unknowns the plans flag (widget memory budget, whether a custom Layout survives archiving, whether LiveActivityIntent really runs in-process, real RemoteViews parcel limits, the WidgetKit refresh budget); then widget configuration and a dotnet new template for the Xcode extension target.

These are the first targets where another process renders our tree. A Live Activity's UI is built by a WidgetKit extension and archived by SpringBoard; an Android notification or app widget is a RemoteViews recipe inflated inside SystemUI. .NET cannot run in either renderer, so the existing drive loop — C# owns the tree and pushes patches through IBridge — does not apply, and neither the SwiftUI nor the Compose shim can be reused as-is. What does transfer is the wire format (WireNode in the Swift bridge is already Decodable) and SwiftApp's action dispatch.

The result is a restricted LiveView vocabulary validated before every publish (the widget SwiftUI subset ∩ the RemoteViews whitelist), a pure-Swift widget interpreter, a C# RemoteViews interpreter — the planned Kotlin one proved unnecessary, since a RemoteViews is a serialized recipe and an AppWidgetProvider runs in our own process — and a bitmap escape hatch that reuses the headless VisualBridge path already in SwiftDotNet.Graphics. A shared ISurfaceChannel — a durable store-and-nudge mailbox, not a socket, because an iOS app and its extension are almost never alive at once — carries state out and actions back.

The platforms are structurally asymmetric and the plans say so rather than papering over it: Android's AppWidgetProvider runs in our process (C# builds the RemoteViews directly; Glance is rejected), while Apple's extension has no .NET, so the app must pre-render every timeline entry into an App Group. Android work ships first because it needs no extension plumbing; the Apple half's real cost is that a consumer must add an Xcode widget-extension target and an App Group entitlement — there is no "just add a NuGet" story. Android 16 Live Updates (Notification.ProgressStyle) are modelled as data, not a view tree, because that is what they are.

Backend-specific next steps

  • Windows / WinUI 3 — compile + verify the WinUI 3 backend on a Windows host (expect minor API fixes). It is now the only Windows backend that has never been through a compiler. See Windows backend.
  • Windows / WPFrun it. The backend and its sample head compile on macOS and on a windows-latest CI runner, but no window has ever been opened, so every runtime behaviour is intended rather than verified. Known gaps to revisit once it runs: .Animation (implicit layout transitions) is a no-op, PasswordBox has no placeholder, and ColorPicker offers a fixed palette rather than a full picker. See WPF backend.
  • Windows Formsrun the Skia host. Same status as WPF: compiles, never run. There is deliberately no native-control WinForms backend; see WinForms for the reasoning, which is not expected to change.
  • Skiaaccessibility bridge; WebView/Map native-overlay punch-through; caret placement and selection (the IME replaces the whole string, so edits always land at the end); keyboard avoidance; pan inertia/rubber-banding; dirty-rect repaint; the Windows MAUI head. The iOS/Android MAUI TFMs, the AndroidX reconciliation, finger scrolling, slider scrubbing and the soft keyboard have landed. See Skia backend.
  • Terminal / TUI — drive it by hand in a live terminal (focus order, mouse reporting, the alternate-screen lifecycle are all framework-supplied but unverified by us); wire the terminal-only controls Terminal.UI ships and the DSL has no node for — Table, DataGridControl, TreeView, CodeEditor, MarkdownControl, the chart family — through the TuiRenderers seam; consider a windowed List on Terminal.UI's own ListBox<T> virtualization. See Terminal/TUI.
  • WebGPU — a windowed host (only the headless one exists; mirror SampleApp.Skia.Silk); run it on Vulkan and D3D12, which are written but unexercised; real compositing layers so .Opacity on an overlapping subtree stops approximating; text shaping for scripts needing ligatures/reordering; image formats beyond PNG. See WebGPU backend.
  • Unity — open the package in a Unity 6 project and run it (it never has; it does now compile against UnityEngine reference assemblies via tooling/unity-compile-check.sh); verify IL2CPP/AOT; wire safe area (needs a host-facing entry point — SafeArea.Update is internal) and the soft keyboard off FocusChanged. Route B (node tree → UI Toolkit VisualElements) remains open if Unity-native inspectability ever matters more than reusing the verified Skia backend. See Unity backend.
  • MonoGame — pinch (GestureType.PinchPointerRouter.Pinch), safe area, automatic HiDPI RenderScale, and a run on the WindowsDX / Android / iOS heads (the assembly contract is the same, so they should just work). See MonoGame backend.
  • Godot — export to Android/iOS/desktop and confirm the native route really is dependency-free there; drive the gestures and the on-screen keyboard by hand; soft shadows on ellipses (Godot has no canvas blur, so today they are a solid offset ellipse); safe area. Route B (node tree → Godot Control nodes) stays open but is unattractive — it would trade SwiftUI layout semantics for Godot's container layout. See Godot backend.
  • Collection View — true windowed streaming (WinUI/GTK/Web); Web pull-refresh/load-more/windowing (needs JS-interop scrollTop); Swift load-more wiring. See Collection View → Deferred.

Framework-wide

  • Binary bridge protocol — replace JSON on the hot path.
  • Physical-device runs on iOS/Android (currently simulator/emulator verified).
  • Publish the combined SwiftDotNet + SwiftDotNet.Gtk + SwiftDotNet.Web (+ Graphics, Skia, WebGPU, Tui) as NuGet packages.
  • An arbitrary path primitive in ICanvas, behind a capability check rather than in the interface — adding it unconditionally would make every future self-drawing backend owe a full vector rasterizer. Skia gets it free; WebGPU would need something like Vello. Now scheduled as Phase 3 of the game surface plan, alongside blend modes and sprite atlases.