Native fidelity and native plugins on iOS, macOS and Android

Date: 2026-10-16. Asked by the owner: “some things already run in another demo, but we must see, the way Flutter or React Native do it, how to exploit a maximum of native in the UI and the behaviour, how this is maintained over time, and how we make it easy for iOS, macOS and Android.” Research only: no code changed. Read in main at bc0c360d.

This report adds to mobile-native-feel.md (written the same day: the audit of the phone feel and a 13-item plan). That report covers what feels wrong on a phone. This one covers the architecture: what Zinc imitates in its own drawing, what it hands to real native components and how it composites them, and the plugin model that keeps native code maintainable for years. Items that report already plans are named “MNF #n” and not repeated.

1. Verdict

Keep Flutter’s split, which Zinc already half follows: draw the content, imitate each platform’s behaviour, delegate to the OS what the OS owns (system chrome, privacy-mediated pickers, the text input client, accessibility, web content). Three pieces are missing, and they are the work:

  1. A platform profile in zinc:ui that the kits read by default (adaptive by default, unlike Flutter where the developer must ask).
  2. A <NativeView> host element: a leaf of the zinc:ui tree whose laid-out rect the host gives to a native view placed above the surface. It generalises what plugins/webview already does by hand on macOS.
  3. A mobile half for the plugin model: Zinc already has a typed spec with generated C++ glue (*.spec.ts + zinc native-gen, our Codegen/Pigeon), per-target files, a recording simulator, and zinc export that regenerates the Android project from SDL’s template (our CNG). It lacks the Java/ObjC side of the glue, a way for a plugin to add to the generated project (manifest, Gradle, Info.plist, entitlements), and lifecycle hooks into the activity and the app delegate.

The demo the owner means is most likely examples/system/desktop (native menu bar, dock badge, bounce, notifications, native context menu on macOS) together with examples/webview/hybrid (a real WKWebView over the Zinc surface) and the desktop-app template.

2. Inventory: what Zinc already does natively

“Native” = the OS does it; “drawn” = Zinc draws an imitation; “sim” = recording simulator only. ui.ts is lib/std/ui.ts.

FeatureWheremacOSAndroidiOSOthers
App menu bar, roles (About, Quit, Edit…)plugins/system/menu.ts, native/system.macos.mm; kit fallback lib/std/kit/menubar.tsx (native prop renders nothing)native NSMenunonenonekit-drawn (Linux, Pi, sim)
Context menumenu.popup (zinc:system); kit ContextMenu (menubar.tsx:136)native via menu.popup (examples/system/desktop/main.tsx:44); the kit’s ContextMenu is always drawndrawndrawndrawn
Tray, dock badge/bounce/menuplugins/system/tray.ts, dock.tsnative NSStatusItem, dockTilen/an/aLinux: SNI over D-Bus (planned, system-integration.md §3)
Notificationsplugins/system/notification.tsnative UNUserNotificationCenter (dev bundle src/tc/bundle.cpp)none (capabilities.json: notifications: false)nonesim
File dialogs, message boxesplugins/system/dialog.ts, system.macos.mm:393native NSOpenPanel/NSSavePanel/NSAlertnone (dialogs: false)nonesim
Global shortcuts, single instance, autostart, deep links, opener, power, rich clipboardplugins/system/*.ts, one op table plugins/system/ops.jsonnative (Carbon, SMAppService, CFBundleURLTypes, IOKit, NSPasteboard)nonenonesim
Dark modeui.setScheme('auto') + setSystemScheme (ui.ts:1764); zinc:system onAppearance (plugins/system/power.ts:25)native signal, but the app must wire it; RN Appearance reads only ZINC_COLOR_SCHEMEnot readnot readenv var
Accent colournonenot read (NSColor.controlAccentColor)not read (Material You)not read (tint)-
Safe areasenv(safe-area-inset-*), ui.setSafeArea (ui.ts:1769); RN SafeAreaView is a Viewn/ahook ready, not fed (MNF #3)not fed-
Status bar, back buttonRN StatusBar renders nothing; shell_sdl.cpp handles only background/foregroundn/akiosk/fullscreen in zinc.json (D123); back key unmappedn/a-
Keyboard, IMEhal_text_input -> SDL_StartTextInput (targets/macos/hal_sdl.cpp:1339); no SDL_EVENT_TEXT_EDITINGcommitted text only (no marked text)SDL’s hidden InputConnection (MNF 2.3)SDL’s hidden UITextField [unverified]in-app kit keyboard (kit/keyboard.tsx)
Scroll physicsui.ts:4302iOS rubber band on every platformiOS physics (wrong)iOS physicssame
Press feedbackdim to 0.6 while pressed (commit 9b4290a3)iOS-likeno rippleiOS-like-
FontsInter built in, ui.setFontSansnot the system fontnot Robotonot SF-
Haptics, vibrationnavigator.vibrate (lib/std/web.ts:1666) -> device query vibrate (src/host/sys_host.cpp:234)n/anonenonewebOS: real (PDL_Vibrate, plugins/display-gl/src/webos_device.cpp:98); sim panel
Wake lock, brightnesszinc:sys device queries (D122)-native (SDL screensaver hint, window brightness)-webOS: native
Web viewplugins/webview (src/webview.mm, index.ts)native WKWebView added as a subview over the SDL surface, placed by hand in logical pxnone (plugin.json targets macOS only)none-
Videoplugins/videodecoded with VideoToolbox, drawn by Zincnot portednot portedV4L2, GStreamer on webOS
Cameraplugins/cameranative AVFoundation capture, drawn by ZincnonenoneV4L2
Mapsplugins/mapdrawn by Zinc (MVT tiles)same codesame codesame
Platform identityPlatform.OS = platform() (lib/std/react-native.tsx:118)macosanswers linux (bug, MNF #1)would answer macos-
Platform-specific filesZN-745 (Backlog): x.android.tsx, x.ios.tsx, x.native.tsx----
Permissions to manifestszinc.json permissions -> <uses-permission> (src/cli_core.cpp:1020)bundle Info.plist (bundle.cpp:55)INTERNET, CAMERA, RECORD_AUDIO, locationZN-623 (usage strings planned)-
Native projectszinc export --target android regenerates the Gradle project from SDL’s android-project each time (cli_core.cpp:941-1080).app dev bundle, .dmgregeneratedZN-623 tonight-

Reading: macOS is well served for desktop chrome (one plugin, 14 modules, a typed op table, a simulator). The phones have the engine and the shell, but no native service yet: no plugin lists an android or ios target (plugins/*/plugin.json), and Android export builds a plugin’s linux sources in its place (cli_core.cpp:981, a ponytail note pointing at ZN-628).

3. How the references do it

Flutter (own renderer, the closest model)

  • Automatic per-platform behaviour keyed on TargetPlatform: page transitions (zoom on Android, slide with parallax and edge swipe-back on iOS), back (Android key pops the route), scroll physics (Clamping + glow vs Bouncing), scrollbars, stacked flings and status-bar tap on iOS, Roboto vs San Francisco, adaptive icons, haptics (long-press selection vibrates on Android, picker ticks on iOS), text editing gestures (tap, long press, double tap, handles) and the selection toolbar. Not automatic: the widget look. Material does not turn Cupertino on iOS; the developer picks Cupertino widgets or .adaptive() constructors (Switch, Slider, Checkbox, Radio, AlertDialog, CircularProgressIndicator, RefreshIndicator), and app bars, bottom navigation and text fields need manual branching. https://docs.flutter.dev/ui/adaptive-responsive/platform-adaptations
  • Keeping the looks current is the expensive part. On 2026-09-09 Flutter moved Material and Cupertino out of the framework into material_ui and cupertino_ui (1.0.0, semver, weekly releases), because design changes were tied to the three-month SDK release (up to 16 weeks late) and upgrades for fixes dragged UI changes along. Liquid Glass (iOS 26) and Material 3 Expressive were “work underway”, not shipped. https://flutter.dev/blog/decoupling-material-cupertino
  • Platform views, the native views inside Flutter UI. Android: Texture Layer (default; good performance, but jank in fast scrolling web views, SurfaceView loses accessibility), Hybrid Composition (full fidelity, but merges the raster and platform threads and lowers Flutter’s frame rate; before Android 10 a GPU copy per frame), HCPP (experimental since 3.44, API 34+ and Vulkan only), Virtual Display (extra buffers, jank). https://docs.flutter.dev/platform-integration/android/platform-views. iOS: hybrid composition, the UIView is appended to the hierarchy; ShaderMask and ColorFiltered unsupported over it; mitigation: a screenshot texture during animations. https://docs.flutter.dev/platform-integration/ios/platform-views. macOS AppKitView: hybrid composition, “isn’t fully functional”, no gestures yet; the raster thread waits for the platform thread. https://docs.flutter.dev/platform-integration/macos/platform-views
  • Plugins. Federated: an app-facing package, a platform-interface package (the contract), one implementation package per platform; “endorsed” implementations come with the app-facing package, others can be added by the app; an implementation may be pure Dart over FFI (dartPluginClass). https://docs.flutter.dev/packages-and-plugins/developing-packages. Pigeon generates both sides of the channel (Dart + Kotlin, Java, Swift, ObjC, C++, GObject) from one Dart declaration file (@HostApi, @FlutterApi, async, event channels), to “remove the necessity to manage strings across multiple platforms”; generated code is an internal detail. https://pub.dev/packages/pigeon
  • Text input and accessibility are native bridges: a TextInputConnection to a hidden native client mirroring text, selection and composing range, and a semantics tree sent to the platform’s accessibility API. https://api.flutter.dev/flutter/services/TextInputConnection-class.html, https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility

React Native, new architecture, and Expo

Others, briefly

The lesson across all of them: the self-drawing engines (Flutter, Compose) converged on the same three native bridges (text input, accessibility, platform views for leaves) and adapt behaviour automatically while the look is a library that must be updated after each OS redesign. The native-view engines (RN, MAUI, Lynx) get fidelity for free but pay per platform in bugs and never run where those toolkits do not exist. Everyone generates the native glue from one typed spec and, the mature ones, the native project too.

4.1 Decision A: where fidelity comes from (RULES §4, max 70)

OptionFit x3Perf x3Size x2Maint x2Licence x2Port x1Effort x1Total
Own drawing + platform profile (behaviour, tokens) + native leaves and bridges554455364
Own drawing, one look and one behaviour everywhere255555561
Native widgets for host components (RN/MAUI)233251137

Native widgets would break the Pi, ESP32, webOS and pixel goldens (RULES §1); “one look” ignores the owner’s question.

4.2 (a) What Zinc imitates in its own drawing: the platform profile

One value, ui.platformStyle(): ios | android | macos | desktop, defaulted from platform(), set by zinc.json "platformStyle" or ZINC_PLATFORM_STYLE (tests). MNF #2 defines it; what this report adds:

  • Two layers, versioned separately in lib/std/platform/: behaviour (scroll physics, fling stacking, overscroll, page and modal transitions, back and swipe-back, long-press and selection gestures, press feedback, haptic cues, keyboard return and Tab behaviour, scrollbars) is automatic and not themeable; look (radii, shadows, control sizes, font family, motion durations) is tokens the kits read and an app may override. Flutter’s decoupling shows the look is what changes each WWDC; tokens make that a data change in lib, not an engine change.
  • Adaptive by default in the kits and zinc:react-native (Switch, Button, ActivityIndicator, RefreshControl, Alert, checkboxes, sliders, menus): Flutter makes the developer opt in; Zinc’s users (education, makers) should not have to know. variant="neutral" keeps today’s look for apps with their own design. Nuxt UI and shadcn kits keep their brand look and take only the behaviour layer and the system font.
  • macOS counts as a platform: system font (SF through CoreText at run time), accent colour, native context menus by default, Cmd shortcuts, overlay scrollbars, no rubber band on non-trackpad wheels [unverified: SDL’s precise-scroll flag].
  • Proof without a device: goldens per profile under ZINC_PLATFORM_STYLE, a scripted fling with the virtual clock against the published curves (MNF #6), as Flutter’s debugDefaultTargetPlatformOverride.

4.3 (b) What Zinc delegates to real native components

The rule: delegate what the OS owns or mediates; draw what lives inside the app’s content.

Delegate (native)WhyDraw (Zinc)Why
Menu bar, dock, tray, notifications, status bar style, share sheetsystem chrome, outside the windowbuttons, switches, lists, tabs, navigation, sheets, dialogs inside the appone renderer, goldens, every target
Photo, document, contact pickers; permission prompts; paste promptprivacy: the OS grants access only through its own UI (PHPicker, SAF)date/time pickers (drawn per profile; native as an option, MNF #12)they live in the content
Text input client (IME, autocorrect, dictation, autofill)only the OS has the IMEthe text, caret, selection, handles, magnifierFlutter and Compose draw these too
Accessibility tree to VoiceOver/TalkBackscreen readers read only native APIsthe focus ring, the visual state-
Web content, ads, payment sheets, platform map SDKs when required by licencea browser engine or a vendor SDKvideo, camera preview, MVT maps, 3Dalready decoded into Zinc images, so they clip, scroll and composite for free

Decision B: compositing native views with Zinc’s frame

OptionFitPerfSizeMaintLicencePortEffortTotal
Overlay: native view above the surface at its node’s laid-out rect; Zinc content never drawn over it (hidden or snapshotted while a Zinc overlay covers it)455554566
Hybrid: split the Zinc frame into layers below and above each native view (several surfaces)534253150
Texture: native view drawn into a texture Zinc composites (Android TextureView/VirtualDisplay)324251240

The texture route has no general iOS equivalent; hybrid is what costs Flutter frame rate (merged threads) and needs API 34 + Vulkan for its fast form, beyond a Nexus 7. The overlay is what plugins/webview does on macOS today; it must become a layout element:

  • <NativeView kind="webview" props={...} /> is a zinc:ui leaf. After layout, the host sends the plugin the node’s rect in logical px, its clip (the intersection of scroll ancestors), visibility and opacity, only when they change. macOS: an NSView subview of the SDL content view (done); iOS: a UIView subview of SDL’s view controller’s view; Android: a child of SDLActivity’s mLayout (a RelativeLayout holding the SDLSurface, SDLActivity.java:460 in the vendored SDL3), above the SurfaceView.
  • Known costs, accepted and documented: the native view moves one frame late during a scroll (so no native views inside scrolling lists); no Zinc popover over it (the layer system hides it, or swaps in a snapshot as Flutter suggests); no shaders, blur or rounded clips applied to it beyond a rectangle (iOS: layer.cornerRadius is cheap and can be offered).
  • Gestures: a touch inside the rect goes to the native view; zinc:ui’s gesture arena does not see it.

4.4 (c) The plugin model that stays maintainable

What Zinc has, mapped to the references:

Reference pieceZinc today
Federated app-facing packagethe plugin’s index.ts (plugins/webview/index.ts)
Platform interface / Codegen specnative/<x>.spec.ts (plugins/webview/native/webview.spec.ts), checked at call time by signature letters (docs/reports/zinc-next-native-abi.md)
Pigeon / Codegen outputzinc native-gen -> zinc_native_<x>.h + thunk (C++ only)
Per-platform implementations<x>.host.cpp, <x>.<target>.cpp, src/*.mm, selected by plugin.json targets
Op table for wide APIsplugins/system/ops.json -> ops.d.ts, ops.gen.h, permission ids, simulator results (tools/system-ops, tests/t0/system_ops.sh)
Mock / test implementation<x>.sim.ts, recording simulator, deterministic runs
Permissionszinc.json permissions, checked at compile time and natively
CNGzinc export --target android regenerates from SDL’s template each time

What to add, in order:

  1. Mobile targets in plugin.json, declarative, merged by zinc export (Expo’s config plugins without code): "android": { "sources": ["native/android/WebviewPlugin.java"], "gradle": ["androidx.webkit:webkit:1.x"], "manifest": { "permissions": [...], "queries": [...], "application": "<xml fragment>" }, "minSdk": 24 }, "ios": { "sources": ["native/webview.ios.mm"], "frameworks": ["WebKit"], "plist": { "NSCameraUsageDescription": "..." }, "entitlements": {...}, "spm": [...] }. An app adds raw fragments the same way under zinc.json targets.android / targets.ios (Expo’s ios.infoPlist). No imperative hooks until a real plugin cannot be expressed this way.

  2. Generated glue for Java and Objective-C (Decision C), from the same spec.ts: zinc native-gen --android writes a Java interface and the JNI thunk; --ios an Objective-C protocol and the thunk. The author implements the interface in Java or Kotlin and the protocol in ObjC or Swift (@objc). Callbacks and promises keep the native ABI’s cb_post / promise_resolve, so threads stay safe (Tauri’s note: Android commands land on the main thread; long work must move off it).

    Option (Decision C)FitPerfSizeMaintLicencePortEffortTotal
    Generated Java + ObjC glue; author writes Java/Kotlin and ObjC/Swift555454365
    Generated Kotlin + Swift glue (Expo DSL)543453256
    C++ only, JNI and objc_msgSend by hand (webview.mm style)355254456

    Java and ObjC need no extra compiler or runtime (SDL’s glue is already Java and ObjC); Kotlin and Swift remain the author’s choice.

  3. Lifecycle subscribers: the generated ZincActivity and app delegate forward onNewIntent, onActivityResult, permission results, openURL, push tokens and background events to every plugin that declares them (Expo’s native subscribers), so no plugin edits the shell.

  4. One plugin per domain with backends per platform, as zinc:system (one op table, sim/macos/linux backends, D-record in system-integration.md §3): add ios and android backends to zinc:system for what exists there (notifications, share, pickers, clipboard, opener, deep links, haptics) instead of new plugins. The op table already generates typings, the permission gate and the simulator; extend tools/system-ops to emit the Java/ObjC dispatch stubs.

  5. Tests per platform without a device: every plugin keeps its sim.ts; the generated project tree is a golden (ZN-623 #1); the same TS test runs on the Android emulator and the iOS simulator in the cross-platform pass.

  6. Keeping up with OS releases: one pins table (NDK, AGP, Gradle, compile/target SDK, min API, Xcode, min iOS: the constants kNdk, kAgp, kGradle, kMinSdk already in cli_core.cpp) shown by zinc doctor, bumped once a year after WWDC and the Android release (Expo’s SDK table); SDL3 updates absorb most shell changes; looks are tokens (4.2). A bump shows as a golden diff of the generated projects.

4.5 (d) Making it easy

  • One command: zinc run --target android|ios = export, Gradle or xcodebuild, install (adb, simctl), launch, logs; and the player app (ZN-631, “Zinc Go”) so a beginner sees a phone run without Android Studio or Xcode.
  • Defaults: adaptive profile on, system font, dark mode auto wired by the host (not by the app), safe areas and back handled by the kits’ screens, permissions inferred from imported modules and named by zinc check.
  • Escape hatches, narrowest first: Platform.select and Platform.OS (fixed, MNF #1); platform files x.ios.tsx / x.android.tsx / x.native.tsx (ZN-745); ui.platformStyle forced per subtree; a plugin’s per-target native file; raw manifest and plist fragments in zinc.json. Never “eject and edit the generated project”.

5. Next steps, ranked

Sizes: S (a day), M (days), L (a week+). Backlog frozen: existing tasks are named; the rest are later.md lines.

#StepSizeFilesTask
1Platform identity: platform() = ios/android, Platform.select branches, Version, coarse pointer at startSsrc/host/sys_host.cpp:570, lib/std/react-native.tsx:115-130MNF #1 (ZN-625 area)
2Platform profile with behaviour and look layers, adaptive kits by default, goldens per profileMlib/std/ui.ts, new lib/std/platform/{ios,android,macos}.ts, lib/std/kit/*.tsx, lib/std/react-native.tsxMNF #2, #11
3System signals fed by the host on all three: theme (SDL_GetSystemTheme, default auto), accent colour, safe area, font scale, locale, reduce motion, backMtargets/macos/hal_sdl.cpp, src/app/shell_sdl.cpp, ui.ts:1764-1769, RN AppearanceZN-625
4Mobile plugin model: android/ios in plugin.json (sources, gradle, manifest, plist, entitlements), native-gen --android/--ios, lifecycle subscribers, refuse a plugin without the targetLsrc/tc/plugin_build.*, src/cli_core.cpp:975-1080 (move Android export to its own file), ZN-623 generator, src/frontend manifest parserZN-628
5<NativeView> element + webview ported to it, then Android WebView and iOS WKWebViewMlib/std/ui.ts, src/host/gfx_host.cpp, plugins/webview/{index.ts,native/,src/}ZN-628 (webview), MNF #12
6Text input connection: macOS marked text first (SDL_EVENT_TEXT_EDITING), then Android InputConnection, iOS UITextInputLtargets/macos/hal_sdl.cpp, ui.ts text fields, shell Java/ObjCZN-626, MNF #4
7zinc:system backends for phones: notifications (UNUserNotificationCenter shared with macOS), share sheet, photo/document pickers, haptics, opener, deep links; flip capabilities.jsonMplugins/system/ops.json, native/system.ios.mm, native/android/*.java, targets/capabilities.jsonZN-628
8Kit ContextMenu uses menu.popup when menu.native (as MenuBar does)Slib/std/kit/menubar.tsx:136, nuxt ContextMenulater.md
9System UI font at run time (CoreText on macOS/iOS, Android system fonts) as the profile’s default font-sansSsrc/text/, ui.ts:2508MNF #10
10Accessibility: tree ids and actions, macOS NSAccessibility first, AccessKit checked before the phone bridgesLui.ts, hostZN-630, MNF #8
11Pins table + zinc doctor row + yearly bump checklist + generated-project goldensSsrc/cli_core.cpp constants, docs/targets/later.md
12`zinc run —target androidios` in one command; the playerMsrc/cli_core.cpp, src/dev

Top five by value per effort: 1, 3, 2, 4, 5. Step 1 is a day and unblocks every per-platform rule; 3 makes the first run look right (dark mode, notch, back); 2 makes the feel right on Android and iOS; 4 and 5 are what lets anyone add native features without touching the engine.

6. What NOT to do

  • No native widgets for host components (RN, MAUI): it breaks the one renderer that runs on the Pi, ESP32, webOS and the goldens.
  • No pixel-perfect Cupertino or Material clones chasing each redesign: Flutter needed a whole decoupling to keep pace and still had no Liquid Glass after a year. Copy behaviour; keep the look as tokens.
  • No hybrid composition (Zinc layers above and below native views): Flutter’s most expensive mode; out of reach of GLES 2 devices.
  • No native views inside scrolling lists, and no video, camera or maps as native views where Zinc already decodes or draws them.
  • No hand-written string channels: every native call goes through a generated spec (spec.ts or an op table).
  • No hand-maintained or committed android/ and ios/ trees, no imperative project-editing hooks; regenerate (CNG).
  • No second HAL per platform while SDL3 does the job (D59), no dlopen on phones, no new plugin per small feature when a domain plugin with backends exists (zinc:system, system-integration.md §3).
  • No native text field drawn over Zinc’s to get the IME: mirror the state into a hidden native client and keep drawing the text.

7. Not verified

  • Nothing was built or run for this report; no phone, emulator or simulator.
  • SDL3’s iOS view controller as the place to add subviews, and how a view over the Android SurfaceView behaves on API 25.
  • Compose Multiplatform 1.11’s UIView text input: from a secondary source (the JetBrains post returned HTTP 403).
  • Whether SDL reports precise (trackpad) versus wheel scrolling on macOS well enough for per-device physics.
  • The Expo autolinking and config-plugin pages were cited, not re-read; the CNG page was.

8. Lines for backlog/later.md (the coordinator adds them)

  • Native fidelity: a platform profile in two layers, behaviour (automatic) and look (tokens in lib/std/platform/), adaptive by default in the kits and zinc:react-native; macOS is a profile too (system font, accent, native context menus) (docs/reports/native-fidelity-and-plugins.md 4.2).
  • ZN-628: a <NativeView> zinc:ui leaf whose rect, clip and visibility the host forwards to a native view above the surface (NSView/UIView subview, Android view over SDL’s SurfaceView), hidden or snapshotted under Zinc overlays; port plugins/webview to it (4.3).
  • ZN-628: plugin.json android/ios entries with sources, gradle deps, manifest and plist fragments, entitlements, merged by zinc export; zinc native-gen --android/--ios writes Java-interface and ObjC-protocol glue from the spec; lifecycle subscribers in the generated activity and app delegate (4.4).
  • ZN-628: zinc:system gets ios and android backends (notifications, share, pickers, haptics, opener, deep links) on the same op table; tools/system-ops emits their dispatch stubs (4.4).
  • Native fidelity: the kit ContextMenu uses menu.popup when menu.native, as MenuBar does (lib/std/kit/menubar.tsx:136) (5 #8).
  • Native fidelity: one pins table (NDK, AGP, Gradle, SDK levels, Xcode, min iOS) shown by zinc doctor, bumped yearly after WWDC and the Android release, with goldens of the generated projects (4.4 #6).

Edit this page on GitHub ↗