Compare commits

...
136 Commits
Author SHA1 Message Date
Bitsy 30cbbc4be1 fix(demo): mingwX64 compile — currentResidentMb via expect/actual
platform.posix.popen/pclose don't exist on mingw (Windows uses _popen/_pclose),
so the soak-test RSS helper failed :demo:compileKotlinMingwX64 (broke the v0.2.0
publish). Make currentResidentMb() an expect: posix (appleMain/linuxMain) reads
`ps -o rss=`, mingw returns -1 (the soak gate runs on macOS/Linux). Verified
:demo:compileKotlinMacosArm64 green.
2026-07-18 10:21:50 +02:00
Bitsy 4a196c9c23 feat(bridge): expose the Compose versions the port tracks; catalog as single source
The bridge now advertises the official Compose versions its vendored sources
build against, so consumers align their coords without reading compose.properties
by hand (material3 is versioned separately upstream, an easy mismatch).

- version catalog gains composeMaterial3 + composeRuntime alongside compose; the
  three become the single source consumed by the plugin and the JVM parity leg
- plugin stamps version/compose/composeMaterial3/composeRuntime into
  bridge-version.properties and exposes them via a composeDesktopNative { }
  extension (+ project.extra mirror for the settings-apply case)
- demo/apidemo/material-symbols + the ui modules now source runtime + the JVM
  forcing versions from the catalog instead of hardcoding
- fix doc bug: override property is composeDesktopNative.version (was composeSdl.version)
- READMEs updated to use the exposed versions
2026-07-18 01:41:01 +02:00
Bitsy a62a17a7bf docs: add TODO.md — audited no-ops/stubs/hardcodes + what's missing for stable 1.12
Audited project actuals (not vendored) across text input/IME, accessibility,
locale/i18n, cursor, fonts, drag-and-drop, platform info, and the SDL renderer.
Each entry carries file:line + severity. Linked from CLAUDE.md's doc map.
2026-07-18 01:24:49 +02:00
Bitsy b0530cdccb docs: consolidate renderer docs into RENDERER.md; add TOOLING.md + versioning/release runbook; clean README + CLAUDE
- README rewritten in the clean CM-inspired style (concept, quickstart, apps); links the bridge example repo
- new TOOLING.md: build-sdl, vendor sync + drift/clean, parity, probe, profiler, coverage, verify-mac, + version map and ref-bump/release runbooks
- new RENDERER.md consolidates RENDERER_CONVERGE.md + RENDERER_TASKS.md (architecture, decisions, learnings, open work); old planning docs removed
- CLAUDE.md: add a documentation map, trim tooling/build/vendor sections down to pointers
- repoint all code-comment/script references to RENDERER.md
2026-07-18 01:11:26 +02:00
Bitsy e483a4b683 Merge renderer-refactor: upstream-tracking convergence (G1) + leak fix + guardrails
Brings the renderer refactor to main. Highlights (all MAC-VERIFY green, both legs):

- B2/B6 convergence: the Skia leg (macOS/Linux) now draws through upstream Compose's
  OWN engine verbatim — SkiaBackedCanvas + SkiaGraphicsLayer + SkiaGraphicsContext
  (skiko RenderNode). Retired the hand-rolled SkiaCanvas/GraphicsLayer. Bonus perf:
  draw ~1.75ms -> ~0.2ms (real display-list caching). Text stays the port's SdlParagraph
  (skiko-metric-aligned, P3.1 ~2% parity); B6.3 (upstream text) deliberately skipped.

- Guardrails/harness (Phase 0): target-aware parity gate with golden baselines +
  render-to-quiescence; MAC-VERIFY runbook (verify-mac.sh); manual-vendor drift tripwire +
  vendor-clean check. Fixed a real HiDPI parity-harness bug (crop-vs-resize).

- Memory leak FIXED: the port never swept OwnerSnapshotObserver.clearInvalidObservations();
  disposed scopes' observations leaked (worst on ripple draws). Now swept per-frame; soak
  probe added and wired as a verify-mac gate. All-63-screens soak 260->458MB (was leaking)
  -> flat.

- Sync-tax win, proven: O.2 bump beta02 -> beta03+dev4483 needed ZERO manual-vendor
  reconciliation (the upstream graphics/layer files re-sync verbatim now).

- RenderBackend.drawRoot decoupled from ComposeRootHost (removes a ui<->ui-graphics cycle;
  the module split itself is shelved — the sdl3 cinterop is a shared substrate, infeasible
  as CONVERGE §6 specified).

CAVEAT: Windows/mingwX64 is UNVERIFIED — this whole line of work was Mac-only. WIN-SMOKE
(mingw build + scripts/probe + publish dry-run) is still required before shipping.
Stabilization: re-pin to clean v1.12.0 + close the JVM Maven skew when 1.12.0 stable ships.
2026-07-18 00:44:30 +02:00
Bitsy 23eaa5bc8b docs: O.2 second bump (beta03+dev4483) + stabilization plan (clean v1.12.0) 2026-07-18 00:40:22 +02:00
Bitsy 18286f689b chore(vendor): O.2 bump — both pins v1.12.0-beta02 -> v1.12.0-beta03+dev4483
Second real ref-bump through the O.2 runbook. No CLEAN beta03/rc/stable tag exists yet
on either repo, and beta03 isn't on Maven Central, so this is the latest DURABLE
(fetchable-by-name) ref — a beta03 dev snapshot. VENDORED (native) side only; the JVM
parity leg's Maven forcing stays at beta02 / m3 alpha03 (latest published) — a documented
native(beta03+dev) vs JVM(beta02) skew until beta03 publishes to Maven.

Flow: bump compose.properties (both pins) -> sync (1559 files) -> DRIFT-CHECK: all 9
manual vendors' upstream bases UNCHANGED beta02..beta03+dev4483 -> re-stamped their
VENDOR-BASE refs, ZERO hand-reconciliation (the B2/B6 sync-tax win, validated for real:
the newly-upstream Canvas/GraphicsLayer/GraphicsContext re-synced verbatim) -> builds
green (ui both legs + metadata + material3) -> full MAC-VERIFY ALL GREEN both legs
(demo+apidemo, 10 probes incl. soak gate, parity PASS both legs, perf, vendor-clean).

Windows WIN-SMOKE still pending (no Windows host this session).

STABILIZATION PLAN (user): cut a stable compose-desktop-native when Compose 1.12.0
STABLE releases — at that point re-pin both refs to the clean `v1.12.0` tag and bump the
vComposeJvmVersion forcing (demo/apidemo/material-symbols) to match, closing the skew.
2026-07-18 00:39:41 +02:00
Bitsy 319c550af4 docs: module split re-shelved — sdl3 cinterop is a shared substrate (infeasible as §6 specified) 2026-07-18 00:03:33 +02:00
Bitsy c7608b6404 docs: module split un-shelved — cycle finding + staged plan, step 1 (decouple) done 2026-07-17 23:51:07 +02:00
Bitsy c71febbb23 refactor(ui): decouple RenderBackend.drawRoot from ComposeRootHost (module-split step 1)
Prerequisite for extracting :ui-graphics. The render backends (SkiaRenderBackend /
Sdl3RenderBackend, which would move to :ui-graphics) referenced
com.compose.sdl.node.ComposeRootHost (which stays in :ui), while :ui node code
references androidx.compose.ui.graphics.Canvas (:ui-graphics) — a ui<->ui-graphics
dependency cycle §6 didn't account for (it assumed pure relocation).

RenderBackend.drawRoot now takes a (Canvas) -> Unit instead of the host: the backend
builds the frame Canvas and hands it to the callback; ComposeWindow passes
`{ canvas -> host.drawRoot(canvas) }`. Backends + RenderBackend no longer reference the
node/host layer, so they can live in :ui-graphics with no back-edge to :ui.

Behaviour-preserving (inDraw == host::drawRoot). Verified: both legs + metadata compile,
demo builds, nav3/click/scroll probes PASS, skia parity PASS.
2026-07-17 23:50:21 +02:00
Bitsy 5ab3bd6b97 docs+ci: P2.2 done — leak fixed, soak wired into verify-mac gate 2026-07-17 23:34:14 +02:00
Bitsy c59caf7272 fix(ui): sweep invalid snapshot observations each frame — fixes the P2.2 leak
ROOT CAUSE (found via the --soaktest reproducer + macOS heap/vmmap/leaks + a
component bisection): the port never called
`OwnerSnapshotObserver.clearInvalidObservations()`. Upstream RootNodeOwner runs it
after every measure pass; without it, snapshot read-observations whose scope became
invalid on dispose (detached nodes, destroyed layers, disposed DRAW scopes) linger in
the observer forever. Each lingering observation pins its observed object graph via a
K/N ExternalRCRef, so every composition dispose leaked — most visibly on ripple
indication draws (each Button read animatedAlpha/colors in draw, whose layer draw-scope
observation was never swept), pinning the whole button subtree.

FIX: ComposeRootHost.measureAndLayout() now calls
`fOwner.snapshotObserver.clearInvalidObservations()` after the measure/layout pass,
mirroring upstream. (Complements the earlier onDetach `snapshotObserver.clear(node)`.)

RESULT (soak, current RSS which drops after GC): the leak is GONE.
  Counter   123->176MB  =>  113->113MB
  Images    132->199MB  =>  118->119MB
  LazyGrid  133->204MB  =>  113->115MB
  ALL 63 screens x6  260->458MB (FAIL) => 238->240MB (PASS)
Diagnosis narrowed it: not layers/RenderNodes (live counts flat), not nodes
(attach/detach balanced), not per-frame (static flat), not caches (bounded), not true
malloc leaks (`leaks` clean) — the growth was entirely the K/N GC heap (vmmap
Memory Tag 246), pinned by ExternalRCRefs from unswept observations.

MAC-VERIFY ALL GREEN both legs (10/10 probes, parity PASS, perf unchanged —
clearInvalidObservations is a cheap per-frame validity sweep). Soak diagnostics
(--soaktest: current RSS, CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES) retained.
2026-07-17 23:32:59 +02:00
Bitsy c36f11ab4f docs: P2.2 focused session — onDetach observation-clear fix + residual leak characterized 2026-07-17 22:24:27 +02:00
Bitsy 7189433e5b fix(ui): clear detached node's snapshot observations (partial P2.2)
ComposeOwner.onDetach now calls snapshotObserver.clear(node), exactly as upstream
RootNodeOwner.onDetach does. Without it, every disposed LayoutNode's measure/layout/
draw read-observation scopes accumulated in the OwnerSnapshotObserver forever — an
unbounded (if small) growth over a long session. Matches upstream verbatim; MAC-VERIFY
ALL GREEN both legs (10/10 probes, parity PASS, perf unchanged).

Also lands the P2.2 leak-hunt diagnostics on --soaktest:
  - current RSS (via `ps`, drops after GC) instead of getrusage peak (monotonic) —
    the correct leak metric; verdict now uses it.
  - CDN_SOAK_SCREEN=<name> to bisect one screen; CDN_SOAK_STATIC=1 to isolate
    per-frame vs per-mount.

Leak-hunt status (documented in RENDERER_TASKS.md): the residual leak is real, linear,
~141KB/mount (Counter), referenced (macOS `leaks` shows no malloc leaks in our code),
survives GC, and is NOT nodes/layers/RenderNodes (all balanced by exact count),
per-frame (static flat), or the skia GrContext budget (linear, no plateau). It needs a
skia-aware heap profiler to pin exactly — a follow-up. This observation-clear fix is one
confirmed contributor fixed.
2026-07-17 22:23:56 +02:00
Bitsy dadb9e45d4 docs: P2.2 leak hunt characterized + D2-D6 evaluated (all deferred, rationale) 2026-07-17 21:44:20 +02:00
Bitsy c1fabc5b5c test(demo): P2.2 leak hunt — soak single-screen bisect mode + characterization
Added CDN_SOAK_SCREEN=<name> to the soak (repeat ONE screen 40x/cycle) + key(vShown)
to force a full dispose+recompose each advance, to bisect the leak the soak found.

Leak hunt findings (a real, pre-existing, slow leak — reproducer in --soaktest):
  - Layers are CLEAN: a live-OwnerLayer counter stayed flat (5/7/21/24) while RSS
    climbed — destroy()->releaseGraphicsLayer() runs correctly. (Instrumentation used
    to prove this, then reverted to keep the vendored GraphicsLayerOwnerLayer clean.)
  - Not GC/cleaner lag: Counter over 10 cycles climbs ~linearly 114->176MB (no plateau).
  - Not animation churn: persists with infinite animations disabled.
  - Bisected: a BASELINE composition-machinery leak (~0.15MB/mount, hits even trivial
    Counter) + CONTENT amplification on image/vector/list screens (Images/LazyGrid
    ~0.55MB/mount). Both legs; SDL ~5x worse (offscreen textures).
  - Ruled out: the image decode caches (SkiaImageCache, components-resources
    ResourceCaches) are BOUNDED (keyed by path/id; revisits hit, no growth).
  - Remaining suspects (need heap tooling, out of scope here): composition/snapshot
    observation retention (baseline) + DrawCache/vector offscreen bitmap lifetime
    (content). A precise fix is a dedicated focused session.

Soak stays a standalone diagnostic (not a verify-mac gate) until the leak is fixed.
2026-07-17 21:41:23 +02:00
Bitsy 5def89f182 docs: Phase 2 — P2.1 audit done, P2.3 done via B6.2, P2.2 soak added (found pre-existing leak) 2026-07-17 21:18:00 +02:00
Bitsy 4bd9ca234e test(demo): P2.2 soak probe — cycle all screens x N, track peak RSS
--soaktest cycles through every registered screen kCycles times (CDN_SOAK_CYCLES
override, default 3) in ONE process, disposing each via a changing key() so
composition + layer (skiko RenderNode / SDL node + texture) allocate/release churn
is exercised repeatedly. Disables infinite animations + uses virtual frame time so
RSS reflects lifetime, not animation churn. After each full cycle: GC.collect() then
record peak RSS (getrusage ru_maxrss, MB; macOS-bytes/Linux-KB disambiguated by
magnitude). Verdict: PASS iff last-cycle peak stays within a ceiling of cycle-1 peak.

FINDING (both legs): the probe reveals a real pre-existing memory leak. Peak RSS
climbs ~linearly and does NOT plateau over 6 cycles: skia 264->458MB (~40MB/cycle),
SDL 523->916MB over 3 (~195MB/cycle). SDL (untouched by B6) leaks ~5x worse, so it
is NOT B6-introduced. Persists with animations disabled, so it is composition/layer/
cache teardown, not animation-state churn; SDL's larger delta points at offscreen
textures. The layer release path (destroy -> releaseGraphicsLayer -> close) is wired,
so it's deeper — needs a dedicated heap-profiled hunt. Kept as a standalone diagnostic
(NOT wired into verify-mac's gate) until the leak is fixed.
2026-07-17 21:16:57 +02:00
Bitsy 924229fafe docs: B6.3 skip decision + P1.8 sync-tax measured (~8.5x cut) — B2 convergence complete 2026-07-17 20:57:51 +02:00
Bitsy 2314fca8ee docs: B6.2 done (= P1.4-P1.7) — upstream GraphicsLayer on skia; perf 1.75ms->0.2ms 2026-07-17 20:14:11 +02:00
Bitsy 2210d1fa01 feat(ui): B6.2 — Skia leg uses upstream GraphicsLayer + GraphicsContext
The core B2 convergence (RENDERER_CONVERGE.md §4; = P1.4/P1.5/P1.6/P1.7). Now the
skiko canvas is upstream's SkiaBackedCanvas (B6.1), un-refuse + vendor upstream's
own layer engine VERBATIM: SkiaGraphicsLayer.skiko.kt (actual class GraphicsLayer
backed by org.jetbrains.skiko.node.RenderNode), SkiaGraphicsContext.skiko.kt
(owns a RenderNodeContext), Blur.skiko.kt. ChildLayerDependenciesTracker was already
vendored (commonMain), so SkiaGraphicsLayer compiles verbatim — no manual edit.

Per-renderer wiring (disjoint targets):
  - GraphicsContext seam (P1.3): skiko createGraphicsContext -> SkiaGraphicsContext;
    SDL keeps ProjectGraphicsContext (moved nativeMain -> sdlRendererMain along with
    the createProjectGraphicsLayer factory, now a plain internal fun; the commonMain
    expect is dropped — only SDL uses it).
  - Blur.native.kt (port no-op) moved nativeMain -> sdlRendererMain; skiko gets
    upstream Blur.skiko.
  - Deleted the transient skiko port cluster: GraphicsLayer.native.kt,
    NativeRenderNode.kt, DeferredRenderNode.kt, NativeRenderNode.skia.kt,
    GraphicsLayerFactory.native.kt.

GraphicsLayerOwnerLayer stays SHARED and compiles against BOTH GraphicsLayer actuals
(the API-parity invariant, P1.7 — enforced by the metadata compile). Hit-testing
(shared prepareLayerTransformationMatrix) agrees with the new skiko-RenderNode draw
transform — clicktest/scrolltest/backtest PASS on transformed layers.

MAC-VERIFY ALL GREEN both legs: builds (demo+apidemo), 10/10 probes, parity PASS
(layer screens GraphicsLayer/Shadows/DragAndDrop/Animation byte-stable — the swap is
visually transparent), metadata + drift + vendor-clean pass. PERF WIN: skia draw
1.75ms->0.2ms on LazyColumn/Tabs (skiko RenderNode record-once/replay display-list
caching vs the port's per-frame replay).
2026-07-17 20:12:36 +02:00
Bitsy 746d3cca9c docs: B6.1 done — Skia leg on upstream SkiaBackedCanvas, MAC-VERIFY green 2026-07-17 00:24:21 +02:00
Bitsy 7f5773fc3c feat(ui): B6.1 — Skia leg draws through upstream SkiaBackedCanvas
Full upstream Skia GRAPHICS pipeline on the skiko leg (RENDERER_CONVERGE.md §4,
the enabler for B2). The composition's frame + offscreen drawing now goes through
upstream's SkiaBackedCanvas / SkiaBackedPaint / SkiaShader / SkiaImageAsset /
ImageBitmap.skiko instead of the project SkiaCanvas.

Split the port graphics actuals per-renderer (createRenderBackend disjoint-target
trick): CanvasPaintActuals.native.kt (Paint/Shader/all gradient shaders/
ActualImageBitmap/createImageBitmap/ActualCanvas/NativeCanvas/NativePaint) and
BlendMode.native.kt move nativeMain -> sdlRendererMain (SDL keeps the port flavor);
the skiko leg gets the un-refused upstream files (manifest).

Text/painter/shadow keep the port engine: SkiaBackedCanvas is MANUAL-VENDORED to
also implement NativeTextCanvas/NativePainterCanvas/NativeShadowCanvas/
NativeFinishableCanvas, forwarding to the port SkiaTextRenderer (P3.1 metrics
preserved) + SkiaImageCache via a global skiaLeafDrawer over internalSkiaCanvas.
SkiaImageAsset manual-vendored too (flattened toBitmap/putBytesInto expect/actuals
that would collide in our single native source set). SkiaCanvas.kt deleted; the
offscreen (VectorPainter/DrawCache) now uses upstream's ActualCanvas + the encoded
decoder produces an upstream ImageBitmap.

skiaLeafDrawer is per-WINDOW state re-pointed each frame in drawRoot (+ cleared on
destroy) — mirrors ComposeWindow.installGlobals(); a ctor-set version dangled at a
closed window's destroyed renderer and crashed the surviving window (caught by
multiwintest).

MAC-VERIFY ALL GREEN both legs: builds (demo+apidemo), 10/10 probes, parity PASS
(56/57 byte-identical to baseline; Images within noise), perf within/below baseline,
metadata compile clean, drift + vendor-clean checks pass. B2 (P1.4/1.6) unblocked.
2026-07-17 00:23:31 +02:00
Bitsy 90f56143c4 docs: B6 decision (Option B) + staged plan (B6.1 graphics / B6.2 layer / B6.3 text) 2026-07-16 23:39:40 +02:00
Bitsy ab4b82e492 docs: P1.3 done; P1.4 blocker — SkiaBackedCanvas closure gap + 3 options 2026-07-16 23:27:56 +02:00
Bitsy 4a3d8e5ca2 refactor(ui): P1.3 — fork GraphicsContext behind a per-renderer factory seam
B2 convergence step 2 (RENDERER_CONVERGE.md §4). ComposeOwner.graphicsContext
was an ad-hoc anonymous GraphicsContext calling createProjectGraphicsLayer().
Extracted into createGraphicsContext() — an internal expect in nativeMain with an
actual per renderer source set (disjoint targets, the createRenderBackend trick):

  - ProjectGraphicsContext (shared nativeMain): the project record/replay context,
    layer create/release via the createProjectGraphicsLayer factories.
  - sdl actual -> ProjectGraphicsContext (permanent SDL home).
  - skiko actual -> ProjectGraphicsContext (TRANSIENT; P1.6 swaps in the vendored
    upstream SkiaGraphicsContext + skiko RenderNodeContext).

Behaviour UNCHANGED — both legs return the same ProjectGraphicsContext this step.
MAC-VERIFY ALL GREEN (both legs build, metadata compile clean, 10/10 probes PASS,
parity PASS both legs, perf within baseline).
2026-07-16 23:26:14 +02:00
Bitsy 7f7f57d06d docs: P1.2 done — SDL node cluster relocated, MAC-VERIFY green 2026-07-16 23:12:45 +02:00
Bitsy c45aed105c refactor(ui): P1.2 — relocate SDL node cluster nativeMain -> sdlRendererMain
B2 convergence step 1 (RENDERER_CONVERGE.md §4). The port's actual class
GraphicsLayer lived in shared nativeMain; it cannot coexist there and in
skikoRendererMain, so the whole node cluster moves down into the renderer
source sets (which attach to disjoint targets — the createRenderBackend trick):

  GraphicsLayer.native.kt, NativeRenderNode.kt (interface + context),
  DeferredRenderNode.kt, GraphicsLayerFactory.native.kt (createProjectGraphicsLayer
  actual) -> sdlRendererMain.

The internal expect fun createNativeRenderNode in nativeMain is dropped (its
return type NativeRenderNode is no longer shared); the two per-renderer factories
become plain internal funs. createProjectGraphicsLayer stays a commonMain expect
with an actual now in each renderer set.

Behaviour UNCHANGED: both legs still run the port GraphicsLayer this step — the
Skia leg keeps a TRANSIENT copy of the cluster in skikoRendererMain (deleted in
P1.6 when it switches to upstream SkiaGraphicsLayer + skiko RenderNode).
GraphicsLayerOwnerLayer / ComposeOwner stay SHARED (they only touch GraphicsLayer's
public expect API + createProjectGraphicsLayer).

MAC-VERIFY green: Skia + SDL legs build, metadata compile clean (Windows-publish
trap), 10/10 probes PASS, parity PASS both legs, perf within baseline.
2026-07-16 23:12:15 +02:00
Bitsy 7e829ef87b docs: P1.1 done — skiko:0.150.1 exposes node.RenderNode API (no bump needed) 2026-07-16 22:52:02 +02:00
Bitsy cc9940c705 docs: P0.2 done + first-Mac-session log (P0.3/P0.4 Mac keys, HiDPI harness fix) 2026-07-16 22:49:12 +02:00
Bitsy bb0b5a9a27 test(parity): seed macosArm64/sdl3 golden baselines (57 screens)
Both Mac legs now gate. SDL leg on macOS shows the same ~2% golden-master
median as Skia after the P3.1 text-metric fixes + the HiDPI align fix;
Pickers 19.5 the known TimeInput auto-scroll outlier.
2026-07-16 22:48:17 +02:00
Bitsy 5686e8785f fix(parity): normalize native HiDPI render to JVM reference size before diff
On a Retina Mac the native leg renders at physical density (2000x1400,
DPR 2.0) while the JVM ImageComposeScene reference is fixed at density 1.0
(1000x700). Both represent the same 1000x700-dp viewport, but diff_pair /
side_by_side cropped the top-left min(w,h) instead of resizing, so the
native leg was compared as a 2x-magnified top-left quarter. Invisible on
Windows (both legs 1000x700); on macOS it inflated every screen to 24-78%.

align_native() resizes native down to the JVM size (LANCZOS, no-op at
DPR 1.0). Skia-leg parity collapses to the expected golden-master: Buttons
35->1.96%, Counter 61->0.65%, LazyGrid 78->1.77%; ~2% median.

Seeds macosArm64/skia baselines (57 screens).
2026-07-16 22:38:44 +02:00
Bitsy 3da62b654e docs: P3.1 tracker — post-metrics ranking + h=1.0 boundary log entry 2026-07-16 22:08:05 +02:00
Bitsy b6fb974264 fix(text): lineHeight applies only when STRICTLY greater than the font size
Probe-verified boundary: upstream ignores lineHeight at exactly 1em
(24sp/24lh -> the 33px cell; 24sp/25lh -> 25) — the >= gate applied the
band at h=1.0, off-by-cell on every Text(fontSize = lineHeight) row.
Text screen parity 9.19 -> 3.08; all other screens byte-identical.
Boundary cases added to both metrics probes; baselines re-seeded.
2026-07-16 22:07:32 +02:00
Bitsy 07bfd84810 feat(text): P3.1 — match upstream text metrics exactly; parity median 17% -> 2%
The port's whole 'font-drift baseline' turns out to be line-metric drift,
not typeface difference. Driven by a new matched metrics-probe pair
(demo --metricsprobe native / --metrics JVM), three changes make
SdlParagraph's numbers agree with upstream Paragraph on every probed
config:

1. Font cell = round(hhea_ratio x size), ratios read once per family from
   the face tables via a metrics-only FreeType library (SDL3_ttf only
   exposes grid-fitted CEILED ints — +1px at the M3 body sizes 12/14,
   ~1px per stacked label of accumulating drift). Icon families keep the
   TTF path so icon boxes don't shift.
2. lineHeight application is MODE-dependent: raw styles (lineHeightStyle
   unset) use compat trim — first line keeps the tight font cell,
   lineHeight is the advance between lines; M3 styles (Trim.None, which
   every M3 Typography style sets) use UNIFORM bands — every line exactly
   lineHeight, even compressing below the font cell (probe: 22sp/28lh -> 28).
   Threaded through both renderers' draw paths as inTrimFirstLine.
3. lineHeight smaller than the font size is ignored (probe: 48sp/24lh ->
   65px cell) — upstream skia only applies height multipliers >= 1em.

Parity collapse: Tabs 33.3->1.8, GridsExtra 28.5->1.3, Shadows 24.6->1.5,
Brushes 22.3->2.2, Animation 24.5->3.0; 'gradient ramp' and 'shadow
falloff' were also mostly drift. Remaining outliers: Pickers 19.5
(auto-scroll positioning), Images 16.6, Text 9.2, AnnotatedString 8.1.

Verified on Windows: full 57-screen parity PASS, 9/9 interaction probes
PASS, baselines re-seeded at the collapsed levels. skiko-side mirrors
(SkiaCanvas/SkiaTextRenderer) compile-pend the first Mac run.
2026-07-16 22:02:15 +02:00
Bitsy 2db54e0e19 feat(text): P3.1 fix #1 — honor TextStyle.lineHeight (compat-trim), the top parity gap
SdlParagraph ignored TextStyle.lineHeight entirely (every M3 Typography
style sets one, e.g. bodyMedium 14sp/20sp), so multi-line paragraph
heights diverged from upstream by ~1px per wrapped line — the
accumulating vertical drift that dominates every text-bearing screen's
parity diff.

Implemented with upstream's compat-trim semantics (LineHeightStyle.Trim
of the first line): the first line keeps the tight font cell; lineHeight
is the advance for every following line. Single-line labels are
unchanged — a uniform-band first attempt regressed Counter 2.53->9.18
in the parity gate and was rejected.

The band is threaded as inLineHeightPx through NativeTextCanvas into
both renderers' draw paths (Sdl3Canvas + Sdl3TextRenderer; SkiaCanvas +
SkiaTextRenderer textually — skiko leg compile pends the first Mac run)
so glyph rows stack exactly where layout put the line boxes.

Verified on Windows: full 57-screen parity PASS — LazyExtra 30.00->27.74,
AnnotatedString 24.88->24.59, all 55 other screens byte-identical;
paragraph/key/click/scroll/back probes PASS; baselines re-seeded.
Remaining text-drift term (single-line label cells: TTF integer font
height vs Skia fractional ascent+descent) recorded as the next P3.1
candidate in RENDERER_TASKS.md.
2026-07-16 21:35:58 +02:00
Bitsy a2252a2ede chore(vendor): VENDOR-BASE provenance mandatory + retroactive (all manual vendors)
Per review guidance: every manually-vendored (copy-edited) file must carry
// VENDOR-BASE: <upstream-path> @ <pinned-tag> so it can be diffed against
its base and against later upstream versions. CLAUDE.md vendoring rule 3
now codifies it.

Retro-annotated the 4 manual vendors that predated the convention — both
Synchronization.kt copies (K2 @PublishedApi edit), foundation's
SelectionLayout.kt (OOB clamp fix), components-resources' DomXmlParser.kt
(fresh reimpl; base marks the upstream API it tracks) — each stamped
v1.12.0-beta02 after verifying its upstream base is unchanged
beta01..beta02 in the repo's clone.

check-vendor-drift.py extended: VENDOR-BASE(<VARNAME>) form resolves
non-core pins (COMPOSE_REF -> ../cmp-ref-compose-multiplatform clone),
and a ls-tree existence guard turns a renamed/moved base into 'diff
unavailable' instead of a silent 'unchanged'. 7 files tracked, all OK;
simulated stale umbrella ref fails exit 1 with the correct clone verdict.
2026-07-16 21:22:02 +02:00
Bitsy fcc581d48b chore(vendor): bump both upstream pins to v1.12.0-beta02 (O.2 first run)
compose-multiplatform-core + compose-multiplatform umbrella both pinned to
the v1.12.0-beta02 release tags; Maven side moved in lockstep (catalog
compose=1.12.0-beta02; JVM parity forcing 1.12.0-beta02 core groups /
1.12.0-alpha03 material3 — released pairs confirmed on Maven Central, no
+dev suffix needed anymore).

sync.py fix the bump exposed: fetching a pin that is a TAG created after
the clone only reached FETCH_HEAD, so the checkout failed. Now fetches
refs/tags/x:refs/tags/x (keeping future syncs offline) with a plain-fetch
+ FETCH_HEAD fallback for branches/SHAs.

DRIFT-CHECK: all 3 manual vendors flagged by the P0.6 tripwire; the clone
diff proved each upstream base UNCHANGED base..pin, so their VENDOR-BASE
refs were re-stamped (no reconciliation needed).

Verified on Windows: demo+apidemo build on both legs with zero code
fallout; 8/8 interaction probes PASS; full parity PASS against the
beta01-era baselines with 56/57 screens at their exact percentage (only
GraphicsLayer +0.01pt); baselines re-seeded. Mac legs pend P0.2's first
run. :ui manifest annotation refresh picked up 2 new upstream desktopMain
files as commented gaps (AWT-only; not vendored).
2026-07-16 21:04:44 +02:00
Bitsy ec3bc51346 chore(vendor): O.1 — pin COMPOSE_CORE_REF to the durable tag v1.12.0-beta01+dev4324
Tag verified to dereference to the previously pinned hash 1be9d64 and to
exist on upstream's remote, so any fresh clone can fetch the pin by name
(a bare unreferenced SHA could stop being fetchable if upstream GCs).
VENDOR-BASE headers re-spelled to the tag so check-vendor-drift's
ref-compare stays green. Re-sync at the tag is byte-identical
(check-vendor-clean PASS, 1553 files).
2026-07-16 20:51:48 +02:00
Bitsy 08a14d79cb feat(vendor): P0.7 — vendor-clean check (src/vendor must match the pinned refs)
check-vendor-clean.py hashes every module's src/vendor tree, re-runs
sync.py (which regenerates byte-for-byte from the pinned clones, deleting
stale files), re-hashes and fails exit 1 on any changed/stale/missing
file or manifest-annotation churn — the tree ends up restored to the pin
either way. Wired into verify-mac.sh as step 0 alongside the P0.6
manual-vendor provenance tripwire.

Verified on Windows: clean PASS exit 0 (1553 files / 15 modules / both
upstream repos at their pins); simulated hand-edit + orphan file FAIL
exit 1 listing both, immediate re-run PASS.
2026-07-16 20:48:12 +02:00
Bitsy 812063a320 feat(verify): P0.2 (WIP) — one-command MAC-VERIFY runbook (scripts/verify-mac.sh)
Chains the MAC-VERIFY primitive for both renderer legs on a macOS/Linux
host: build :demo+:apidemo -> nav3/back/click/scroll/multiwin probes
(gated on PASS output) -> parity.py per leg -> CDN_PROFILE draw-ms
spot-check on LazyColumn/Tabs against a self-seeded perf baseline
(build/verify-mac/perf-baseline.txt, +20% regression gate,
--update-perf-baseline to reseed). Non-zero exit on any failure.

Authored + bash -n / parse-logic smoked on Windows; end-to-end run
pending the first Mac session (which also seeds the perf baselines and
the parity macosArm64/{skia,sdl3} baseline keys).
2026-07-16 20:44:55 +02:00
Bitsy 983cafc9b9 feat(parity): P0.5 — render-to-quiescence capture, deterministic on both legs
Native: windowHasInvalidations() (needsFrame + recomposer.hasPendingWork +
both BroadcastFrameClocks' hasAwaiters) queryable from onFrame probes;
disableInfiniteAnimations injects a cancelling InfiniteAnimationPolicy into
each window's Recomposer context (upstream's test mechanism); and
useVirtualFrameTime steps the composition/animation clocks a fixed 16.6ms
per loop iteration — the native mirror of the JVM leg's render(nanos).
Quiescence alone left Pickers drifting ±10px per run (TimeInput focus →
bring-into-view scroll racing real-time frames); virtual time makes the
race resolve identically every run.

JVM: screenshotAllScreens steps a virtual 60fps clock via render(nanos)
until hasInvalidations() clears (cap 300), same cancelling policy through
ImageComposeScene's coroutineContext.

Demo: --screenshot captures after 3 consecutive quiescent frames;
--frames=N is now the quiescence CAP (default 300), not a fixed capture
point.

Verified on Windows: Pickers/Animation/Buttons byte-identical across 3
native runs (Pickers settles at exactly frame 12); all 57 JVM screens
byte-identical across 2 sweeps; 5 interaction probes PASS (flags inert
outside screenshot mode); parity PASSed against the OLD frame-6 baselines
(55/57 identical %), re-seeded mingwX64/sdl3, fresh full run PASS exit 0.
2026-07-16 20:42:09 +02:00
DWA 5e05f0d354 feat(parity): P0.4 — parity.py is now a gate (baselines + non-zero exit)
Per-(target/renderer) golden baselines in scripts/parity/baselines.json, a
--update-baselines seeder, and a non-zero exit when a screen exceeds baseline+max(3pts,25%)
or NATIVE FAILED — so parity can actually gate a change instead of only ranking. Report now
shows baseline + verdict per screen. Seeded a full 57-screen mingwX64/sdl3 baseline.
Output made ASCII-only (Windows cp1252 console crashed on the em-dash/arrow).

Verified on Windows: PASS exit 0, simulated regression exit 1, restore clean.
2026-07-16 17:58:00 +02:00
DWA 73d98a3dc3 feat(parity): P0.3 — font-align the JVM parity leg with the bundled NotoSans
Stage font/NotoSans.ttf onto the demo JVM classpath (jvmProcessResources) and load it in
MainJvm's ScreenHost — applied via an M3 Typography copy (all core styles) + LocalTextStyle
so both Material and BasicText render NotoSans, matching the native leg's default font.

Verified on Windows: staged + loads + applies (no not-aligned warning). Finding: on the SDL
leg parity is dominated NOT by typeface but by an accumulating vertical line-metric delta
(SDL3_ttf NotoSans line height vs Skia) — so the SDL % barely moves; the ~0% golden-master
payoff is Skia-leg (Mac) only. Confirms the CONVERGE §1 layout-engine delta.
2026-07-16 17:52:22 +02:00
DWA 7a78e5e7a6 feat(vendor): P0.6 — manual-vendor drift tripwire
Add machine-readable VENDOR-BASE: <upstream-path> @ <ref> provenance to the three renderer
manual-vendors (GraphicsLayerOwnerLayer, GraphicsLayer.native, LayerTransformationMatrix) +
scripts/compose-fork/check-vendor-drift.py: scans for VENDOR-BASE lines, flags any whose
recorded ref lags the current COMPOSE_CORE_REF pin (optional upstream base..pin diff when a
../cmp-ref clone is present), exits non-zero on staleness so it can gate a ref bump. Also
refreshed the stale RENDERER_REFACTOR.md refs in those headers → RENDERER_CONVERGE.md.
Verified on Windows: clean exit 0, simulated stale exit 1.
2026-07-16 17:42:53 +02:00
DWA f6164e7cf0 feat(parity): P0.1 — make parity.py target-aware (both renderers, any host)
Unhardcode the mingwX64 exe: infer the host's native target by default (Windows→mingwX64
so existing usage is byte-identical), add --target= and --renderer= so macOS/Linux can run
BOTH the Skia leg (default) and the SDL leg (-Prenderer=sdl3). Compute the link task + exe
path (.exe vs .kexe) + default GPU driver from those. Verified on Windows: default run
intact (Buttons 16.47%); macOS legs resolve correctly. RENDERER_TASKS.md P0.1 checked.
2026-07-16 17:37:28 +02:00
DWA 43f88c7866 docs: add RENDERER_TASKS.md (executable checklist) + lock goal to G1 upstream-tracking
RENDERER_CONVERGE.md = study file (why/decisions/analysis); RENDERER_TASKS.md = pure
checkbox task tracker an agent progresses over time (status marks, done-criteria, blocked-by,
Mac-verify primitive, progress log). §0.5 goal locked to G1 (upstream-tracking): B5+B2 lead,
guardrails first, JVM = Windows fidelity tier, B3 capped, Track A shelved, B1 + module split
deferred.
2026-07-16 17:28:15 +02:00
DWA 70602e4c91 docs(renderer-converge): macOS/Linux run BOTH renderers — one box verifies everything
Correct the verification framing: macOS/Linux build the Skia leg (default) AND run the SDL
leg under -Prenderer=sdl3, so the Mac (or a Linux box) is a single machine that verifies
both renderers — not just the Skia leg. Windows is only needed for the shipped mingw target
binary, the Windows-only PrintWindow probe, and the tag-time metadata publish. Updated §5
runbook, the why-it-suffices note, and §8.
2026-07-16 17:22:18 +02:00
DWA 653f32e08c docs(renderer-converge): fold in 5-agent council + switch verification gate to a local Mac runbook (no CI)
Council corrections folded into RENDERER_CONVERGE.md: add §0.5 goal/audience (the root
finding — priority is undefendable without it); restate Rule 3 (bespoke vs vendored-upstream
surface); correct the North Star (Skia-leg text is the shared SdlParagraph, not upstream —
new B6 option / stated axiom); re-rate B2 from file-flip to a source-set MIGRATION (relocate
the SDL node cluster out of nativeMain, fork the ad-hoc GraphicsContext, +Matrices.skiko/
Blur.skiko closure, skiko-version DoD, keep owner layer shared); DECIDE the lifetime model
(shared GC on both legs); enumerate the module-split build fan-out + cohesion cost; make §8
verification concrete (font-aligned golden-master, render-to-quiescence, cross-platform
self-tests, drift tripwire, actual-API-parity invariant); add §9 risks (skiko drift, +dev-ref
fragility, permanent ~3,800-line SDL surface); add §13 council synthesis.

Maintainer opted out of CI (slow); the Phase-0a gate is now a LOCAL MAC VERIFY RUNBOOK
(the Mac builds the Skia leg the Windows box can't) instead of a GitHub CI floor.
2026-07-16 17:19:13 +02:00
DWA a98068ba09 docs: consolidate RENDERER_REFACTOR into RENDERER_CONVERGE (Appendix A), delete the old doc
The retained-layer engine model, the requiresLayer() compositing contract, what
landed (the geo-node story), and the hard-won learnings are preserved as Appendix A
of RENDERER_CONVERGE.md; the commit-by-commit journal is dropped (endpoints kept).
RENDERER_CONVERGE.md is now the single living renderer doc.
2026-07-16 16:32:35 +02:00
DWA b2a23d60b8 docs(renderer-refactor): image-capture investigated + deferred (demo uses drawImageRect, not the capturable drawNativePainter) 2026-07-16 15:51:37 +02:00
DWA 8fddb0a94c docs(renderer): refresh geo-node doc comments for spanned-text + icon capture
The SdlDisplayList / SdlDisplayListRenderNode headers still described the geometry-only
first increment (spanned/icon text listed as deferred). Update them to the current
coverage: GeometryBatch + TextRun (plain AND spanned) + IconRun; unsupported now trips
on image blits, saveLayer, rounded/generic layer clip, compositing, and span
backgrounds. Comment-only.
2026-07-16 15:46:52 +02:00
DWA 15e12ec2b9 docs(renderer-refactor): icon-font glyph capture (Lists/NavRails cache; -23%) 2026-07-16 15:35:50 +02:00
DWA c5945f1a94 feat(renderer): capture icon-font glyphs (Material Symbols) in the geo node
Icon-font glyphs recorded as an IconRun (leaf-local box + params) instead of
deferring the leaf. Replay maps the box through the layer affine and re-draws via
FreeTypeIcons' glyph cache (eviction-safe), with the same SDL_ttf fallback the
immediate path uses for a codepoint absent from the FreeType/subset font.

This unblocks the ubiquitous icon + text list-item / nav-item leaf: Lists text
draws 24 -> 14, NavRails 28 -> 16 (draw -23%). Full 57-screen sweep clean (56 at
0.000% geo-vs-default, Pickers = its settle-timing artifact), nav3test PASS. Icon-only
monolithic grids (Icons / MaterialSymbols) still defer for layer granularity.
2026-07-16 15:35:37 +02:00
DWA d4cb543090 docs(renderer-refactor): spanned-text capture (Tabs -38%) + all flip residuals explained 2026-07-16 15:23:31 +02:00
DWA 63b430873d feat(renderer): capture spanned text runs in the geo node
Styled text (colour / weight / italic / size / decoration spans) now records into
the display list as one TextRun per run — mirroring the plain path — instead of
deferring the whole leaf to block-replay. Replay re-looks-up each segment texture
by params (eviction-safe). A run with a SpanStyle.background still defers (the
background fill isn't a captured command yet; rare — highlight/selection).

Verified: full 57-screen sweep clean (no regression; styled screens Text/Annotated
String/Lists/Chips/Tabs all 0.000% geo-vs-default), and caching now reaches styled
labels where layer granularity allows — Tabs text draws 38 -> 16, draw 4.29 -> 2.65ms
(-38%). Screens whose text sits in one monolithic layer (Text/Lists) still defer for
other reasons in that leaf (child layer / icon) — layer-granularity, as upstream.
2026-07-16 15:22:08 +02:00
DWA 3a584bbe8b docs(renderer-refactor): perf characterization of the geo flip — no regression, win scales with static content 2026-07-16 14:50:20 +02:00
DWA 2873df25ae docs(renderer-refactor): Carousel clip fix + default flipped to geo (-57% draw, sweep clean) 2026-07-16 14:43:06 +02:00
DWA bff6b484e5 feat(renderer): flip SDL default to the cached-geometry node (geo)
CDN_LAYERCACHE default is now SdlDisplayListRenderNode. A full 57-screen sweep of
geo vs the previous block-replay default is clean: 55 at 0.000%, GraphicsLayer 0.122%
and ModShortcuts 0.095% (both the same sub-pixel rotated-edge AA — cosmetic,
deterministic). Former blockers gone: Carousel fixed by the clip change; Pickers was
an entry-animation settle artifact (geo-vs-geo and geo-vs-default both 0.000% once
settled). Perf: LazyColumn steady-state draw 3.55 -> 1.52ms (-57%), text 22 -> 6.

Escape hatches: CDN_LAYERCACHE=off|defer|0 -> DeferredRenderNode (previous default);
=1|texture -> SdlRenderNode (legacy texture cache). nav3test PASS.
2026-07-16 14:42:29 +02:00
DWA 4371335b37 fix(renderer): geo node applies layer clip — Carousel item masks no longer overflow
The geo fast path re-emits raw geometry via SDL_RenderGeometry, which the SDL
canvas clips only to a RECT; its rounded-corner mask is a lazy offscreen realized
from drawRect, which replayBatch bypasses. So a rounded/path LAYER clip went uncut
on the fast path — the Carousel item's morphing rounded mask was ignored and the
coloured card overflowed its silhouette (the deterministic 4.4% geo-vs-default diff).

Fix: apply a rectangular layer clip on the fast path (honoured by SDL_RenderGeometry
via the clip rect), and force rounded/generic-clipped leaves to the block-replay
(which realizes the mask correctly, == DeferredRenderNode). Carousel 4.4% -> 0.000%.
2026-07-16 14:42:29 +02:00
DWA 468c347357 docs(renderer-refactor): Phase 4 text capture landed — -59% draw; flip-blocker analysis 2026-07-16 14:22:41 +02:00
DWA 65503e60b8 feat(renderer): Phase 4 text capture — cache static text runs (geo node)
The real perf win. The geo capture path now records PLAIN text runs (params, not the
texture pointer — mirrors skiko recording a glyph-run into a Picture); replay
re-looks-up via Sdl3TextRenderer's per-run texture LRU (our glyph-atlas analog) and
blits at the transformed origin. Eviction-SAFE: if the LRU dropped the run, the
lookup re-rasterises — no dangling pointer. Spanned / icon text bails (unsupported →
block-replay).

- SdlDisplayList is now an ORDERED command stream (GeometryBatch | TextRun) so
  text↔shape z-order is preserved; replay flushes geometry before each text blit.
- Sdl3TextRenderer: runSink captures the plain run at its blit; blitRun replays it.
- Sdl3Canvas.drawNativeText: capture mode sets the sink for plain text (icons gated).
- SdlDisplayListRenderNode: added cache-leaves-defer-parents (fRecordingStack) — a
  child layer drawn during record defers the parent, and a child's drawInto during a
  parent's record draws NOTHING (no GPU leak into the target-less capture pass). This
  was the fix for the nesting regressions text capture first exposed.

Measured (LazyColumn, forced continuous): draw 3.56ms -> 1.47ms (-59%), text draws
22 -> 6 — static text stops being re-shaped/re-blit every frame.

Verified (geo vs default): Text/LazyColumn/Shapes/Icons/Cards/Chips 0.000%, Buttons
0.006%, GraphicsLayer 0.157% (rotated-square AA). KNOWN =geo issues (flag-gated,
default unaffected): Pickers renders correctly but its scrollable settles to a
different position nondeterministically (capture perturbs the frame timing → scroll
animation) — a timing/app-state sensitivity, not a render bug; Carousel 4.4%
(pre-existing). Resolve both before flipping the default to geo.
2026-07-16 14:13:43 +02:00
DWA a832de5ce1 docs(renderer-refactor): scope text capture + the glyph-texture lifetime constraint 2026-07-16 13:45:52 +02:00
DWA 5caef6b2b3 docs(renderer-refactor): Phase 4 geo node works — pixel-equal + deterministic; text capture next 2026-07-16 13:40:44 +02:00
DWA b50081014e feat(renderer): Phase 4 cached-geometry node — capture + replay (CDN_LAYERCACHE=geo)
The robust retained node. A capture-mode Sdl3Canvas records a leaf's tessellated
geometry ONCE into an SdlDisplayList (layer-local, NO GPU / no render target);
SdlDisplayListRenderNode.drawInto re-emits those cached vertices through the layer
transform via Sdl3DrawScope.replayBatch. Crisp under ANY transform (vertices
re-transformed, not a resampled texture), bit-exact (no texture round-trip), and —
crucially — NO per-frame texture state, so none of the texture node's timing
nondeterminism.

- Sdl3Canvas: capture mode (fCaptureList) routes shapes to the scope's recordingSink
  and gates text/image/clip/saveLayer/shadow to mark the list `unsupported` (that
  leaf falls back to a crisp block-replay — nothing leaks to the GPU). replayGeometryList
  re-emits under the current transform. Sdl3OffscreenRenderer.createCaptureCanvas.
- SdlDisplayListRenderNode behind CDN_LAYERCACHE=geo (1 = texture node, default =
  DeferredRenderNode).

Geometry-only so far: text/image/clip leaves DEFER (correct, not yet cached) — so the
perf win is currently limited to shape-only leaves; extending capture to text/image/
clip is the next increment (the full win). But correctness/robustness is proven:

Verified (geo vs default): Shapes/Text/Buttons/Pickers/Icons/Carousel/LazyColumn/
Canvas/Path ALL 0.000% (incl. every screen the texture node failed — Pickers 13-17%→0,
Carousel 4.4%→0). Pickers DETERMINISTIC (run1-vs-run2 0.000%, vs texture's 0↔17% swing).
nav3test PASS. Only residual: GraphicsLayer 0.134% — deterministic sub-pixel AA fringe
on ROTATED squares (tessellate-then-rotate vs tessellate-rotated), cosmetic.
2026-07-16 13:39:58 +02:00
DWA 5dc3a4525a docs(renderer-refactor): journal Phase 4 start (geometry recording foundation) 2026-07-16 13:23:27 +02:00
DWA 716a005da6 feat(renderer): Phase 4 foundation — geometry recording sink (no behaviour change)
First step of the cached-geometry display list (the robust replacement for texture
caching — no offscreen texture, so no fixed-resolution softness and no timing-
dependent nondeterminism; RENDERER_REFACTOR.md §3/§13).

- SdlDisplayList / GeometryBatch / GeometrySink: capture the tessellated untextured
  triangle batches a layer produces, in layer-local coords (an `unsupported` flag
  trips when a not-yet-captured op — text/image/clip — is hit, so the node can fall
  back to a crisp block-replay; nothing leaks to the GPU).
- Sdl3DrawScope: flush() routes to a `recordingSink` (capture) instead of
  SDL_RenderGeometry when set; `currentDeviceMatrix()` exposes the CTM for replay.
  Recorded with an identity base CTM ⇒ layer-local vertices; replay re-applies the
  layer matrix (crisp under any transform, bit-exact).

Sink defaults null ⇒ zero behaviour change. Next increments: the capture-mode canvas
+ SdlDisplayListRenderNode (record/replay) behind CDN_LAYERCACHE=geo, then text/image/
clip capture. Verified: :ui:compileKotlinMingwX64 BUILD SUCCESSFUL.
2026-07-16 13:22:18 +02:00
DWA 5890dd0fe3 docs(renderer-refactor): Pickers bug — clear fix + nondeterminism finding; Phase 4 is the robust path 2026-07-16 13:13:52 +02:00
DWA 29856d9bea fix(renderer): clear offscreen render targets on begin (reused-texture ghosting)
beginOffscreen set the render target but never cleared it. Fresh SDL target
textures happen to be zeroed (so static leaves were pixel-equal), but a
retained-layer texture (SdlRenderNode) is REUSED across re-records — an un-cleared
re-record ghosted prior content, which showed as multi-frame instability on churny
screens (Pickers: cached changed 16.8% between frame 6 and 60 while default was
stable). Clear to transparent (RenderClear fills with the draw colour ignoring
blend, so 0,0,0,0 = transparent). Shared with the vector-icon offscreen path, which
redraws fully each time, so clearing is correct there too; negligible cost (only on
offscreen begin).

Improves caching robustness (fixed the Pickers instability in controlled tests) but
does NOT fully resolve it: Pickers still renders nondeterministically across launches
(0% or ~17% vs default, even isolated) — a timing-dependent issue in the texture-cache
path on complex screens. Caching stays flag-gated; the robust fix is Phase 4's
cached-geometry list (no texture state → no timing dependence). Static screens remain
pixel-equal.
2026-07-16 13:13:14 +02:00
DWA 5184d3ec24 docs(renderer-refactor): #1 full sweep — DO NOT flip default yet (Pickers/Carousel caching bug) 2026-07-16 12:05:38 +02:00
DWA 0610460ebb docs(renderer-refactor): journal #2 done + #3 (Phase 4) cached-geometry design 2026-07-16 11:46:38 +02:00
DWA 8081fc13f6 fix(renderer): don't texture-cache image/icon leaves (AA-fringe fix)
Content with partial alpha — a decoded image (painterResource), a tinted
vector/icon — does not round-trip bit-exact through the 8-bit premultiplied
offscreen texture (the heart painterResource rendered visibly wrong, Icons
diff 0.36%). Detect it cheaply via a DrawStats.imageBlits delta around the
record block: a leaf that draws any image bails to the crisp block-replay.
Text / shapes / solid fills bump other counters and stay cached (pixel-equal).

Costs almost no perf — icons are blits, not tessellation; the big tessellation
cost is text, which still caches.

Verified (cached vs default): Icons 0.36% -> 0.014% (visible heart gone; residual
is one icon's AA), Buttons 0.006% (disabled semi-transparent fill), Text /
LazyColumn / GraphicsLayer / Shapes still 0.000%. The sub-perceptual residual is
the inherent texture-round-trip rounding floor — removed entirely by Phase 4's
cached-geometry display list (no round-trip).
2026-07-16 11:44:42 +02:00
DWA 3fec62fbc2 docs(renderer-refactor): journal the SDL caching node + measured perf win 2026-07-16 11:19:00 +02:00
DWA 3f76f77e5b feat(renderer): retained texture-cached SdlRenderNode (opt-in CDN_LAYERCACHE=1)
The SDL perf payoff: records a leaf layer's drawing ONCE into an offscreen
texture; replay is a cheap blit, not a re-tessellation.

Correct-by-construction nesting, no upstream divergence: SDL textures composite
children by value (unlike skiko's by-reference Picture), so we CACHE LEAVES,
DEFER PARENTS — a node auto-detects on first record whether its block drew any
child layer (via a recording stack) and, if so, replays the block instead
(children composite their own live textures → no staleness). The texture
fast-path is further restricted to opaque, untransformed, effect-free leaves;
scale/rotation (would bilinear-resample soft) and alpha/blend/colorFilter/
renderEffect (blit can't reproduce offscreen compositing) fall back to a crisp
block-replay — so caching is strictly non-regressing.

Gated behind CDN_LAYERCACHE=1; default stays DeferredRenderNode. Flip the default
after a full parity-harness sweep.

Verified (SDL leg, cached vs default): GraphicsLayer/Shapes/Text/LazyColumn/
Scroll/Brushes/Canvas pixel-equal (0.000%); Buttons 0.006% + Icons 0.36% (AA
fringe from the texture round-trip at rounded-clip/glyph edges — not structural);
nav3test + backtest PASS. Profiler (LazyColumn, forced continuous): per-frame
verts 2958->1158 (-61%), text draws 22->6, draw 4.0->3.4ms — static leaves blit
instead of re-tessellate.
2026-07-16 11:18:08 +02:00
DWA 1f33506c13 docs(renderer-refactor): journal the GraphicsLayer façade + scope the caching node 2026-07-16 10:46:50 +02:00
DWA 44e09aa89f feat(renderer): GraphicsLayer as a façade over NativeRenderNode
Restructure to mirror upstream: GraphicsLayer is now a copy-edit of
SkiaGraphicsLayer.skiko.kt that mirrors every visual property onto a backing
NativeRenderNode and delegates record/replay to it — instead of an ad-hoc
lambda-replay. This is the correct Compose-faithful shape: the per-renderer
caching node (skiko RenderNode on Skia, offscreen/display-list on SDL) will slot
in behind createNativeRenderNode without touching GraphicsLayer.

- NativeRenderNode: reshaped to record(block)/drawInto(canvas)/close() + the full
  skiko-RenderNode property surface (transform/shadow/clip/compositing).
- DeferredRenderNode (nativeMain, renderer-agnostic): the node both renderers use
  today — replay-the-block, UNIFYING the old GraphicsLayer.native lambda-replay
  AND ProjectOwnedLayer's transform/shadow/clip logic in one place. Transform via
  prepareLayerTransformationMatrix + canvas.concat (same matrix as hit-testing, so
  draw and hit-test agree). Behaviour-preserving; no re-tessellation win yet — that
  arrives when createNativeRenderNode returns a caching node (Phase 2a/2b).
- toImageBitmap: implemented (was throwing) — renders the layer into a fresh
  ImageBitmap; flushed via new NativeFinishableCanvas (SDL offscreen must commit
  its texture; Skia finish() is a no-op). Both canvases implement it.

Verified SDL leg: :ui green; demo renders GraphicsLayer (rotation/scale/alpha
identical), Buttons/Shapes correct; nav3test PASS.
2026-07-16 10:45:00 +02:00
DWA 0870cd923a docs(renderer-refactor): journal the navigation crash fix + full flip verification 2026-07-16 10:19:16 +02:00
DWA 49b14337eb fix(renderer): don't shrink dirtyLayers mid-draw (crash on navigation)
The Phase 1 flip crashed with IndexOutOfBoundsException on any navigation
(--nav3test exit 3; sidebar click killed the window). Cause: my hand-written
notifyLayerIsDirty removed a layer from dirtyLayers unconditionally, but
renderRoot iterates `for (i in 0 until dirtyLayers.size)` calling
updateDisplayList(), which sets isDirty=false -> notifyLayerIsDirty(false) ->
removed the layer mid-iteration, so dirtyLayers[i] ran off the end.

Aligned notifyLayerIsDirty + renderRoot to upstream OwnedLayerManagerImpl
verbatim: while isDrawingContent, a clear (isDirty=false) is a no-op — the
dirtyLayers.clear() after the loop drops them all at once, so the list can't
shrink during iteration.

Verified: --nav3test PASS (was exit 3), --backtest PASS, and the sidebar click
navigates to GraphicsLayer correctly (hit-test lands + layer churn, no crash).
2026-07-16 10:18:40 +02:00
DWA 74f186136b docs(renderer-refactor): add progress journal (Phase 0 + Phase 1 flip) 2026-07-16 09:52:29 +02:00
DWA bbaa04354c feat(renderer): flip createLayer to retained GraphicsLayerOwnerLayer
Phase 1 behavioural step: every layout layer now goes through upstream's
dirty-gated record/replay OwnedLayer instead of the immediate-mode
ProjectOwnedLayer stand-in.

- ComposeOwner implements OwnedLayerManager (dirtyLayers + notifyLayerIsDirty
  + recycle + voteFrameRate); createLayer returns GraphicsLayerOwnerLayer;
  invalidate() -> window needsFrame via a callback hook.
- renderRoot(): re-records dirty layers, then walks the tree so clean layers
  replay. ComposeRootHost.drawRoot delegates to it; both backends call it
  instead of rootNode.draw.
- ComposeWindow wires the invalidation callback so a draw-only state change
  still schedules a frame.
- Move ComposeOwner + ComposeRootHost commonMain -> nativeMain (they now touch
  native-only OwnedLayerManager / GraphicsLayerOwnerLayer; :ui commonMain only
  ever compiles to native anyway). The feed*Processor expect/actual collapses
  to plain nativeMain functions.

NOTE: still backed by the existing lambda-replay GraphicsLayer, so this is
behaviour-preserving (no re-tessellation win yet); the storage swap to a
caching NativeRenderNode is the next step. Verified on the SDL leg: demo exe
renders GraphicsLayer (rotation/scale/alpha correct), Buttons (rounded/shape
clip correct), Shapes/Scroll/Text/Counter all non-blank. Skia leg builds on
mac/CI. Hit-testing through the new layer matrix wants a probe pass next.
2026-07-16 09:52:11 +02:00
DWA 86c97cbad1 feat(renderer): vendor GraphicsLayerOwnerLayer (compiles unused, SDL leg)
Phase 1 shared step: bring in upstream's real OwnedLayer (dirty-gated
record/replay over a GraphicsLayer) that will replace the immediate-mode
ProjectOwnedLayer.

- nativeMain: GraphicsLayerOwnerLayer.kt manually vendored from skiko
  (setLightingInfo tail dropped; not wired to createLayer yet).
- nativeMain: prepareLayerTransformationMatrix — the one helper that was
  skikoRenderer-only (Matrices.skiko.kt), lifted to shared nativeMain under a
  distinct name (parent source sets can't see child-set symbols; distinct
  name avoids clashing with the vendored skiko copy on the Skia leg).

Additive — createLayer still returns ProjectOwnedLayer, so no behaviour
change. Verified :ui:compileKotlinMingwX64 BUILD SUCCESSFUL (SDL leg).
2026-07-15 17:38:20 +02:00
DWA 5a05663050 feat(renderer): Phase 0 — NativeRenderNode seam (retained-layer scaffolding)
Introduce the renderer-agnostic retained display-list node the whole
retained-layer engine will hang off (RENDERER_REFACTOR.md §4):

- nativeMain: NativeRenderNode interface + NativeRenderNodeContext +
  createNativeRenderNode expect. Surface mirrors skiko RenderNode /
  SkiaGraphicsLayer (topLeft/size/pivot, transforms, elevation shadow,
  clip rect/rrect/path, layerPaint, beginRecording/endRecording/drawInto/
  close) so the Skia actual can wrap skiko RenderNode / skia Picture and
  the SDL actual can build a command-list + offscreen twin.
- skikoRendererMain / sdlRendererMain: TODO() stub actuals (Phase 1 / 2).

Additive only — nothing constructs a node yet, so no behaviour change.
Verified: :ui:compileKotlinMingwX64 BUILD SUCCESSFUL (SDL leg). Skia leg
compiles on mac/CI (native skiko klibs absent on Windows).
2026-07-15 17:26:54 +02:00
DWA ebe1d67620 docs(renderer-refactor): add minimize-divergence rule for the top renderer layer 2026-07-15 17:19:26 +02:00
DWA 7eaebeb787 docs(renderer-refactor): make scope explicit — whole per-renderer stack, not just retained layers 2026-07-15 17:09:46 +02:00
DWA 2f1f7d5ba5 docs: replace ROADMAP + NEXT-SESSION with RENDERER_REFACTOR plan
Plan to adopt upstream skiko's retained-layer engine (record-once/replay
GraphicsLayer) in place of the current immediate-mode renderer, which
re-tessellates the whole tree every frame.

- Skia leg ~= vendored skiko target (GraphicsLayerOwnerLayer +
  SkiaGraphicsLayer over skiko RenderNode / Picture fallback); SDL leg
  builds a faithful SdlRenderNode twin (display list + offscreen).
- Keep ComposeOwner + SDL loop; wire it as the OwnedLayerManager.
- Adds a feature-parity audit matrix vs skiko (the acceptance gate) and
  Track V: relocate graphics actuals per-renderer, vendor the skiko set,
  delete hand-rolled Skia duplicates.
- Delete ROADMAP.md / NEXT-SESSION.md (superseded; still-valid items
  folded in); repoint CLAUDE.md.
2026-07-15 17:05:20 +02:00
Bitsy 92454ae8a1 fix(window): FPS title shows within ~2 frames instead of needing a page change
The FPS title only updated after a full 1000ms of wall-clock accumulated
since the last update — and the idle-skip branch called resetFpsWindow()
(zeroing the counter + window start) every time the app went idle. Since
the UI renders in short bursts and idles between them, that 1-second
window kept restarting before it ever completed, so the first title
update never fired. A page change was the only interaction whose
transition animation renders continuously long enough (~700ms+) to let
the window complete — hence 'I have to change page for it to work'.

Replace the fixed-window frame counter with an EMA-smoothed
instantaneous rate (from the inter-frame delta), refreshed ~4x/sec. It
shows within ~2 rendered frames of any activity. A dt outside 1..100ms
(first frame / resume-from-idle) isn't sampled, so idle gaps don't tank
the reading and the title holds the last active rate while static.

Verified: demo shows FPS in the title 1.6s after launch, no interaction.
2026-07-14 20:45:36 +02:00
Bitsy aad70f342b fix(sdl): stop re-rasterizing text at device size — popup/dialog text no longer steps size/weight
Regression from 120abbad's glyph device-scaling: drawNativeText scaled
the font size by the layer affine (inFontSize * scaleY, int-quantized)
and re-rasterized. Under an animated layer scale — every Material
DropdownMenu/AlertDialog enter transition scale-animates (Menu.kt
scaleX/scaleY) — text re-rasterized at each integer size every frame, so
it visibly stepped through sizes and (via SDL3_ttf re-hinting) weights
as the popup grew in. User-reported, seen across dropdowns and dialogs.

Revert to rasterising at LOGICAL font size + 1:1 blit at the
affine-mapped position (the pre-120abbad behaviour): the layer scale
reaches text POSITION but not glyph size, so animated-scale popups are
stable. Keeps 120abbad's separate clip-mask AA/overlap fixes. Trade-off:
statically graphicsLayer-scaled text renders at logical size (a rare,
minor parity gap) — smoothly scaling glyphs needs blit-scaling a
logical rasterisation (noted for a future text-renderer pass).

Verified: demo dropdown opens with clean uniform text; text no longer
scales with the layer, so mid-animation == settled by construction.
2026-07-14 20:19:40 +02:00
Bitsy cf597c12e0 cleanup: delete dead modifier scaffolding + fix demo's fake cacheKey claims
ModifierElements.kt (ClipModifier/ClipNode + GraphicsLayerModifier/
GraphicsLayerNode with the never-implemented cacheKey) was entirely
unreferenced — apps use the upstream clip/graphicsLayer; nothing read
these. Deleted the file (compiles clean across :ui and :demo).

The demo GraphicsLayer screen advertised caching that never existed
('Cached subtree (cacheKey)', 'Combined (cache + rotation)', a
'(cached)' label) over plain graphicsLayer() calls. Removed those two
fabricated sections and the cacheKey wording; the honest Rotation
(live) / Scale / Alpha sections already show what graphicsLayer does.
2026-07-14 20:02:30 +02:00
Bitsy cbddf8ca0b docs: RESOLVED — demo-70/apidemo-144 was mixed-refresh monitors, not a renderer bug
User has a 75Hz main + 144Hz second screen; each app's fps is just the
refresh of the monitor its window opened on (SDL vsync follows the
display). Instrumentation already showed both apps at ~3.7ms draw —
well under both the 13.3ms (75Hz) and 6.7ms (144Hz) budgets, so no perf
problem exists. Records that retained-layers/dirty-region/cacheKey are
speculative with no demonstrated need; only the dead-scaffolding cleanup
is worth doing regardless.
2026-07-14 19:54:04 +02:00
Bitsy ad738a643e docs(CLAUDE): profiler now file-output + draw counters + CDN_FORCERENDER; vsync/display caveat
Documents the DrawStats per-frame counters, file output, and
CDN_FORCERENDER added this session, plus the load-bearing caveat: present
is vsync-bound so all timings are capped by the dev display's refresh —
profile on the target rate before diagnosing a frame-rate gap.
2026-07-14 19:53:12 +02:00
Bitsy 8f5c59bd64 feat(window): per-frame DrawStats in profiler + CDN_FORCERENDER; correct the 70fps theory
Extends CDN_PROFILE with per-frame draw counters (DrawStats: geometry
submits, vertices, mask realizations, text/image blits) and a
CDN_FORCERENDER=1 flag to render every frame for steady-state timing on
idle screens.

Used them to test the demo-70fps theory and DISPROVE it: forced-render,
both apps pin at 75 fps (this dev display's refresh), present ~9ms; the
demo's draw (~3.7ms, geo=40, masks=0) is actually LIGHTER than apidemo's
(~2.9ms, geo=146, masks=74). So the demo is NOT draw-bound — the
70-vs-144 split is specific to the user's 144Hz monitor and can't be
reproduced on a 75Hz box. Recorded the correct next step (profile on the
144Hz machine) in NEXT-SESSION.md; noted the vsync/display confound in
CLAUDE.md's profiler docs. Thread A (spurious continuous render) resolved
as a non-issue — the demo idles when static.
2026-07-14 19:52:24 +02:00
Bitsy 914bab840d docs: handoff notes for the renderer-caching session
Captures the demo-70fps investigation for the next session: (A) the
likely-cheaper continuous-render / hover-self-loop thread to chase first,
and (B) the cacheKey/retained-layer work — dead scaffolding today,
blocked on a content-draw-redirect primitive (the 'renderer rewrite')
that also underpins dirty regions. Lists the tooling + existing
infrastructure to build on.
2026-07-14 19:34:39 +02:00
Bitsy 8f96ba9478 docs(roadmap): record demo 70fps cause (draw-bound sidebar re-tessellation)
Investigated the demo-70fps / apidemo-144fps question with CDN_PROFILE:
both apps use vsync(1), so it's NOT a vsync-mode difference. The full
demo is DRAW-bound (draw ~34ms cold / ~14ms steady, present ~1.3ms) —
it re-tessellates the whole always-present sidebar (30+ rows of text +
icons) every rendered frame, capping ~70 fps. apidemo's lighter chrome
fits the 6.9ms/144Hz budget and hits the vsync ceiling. '70≈144/2' is
coincidence. Concrete motivation for retained layers / dirty regions.
2026-07-14 18:43:23 +02:00
Bitsy 5a9e7b0f7f docs(roadmap): mark layer rotation verified-working; warn item-4 list is stale
Layer rotation renders correctly (demo GraphicsLayer Rotation-live +
Combined rows) — like gradients, the claimed gap was already fixed.
Added a note to verify item-4 claims against the parity heatmaps before
acting, since the list predates several fixes.
2026-07-14 18:28:55 +02:00
Bitsy 5dfd9200f8 fix(sdl): grid-mesh gradient fills so radial/sweep aren't flat
The brush samplers (linear/radial/sweep with stops) were fully
implemented, but fills sampled the brush only at the shape's CORNERS and
let SDL interpolate. A square's four corners are equidistant from its
centre → identical radial/sweep t → the fill rendered as ONE flat colour
(the demo Brushes radial was solid green: center==corner pixel). Linear
survived because corners differ along the axis.

Gradient fills (brush !is SolidColor) now tessellate rect and round-rect
bodies into an ~8px-cell grid (emitRectMesh, adaptive 1..24 cells/axis),
so the sampler is evaluated across the interior and SDL interpolates
per-cell. Solid fills keep the single-quad fast path. Round-rect corner
arcs already fan from their centre.

Verified: demo Brushes radial now shows centre→edge falloff matching the
JVM render (was flat); parity Brushes 22.75%→21.72% (swatches are a
small pixel fraction; the rest is font drift).
2026-07-14 18:27:07 +02:00
Bitsy 8c7168a1a9 feat(window): profiler render sub-phases + file output; reprioritize roadmap from data
FrameProfiler is now a named-phase singleton writing to a FILE
(CDN_PROFILE=1 -> cdn_profile.log, or =<path>) not stdout — so
GUI-subsystem apps (the demo links --subsystem,windows) profile too.
renderFrame splits into layout / draw / present sub-phases.

Measured: light screen = 75 Hz, present ~10ms (vsync wait), draw ~1.4ms
(GPU-idle); heavy 84-bubble sheet ~32ms = DRAW, not masks. ROADMAP item
2 re-ordered: dirty-region rendering + retained layers promoted (the
real lever); clip-target right-sizing demoted to memory-only/low.
2026-07-14 18:16:27 +02:00
Bitsy 377f3aa775 docs: consolidated tooling index in CLAUDE.md + simple redirecting table in README
CLAUDE.md gains a '## Tooling — what to reach for' section: a when-to-use
table covering build-sdl, compose-fork sync, compose-coverage,
material-symbols, parity, probe, the CDN_PROFILE profiler, the bridge
plugin and the demo CLI probes — each pointing at its own README. The
parity/profiler/probe entries are expanded inline (parity keeps the
'what the %differ means' detail). README gets a simpler table redirecting
to each tool's README + the bridge/CLAUDE detail.
2026-07-14 17:18:11 +02:00
Bitsy 662975c802 feat(probe): reusable native-window input+capture driver (ROADMAP item 5)
scripts/probe/ — launch a native app, send WINDOW-CLIENT-relative input
(click/hover/hold, fractional coords addressed by process name so it
ignores window position/focus), capture via PrintWindow (works occluded).
The packaged form of the ad-hoc rigs that reproduced the square-on-click
and TLS-chain bugs. Verified: demo --screen=Shapes hover+capture yields
real 1000x700 content.
2026-07-14 17:15:19 +02:00
Bitsy 3b420a7bad docs(CLAUDE): document the parity harness + how to read the %differ
Explains running scripts/parity, the one-process JVM / per-screen native
mechanics, the build/parity outputs, and — the load-bearing part — what
the number means: it's a font-drift-inflated ranking signal, not a
pass/fail score. Ghosted text + dark shapes = normal; a solid bright
block or one-sided shape = regression. Baseline gradient ~2%..32%; known
SDL gaps (solid gradients, effects) inflate specific screens.
2026-07-14 17:09:52 +02:00
Bitsy 2dad6c9bcc fix(parity): use Path.replace so re-runs overwrite prior native PNGs on Windows
Path.rename raised FileExistsError mid-sweep when a screen's
<Name>.native.png already existed from an earlier run (Windows rename
won't overwrite). replace() is an atomic overwrite everywhere.

Full 56-screen sweep now completes: smooth 2%%(Counter)..32%%(Tabs)
distribution, no catastrophic outlier — confirms this session's renderer
changes (lazy clips, batch flush, TLS fix) left no screen blank or
structurally broken. Text density + known SDL parity gaps (solid
gradients, effect differences) explain the top of the ranking.
2026-07-14 17:08:44 +02:00
Bitsy 723f80c8af feat(parity): native-vs-jvm screenshot diff harness (ROADMAP item 5)
A regression net built on the demo's dual-stack design: the same
commonMain screens render on native (SDL/Skia) and on upstream Compose
Desktop (jvm), and scripts/parity/parity.py pixel-diffs them per screen.

- MainJvm gains a headless --screenshot-all=<dir> mode: renders every
  registered screen via ImageComposeScene (density 1, same
  verticalScroll+padding wrapper as MainNative's --screen path) to PNGs
  in ONE jvm process.
- parity.py drives the native exe per screen (--screen/--screenshot),
  diffs against the jvm PNGs with a per-channel tolerance, and writes
  <pct>_<Name>_diff.png (amplified heatmap) + _compare.png (side by
  side) to build/parity/ (gitignored), ranked worst-first by the pct
  filename prefix + report.txt.

The ranking is the signal, not the absolute %: fonts differ between
stacks (text ghosts in the heatmap), so a screen jumping well above its
neighbours/history is the regression. Verified on Buttons/Shapes/Colors
(~16/14/4% — all font drift, structure aligned).
2026-07-14 17:06:08 +02:00
Bitsy 450247175f feat(sdl): deterministic native-resource lifecycle — release queue + bitmap cleaner
ROADMAP.md item 1. SdlImageBitmap textures were NEVER freed (manual SDL
memory the GC nudge can't touch) — the lazy-texture rework made it worse
since the render-target path also leaked. Now:

- NativeReleaseQueue (:ui): thread-safe queue of release actions drained
  on the MAIN thread each frame by the window loop. SDL calls aren't
  thread-safe and both disposal triggers are off-main (a Cleaner runs on
  a GC worker; close() can be reached from other dispatchers), so both
  just ENQUEUE.
- SdlImageBitmap: holds its texture/surface in a by-ref holder with a
  Cleaner that enqueues destruction (backstop) + an explicit close()
  (prompt path). Idempotent — first to run nulls the handles.
- ComposeWindow drains the queue between frames and once at teardown.

This makes the periodic GC.collect() nudge a safeguard rather than the
disposal mechanism (issue #2). Verified end-to-end in the example:
a direct enqueue and a real Cleaner->enqueue->drain cycle both fire.
close()-on-eviction wiring is a documented follow-up (caches hold the
ImageBitmap interface, not the concrete type).
2026-07-14 16:55:33 +02:00
Bitsy 5d6f772a1c fix(sdl): lazily-realized clips use push-time region, not the narrowed current clip
The TLS-chain regression (only the first card + nothing below it, until
a scroll shifted things): realizePendingClips re-intersected each
pending clip's region with the CURRENT fClip. A lazy clip can realize
arbitrarily deep inside descendants whose own clips have since narrowed
fClip — so an OUTER clip (the dialog's rounded Surface) realized with
the innermost descendant's tiny rect as its region. Its offscreen then
composited back only that sliver; every sibling card + the button
drawn into the same offscreen fell outside the blit and vanished.

Each pending clip now realizes with its OWN push-time region
(enclosing-clip-at-push ∩ bbox, already captured in PendingRoundClip),
and the real (already-narrowed) drawing clip is saved across the
realize loop and restored after, so draws into the innermost target
stay correctly clipped.

Reproduced with an outer rounded Surface + height-bounded verticalScroll
of rounded+bordered cards + a button (the TLS dialog's exact shape):
first-card-only before, full list + button after. Demo scroll/card
screens and bubble-wrap unaffected.
2026-07-14 16:37:45 +02:00
Bitsy 575c19f921 feat(window): CDN_PROFILE=1 per-phase frame profiler
Prints avg/max ms per main-loop phase (events / app pump / window pump /
render) every ~2s of rendered frames, via SDL performance counters.
Bubble-wrap sheet under hover: render=32/41ms dominates a 39ms frame —
confirms the renderer, not composition, is the 75Hz bottleneck
(ROADMAP.md item 2).
2026-07-14 16:19:56 +02:00
Bitsy 328663b9e7 docs: renderer roadmap — lifecycle, performance, parity, tooling 2026-07-14 16:14:54 +02:00
Bitsy c9ddd3488d fix(sdl): auto-flush the vertex batch only on triangle boundaries
SDL_RenderGeometry renders floor(count/3) triangles and silently drops
the tail. writeVertex auto-flushed the batch whenever it was full — at
ANY vertex boundary — so a triangle straddling the flush lost its tail
vertices AND left orphan vertices at the head of the next batch,
misaligning every triangle after it: ~350° pac-man circles and glitch
shapes that shifted with every redraw once a frame's geometry outgrew
one batch. Never seen before because clip layers flushed constantly;
bubble-wrap's new unclipped drawBehind sheet (84 bubbles, one giant
batch, ~4 overflows per frame) exposed it — most of the sheet simply
vanished.

Flush now only fires at multiples of 3, and kBatchCapacity drops
8192 → 8190 (a multiple of 3, so the cap lands on a triangle boundary
and the buffer can never overrun).
2026-07-14 16:08:07 +02:00
Bitsy 9958386792 fix(bridge): data.kres zips must not race executable declaration
The resources packaging pre-scanned link-task names in afterEvaluate —
but executables can be declared AFTER that runs, notably by the
bridge's own compose.desktop.native { entryPoint } DSL (also
afterEvaluate, registered later). The scan found no link tasks and
silently packaged nothing: 'data.kres not found' at startup. The old
single-module example masked this with stale build output; a fresh
multi-module layout exposed it.

Zips are now pre-registered lazily for every desktop target/variant and
wired via tasks.matching{}.configureEach — order-independent, and an
unrealised task is free when the target has no executable. (Registering
the zip reactively from inside the link task's configuration callback
is forbidden by Gradle — 'cannot be executed in the current context'.)
Content wiring moved into the zip's own configuration action, which
runs at realisation when the Compose plugin's prepare tasks and the
final compose.resources config exist.

Verified on the restructured multi-module bubble-wrap (shared KMP
library with the new com.android.kotlin.multiplatform.library DSL +
androidApp): packageDebugComposeResourcesMingwX64 runs, data.kres lands
next to shared.exe with the drawable + converted .cvr strings, app
boots with resources loading.
2026-07-14 14:39:29 +02:00
Bitsy f5fae6d32e docs(bridge): document settings-wide vs per-module application
Both worked since the plugin is Plugin<Any> branching on Settings vs
Project, but the module-level path was undocumented and untested —
verified now with the example applied compose-plugin style. Also spells
out that a pluginManagement plugins{} entry only pins the version while
the settings-level plugins{} block is what applies build-wide.
2026-07-14 13:08:39 +02:00
Bitsy b94f1f7bbb feat(bridge): compose.desktop.native { entryPoint } — application{} for native
The native counterpart of compose.desktop { application { mainClass } }:
the bridge grafts a 'native' extension onto the Compose plugin's desktop
extension (extensions are ExtensionAware — no compile-time dependency)
and, in afterEvaluate, declares binaries.executable(entryPoint) on every
Kotlin/Native DESKTOP target (mingw/linux/osx families). Targets that
already declare an executable are skipped, so manual configuration wins.

KGP access is reflective for the same reason as the resources packaging:
settings-applied plugins live in a parent classloader that cannot see
project-plugin types.

Replaces the last piece of consumer boilerplate — bubble-wrap's build
shrinks to targets + dependencies + the two entry points, verified
end-to-end (executable declared by the DSL links, bundles data.kres,
boots).
2026-07-14 13:04:38 +02:00
Bitsy ffbb87eddd perf(sdl): containment-gate text draws under pending clips
Text runs no longer force mask realization unconditionally: like other
draws they pass a containment test against the pending rounded clips
(with a 2px margin for glyph overhang / AA bleed). Small centred labels
inside circle-clipped nodes — a popped bubble's tiny text — previously
cost a full offscreen mask pass each, per frame.

Bubble-wrap probe (84 bubbles, half popped, continuous animation):
23 FPS before the lazy-clip series, 39 after it.
2026-07-14 12:47:18 +02:00
Bitsy 63fad4083f fix(sdl): lazily-realized clip masks belong to the clip's save-frame
The press artifact ('square on click, hover fine'): a press draws TWO
overhanging circles — the state layer, then the ripple animation — each
inside its own inner clipRect{} save-frame. The first one realized the
pending mask INSIDE the ripple's inner frame, whose saved clipLayers
count predated the realization, so that frame's restore() composited
and closed the mask early; the second circle then drew with no pending
and no mask — a faint square filling the clip's rect region. Hover only
draws one circle, hence always looked correct.

A lazily-realized mask is now attributed to the save-frame that PUSHED
the clip: PendingRoundClip records fStack.size at push, and realization
bumps the clipLayers threshold of every save-state entered since, so
only the owning frame's restore composites the layer — matching where
the eager implementation would have opened it.

Reproduced and verified with an automated press-hold rig (locate bubble
by colour, hold, capture mid-hold): square before, round after; eager-
forced realization used as the discriminating experiment.
2026-07-14 12:44:15 +02:00
Bitsy 872d740ce7 perf(sdl): lazy rounded clips — record, prove-safe, or realize on demand
Opening an offscreen mask per rounded clip is the most expensive thing
the SDL canvas does (two render-target switches + clear + corner cut +
feather + composite) — and Compose's dominant idiom
clip(shape).background(color) never needs one. clipRoundRect now only
RECORDS the clip (the rect part applies immediately); each draw either
proves itself safe or realizes the mask on demand:

- full-cover solid rect over the innermost pending clip (the background
  idiom) -> emitted directly as an antialiased rounded-rect fill,
- content whose device aabb passes a 4-corner ellipse containment test
  against every pending outline -> drawn as-is under the rect clip,
- anything genuinely crossing an arc (overhanging text, expanding
  ripples, difference clips, shadows) -> realizes the offscreen masks
  (outermost first) exactly as before.

save/restore tracks pending depth; never-realized pendings pop for
free. Measured on the bubble-wrap sheet (84 circle-clipped bubbles +
highlight dots, continuous animation): 23 -> 37 FPS.

Known open issue under investigation: a press on a bubble can show a
square state-layer/ripple artifact (hover is correct) — suspected
custom-blend fallback in the mask feather pass; follow-up commit.
2026-07-14 12:27:58 +02:00
Bitsy 4c2d46429d fix(publish): Windows owns the root metadata modules — 0.1.15 roots had no mingwX64 variants
The KotlinMultiplatform (root) publications ran on the macOS job, and a
macOS host never declares the mingwX64 target (vHostSupportsMingw) — so
every published root .module carried variants for linux/macos/jvm but
NOT mingw_x64. Windows consumers of the bridge resolved the root fine
and then found 'No matching variant' for every coordinate: v0.1.15 (and
every earlier release) is unusable from the published root modules on
Windows. Local mavenLocal flows never caught it because this repo's dev
box IS Windows, whose roots include all targets.

Two changes:
- createRenderBackend / rendererPreferredGpuMode become a real
  expect/actual seam (:ui nativeMain expect, actuals in each renderer
  source set). They were duplicate no-expect declarations — fine
  per-target, but shared nativeMain METADATA couldn't see them when a
  host's targets span both renderers, which is exactly what blocked
  :window's metadata compilation on Windows hosts.
- The publish workflow moves the KotlinMultiplatform + Jvm + bridge
  plugin publications to the WINDOWS job (the only host declaring every
  target) and updates the delete-then-retry ownership map to match.

Verified on Windows: repo-wide common+native metadata sweep green for
all library modules, full publishKotlinMultiplatformPublicationToMavenLocal
(root .module variants now cover linux_x64/linux_arm64/macos_arm64/
mingw_x64/jvm), demo links + boots, and the bubble-wrap consumer example
resolves and links mingwX64 through the bridge against the local roots.
2026-07-14 10:34:50 +02:00
Bitsy 120abbad93 fix(sdl): glyphs scale with the layer + antialiased, overlap-free clip masks
Text: drawNativeText mapped the pen position through the canvas affine
but passed LOCAL font size and centering boxes to the glyph renderer —
inside graphicsLayer(scale) text rendered full-size and off-centre
(JVM/Skia scales glyphs with the canvas). Font size and boxes now
switch to device units at the drawText boundary (affine column norms);
wrapping stays in local units so line breaks keep matching layout.

Clip masks: the round-rect corner cutter now clamps device radii to the
int-truncated bbox half-size (opposite cuts overlapped by a fraction of
a pixel, flattening N/E/S/W into an octagon look), adapts segment count
to on-screen arc length (~4px/vertex, Sdl3DrawScope density), and adds
a ~1.2px feathered ring (custom dst*=srcAlpha blend) — SDL_RenderGeometry
has no AA, so mask edges were visibly rougher than drawn shapes.

Parity-verified against upstream JVM with OS screenshots of the same
pre-popped bubble-wrap sheet.
2026-07-14 01:30:53 +02:00
Bitsy a39002f1b8 feat(sdl3.def): carry the Linux Skia-stack linker flags in the klib
-L multi-arch dirs + fontconfig/GL/X11 moved from the apps' build files
into sdl3.def's linux linkerOpts — cinterop endorses them into the klib,
so consumers (bridge apps included) need no linkerOpts at all on Linux.
Harmless under -Prenderer=sdl3 (unused -l) and when a -L dir is absent
(LLD skips it). The apps keep their copies until the next publish makes
the klib-carried flags the ones consumers resolve; duplicates are inert.

Windows flags stay app-side ON PURPOSE: --subsystem,windows / -Wl,-s are
policy (no console window, stripped binary), not requirements — a .def
would force them on every consumer binary including debug builds and
test executables.
2026-07-14 01:10:37 +02:00
Bitsy fb470ecf0c fix(sdl): scale round-rect clip radii into device space — scaled circles became lozenges
Sdl3Canvas.clipRoundRect cuts the corner mask in DEVICE space (the bbox
goes through the canvas affine) but used the LOCAL user-space corner
radii. Under a graphicsLayer scale the oversized cuts consumed nearly
the whole shape — a circle clipped at scale 0.55 rendered as a diamond
(bubble-wrap's popped bubbles on the SDL renderer vs perfect circles on
JVM). Scale the radii by the affine's column norms alongside the bbox;
rotation already degrades to AABBs on this path.

Repro: two OS-level screenshots of the same pre-popped sheet, native vs
jvm, before/after.
2026-07-14 01:10:10 +02:00
Bitsy 2b6ac65a1f feat(bridge): auto-package composeResources into data.kres for consumers
With the official org.jetbrains.compose plugin applied, the bridge now
registers a package<Variant>ComposeResources<Target> Zip per native
executable link task: the Compose plugin's PREPARED resources (values
xml already converted to .cvr — zipping the raw source dir would break
stringResource) land in data.kres next to the binary, STORED, under the
composeResources/<res-package>/ prefix the generated accessors carry.
Consumers get the official composeResources UX on the port's targets
with zero build-script code.

Convention-based on purpose: applied from settings, the plugin's
classloader is a PARENT of the project's buildscript loader, so KGP /
Compose-plugin types are structurally invisible (typed compileOnly
access dies with NoClassDefFound at class-init). Link-task names,
binary output dirs, prepare-task names and the prepared-resources dir
are stable conventions; packageOfResClass is read reflectively with the
documented {group}.{module}.generated.resources fallback.

Verified end-to-end with the bubble-wrap example: drawable + strings.xml
resolve through Res.* on mingwX64 (data.kres auto-bundled, app boots,
.cvr conversion intact) while jvm and android keep the official
pipeline.
2026-07-14 00:58:29 +02:00
Bitsy 1b2e83a155 ci(publish): delete-then-retry so a mid-publish network blip can't poison a release
GitHub Packages forbids overwriting files that already exist in a
version, and maven-publish uploads each publication's files sequentially
from the first one. When the v0.1.13 macOS run lost DNS mid-publish, the
retries hit 409 Conflict on the first already-uploaded file of each
publication and aborted before re-attempting the missing tail — the
version could never complete (a 409 is only benign when the publication
had already fully landed, which the 409 alone can't tell you).

Between attempts each host now deletes ITS OWN package versions for the
current release via the API (GITHUB_TOKEN, packages:write) and starts
the upload from a clean slate. Hosts own disjoint package names (roots /
-macosarm64 / -jvm / bridge on macOS, -mingwx64 on Windows, -linuxx64 /
-linuxarm64 on Linux) so parallel jobs can't clobber each other; a 403
on delete degrades to the old blind retry. Last-version deletes (a
package new in this release) fall back to deleting the whole package.
2026-07-13 23:36:12 +02:00
Bitsy 8a6cb868d9 fix(sdl): defer decoded-image texture creation to the first main-thread draw
Sdl3EncodedImageDecoder ran SDL_CreateTextureFromSurface on the
resources pipeline's Dispatchers.Default workers — SDL renderer calls
are not thread-safe, so every painterResource decode on the SDL
renderer was a sporadic-crash risk (same family as #1, SDL flavour).

decode() now does the pure IMG_Load_IO surface decode only (renderer-
free, worker-safe) and hands the surface to SdlImageBitmap, which
creates the texture lazily on first access — createCanvas / the draw
paths, main thread only. Blend modes preserved: straight-alpha BLEND
for decoded surfaces, premultiplied for the vector render-target path.

Verified on Windows: demo --screen=Images renders PNG/JPG/SVG/Android-
vector through the new worker-decode → lazy-texture path.
2026-07-13 22:31:04 +02:00
Bitsy 4a7080b313 fix(window): periodic GC nudge so Cleaner-managed native memory is released
Renderer resources (Skia surfaces / images / typefaces, SDL textures)
free their NATIVE memory through Cleaners that only run when the
Kotlin/Native GC collects — and a Compose app's Kotlin heap is small
enough that the allocation-driven scheduler can starve them for minutes
while the native heap balloons (50MB → 190MB navigating the demo, never
reclaimed). Not a retention leak: layers are plain value objects, the
text/image caches are capped, and the draw paths already close() their
Skia objects eagerly.

The main loop now calls GC.collect() every 10s, only after rendering
activity, scheduled between frames — ~ms pause at these heap sizes,
keeps RSS tracking real usage on both renderers.

Fixes #2
2026-07-13 22:26:35 +02:00
Bitsy 1afb13aae0 fix(skia): register an encoded-image decoder so painterResource works on Skia builds
:components-resources' skikoRenderer variant reuses the SDL actuals,
which decode through :ui's encodedImageDecoder hook — but only the SDL
backend ever registered it. On every macOS/Linux Skia build the first
painterResource() (the demo's Images screen) hit a null decoder and
crashed with 'Image decode failed'. Deterministic, not a race.

Add SkiaEncodedImageDecoder (Image.makeFromEncoded + SVGDOM fallback via
rasterizeSvg, hoisted to file scope from SkiaImageCache) drawn into a
SkiaImageBitmap, registered at SkiaRenderBackend construction — decode
runs on the resources pipeline's Dispatchers.Default workers, which the
CPU-raster path tolerates. Mark the hook @Volatile (main-thread write,
worker-thread reads) and de-SDL the error message.

Fixes #1
2026-07-13 22:26:25 +02:00
Bitsy 3d519bcfcd docs: ui-tooling-preview, apidemo JVM parity target, bridge plugin
README: module tree entries for :ui-tooling-preview and the bridge
plugin, apidemo jvm run command + native-only mTLS note, and a new
"Using it from your own build" section for the bridge.

CLAUDE.md: same module-tree additions, apidemo multiplatform layout
(compat/Compat.kt seams), :apidemo:run parity command, bridge coverage
wording, and a new pitfall: substituted Maven modules hide their
transitives from KGP's granular-metadata visibility check — declare
every artifact commonMain touches directly + a bridge rule each.
2026-07-13 22:06:45 +02:00
Bitsy 72287fc995 feat(bridge): consumer-side Gradle plugin — official CMP coords on every platform
Dependency-substitution rules can't ship inside Maven artifacts, so the
FULL-COMMONIZATION BRIDGE is published as a Gradle plugin instead:
:compose-desktop-native-bridge (id com.bitsycore.compose-desktop-native
.bridge). Consumers apply it once in settings (gradle.lifecycle
.beforeProject, Gradle 8.8+) or per module, declare the OFFICIAL
org.jetbrains.compose coordinates in commonMain, and every native
desktop configuration substitutes the published com.bitsycore.compose
.sdl klibs — android/jvm/iOS/wasm keep resolving the official artifacts.

The substituted version defaults to the plugin's own (stamped into a
resource at build time; plugin and klibs ship from the same tag) and can
be overridden with the composeDesktopNative.version property. The
runtime is deliberately absent from the table — official klibs serve
every target. Published by the macOS publish job (host-independent jar
+ plugin marker).

Verified end-to-end offline: klibs + plugin published to mavenLocal, a
scratch third-party build applying only the plugin compiled a Compose
composable for mingwX64 against official coords — dependencyInsight
shows org.jetbrains.compose.ui:ui -> com.bitsycore.compose.sdl:ui
(selected by rule).
2026-07-13 22:06:45 +02:00
Bitsy 0b4a170910 feat(apidemo): port to JVM — shared commonMain UI + per-stack seams
Same commonization as :demo: the whole UI/logic (App, Sidebar,
ViewerPanel, panels, Model, Http, Persist, SyntaxHighlight, ...) moves
to commonMain compiling against the official Maven compose coords
(native configs substitute the port modules via the root bridge); a
jvm() target runs it on upstream Compose Desktop.

Seams (compat/Compat.kt expects; native actuals are pure delegation to
com.compose.sdl, jvm actuals use AWT + upstream desktop APIs):
file dialogs, appDataDir, revealInFileManager, clipboard text entry,
in-memory response images (Skia decode / loadSvgPainter on jvm),
wrappedRowCount (upstream TextMeasurer at density 1), editorTabWidth,
createApiHttpClient (Curl native / CIO jvm), InstallWindowHooks
(close-intercept + app-wide shortcuts -> Window onCloseRequest/
onPreviewKeyEvent), systemFileSystem (okio SYSTEM isn't in common
metadata), expect/actual mono font. Scrollbar/SplitPane/LayoutExt are
copies of the port's pure-Compose project code (ScrollState adapter
dropped — reads foundation internals; only a LazyColumn is scrollbared).

mTLS (CurlMtls + per-OS ClientCert*) stays native: ChainCert/TlsChain +
expects move to TlsCommon.kt, jvm actuals report the feature as
native-only. kotlinx-coroutines-swing provides Dispatchers.Main on
desktop jvm (App launches on it); jvmProcessResources stages
NotoSansMono + the used Material Symbols fonts.

Verified: common metadata + mingwX64 + jvm compile, native exe links
and boots, jvm app runs (fonts staged, clean exit through the
persist-then-close hook).
2026-07-13 21:40:24 +02:00
Bitsy 72e6f145b3 feat(ui-tooling-preview): common @Preview via vendored module + bridge
The canonical common @Preview is androidx.compose.ui.tooling.preview
.Preview from org.jetbrains.compose.ui:ui-tooling-preview (the umbrella
components-ui-tooling-preview annotation is deprecated). The Maven
artifact ships no mingwX64/linux klibs, so vendor the upstream module
verbatim as :ui-tooling-preview (compose-fork manifest at
COMPOSE_CORE_REF, commonMain + nonJvm actual) and substitute the Maven
coord for it on native configurations via the root bridge.

:demo declares the coord in commonMain; previews render through the jvm
parity target (IntelliJ/AS with the CMP plugin). Example on ColorsScreen.
2026-07-13 21:40:07 +02:00
Bitsy 367ed94c01 refactor(screens): streamline import statements across multiple screen files 2026-07-13 20:46:09 +02:00
Bitsy fa9bd93ada fix(demo): commonize dependencies so commonMain metadata + IDE analysis resolve
demo's commonMain had NO Compose dependencies — shared screens only
compiled because each target folds them in with jvmMain/nativeMain deps,
leaving compileCommonMainKotlinMetadata (and the IDE's common analysis)
broken between the JetBrains artifacts and the local fork.

Apply the :material-symbols pattern: commonMain declares the official
Maven coords for everything the shared screens touch (runtime, ui,
ui-graphics/-text/-unit/-geometry/-util, foundation(+layout),
animation(+core), material3, nav3-runtime, nav3-ui,
lifecycle-viewmodel-navigation3); metadata + jvm resolve them from
Maven, and the root FULL-COMMONIZATION BRIDGE substitutes each for its
port module on native configurations (new rules: ui-text/ui-graphics ->
:ui, ui-unit/-geometry/-util, foundation-layout, animation(+core),
navigation3-ui). Each artifact is declared DIRECTLY because transitives
of a substituted module are invisible to KGP's granular-metadata
visibility check.

nativeMain keeps only :window (the rest now flows from commonMain);
jvmMain keeps only its version bumps (nav3-runtime 1.2.0-alpha05) and
compose.desktop.currentOs.

Verified: :demo compileCommonMainKotlinMetadata + mingwX64 + jvm compile,
mingwX64 link, and a --screenshot runtime smoke test all pass.
2026-07-13 20:43:14 +02:00
Bitsy d1551bc9c1 fix(publish): resolve ui-graphics/ui-unit in material-symbols common metadata
The macOS publish job (the only one running the KotlinMultiplatform
metadata publication) failed in :material-symbols:
compileCommonMainKotlinMetadata — Color/isSpecified/Dp/dp unresolved.
They live in ui-graphics/ui-unit, TRANSITIVES of the Maven ui artifact:
the native substitution (ui -> project(":ui")) hides those transitives
from KGP's granular-metadata visibility check, dropping their metadata
klibs from the commonMain classpath.

Declare both explicitly in commonMain and teach the bridge their
project equivalents (ui-graphics is merged into :ui; ui-unit has its
own module).
2026-07-13 20:34:31 +02:00
DWA e9527ee8c2 refactor(publish): update caching and build logic for native dependencies based on platform 2026-07-13 17:47:41 +02:00
DWA 6040816cf7 refactor(scrollbar): improve drag handling and scrolling logic for smoother interactions 2026-07-13 17:31:07 +02:00
Bitsy 6f3bc915b2 docs: update build instructions and system dependencies for SDL3 integration 2026-07-12 23:46:54 +02:00
Bitsy 1ba84a3a2c feat(material-symbols): common API with per-stack renderers (native IconFont / JVM Skiko)
MaterialSymbols<Style> moves to commonMain so shared app code can use it on
both stacks; an internal expect/actual picks the renderer:
- native: unchanged — :foundation IconFontIcon (IconFont handles SDL3/Skia)
- jvm (NEW): Skiko direct — Typeface.makeClone per axes combination +
  TextLine on the native canvas. Not the upstream Font(variationSettings)
  route: published desktop artifacts never apply the settings at typeface
  load, and even at the pinned dev ref FontCache drops them from its cache
  key, collapsing every axes combination onto the first one loaded. Fill is
  quantised to 1/100 steps so animated FILL keeps the clone cache bounded.

Wiring:
- commonMain declares the official Maven compose coords; a repo-wide
  FULL-COMMONIZATION BRIDGE (root build) substitutes ui/foundation/material3
  for the project modules on native configurations (runtime never swapped)
- jvm stack pinned to 1.12.0-beta01+dev4324 = the vendored COMPOSE_CORE_REF
  (material3 rides its own train: 1.12.0-alpha03+dev4324); forced on jvm
  configs since gradle orders +dev below the plain version
- demo stages used icon fonts onto the jvm classpath (jvmProcessResources);
  publish workflow adds the jvm publication (macOS host)

Demo:
- DemoIcon is now common — same Material Symbols glyphs on both stacks,
  material-icons-extended jvm shim deleted
- IconsScreen (material3) keeps Icon(painter) + IconButton; the symbols
  engine gets its own Others > MaterialSymbols screen: axis sweeps, style
  families, and an animated-FILL selected-state transition
2026-07-12 23:24:53 +02:00
Bitsy e807ecf973 feat(demo): implement clipboard functionality for demo jvm 2026-07-12 22:08:13 +02:00
Bitsy dc35f5bd37 build: update build scripts to use Python for SDL builds 2026-07-12 21:16:36 +02:00
Bitsy 4888fdcda0 build: update build scripts to use Python for SDL builds 2026-07-12 21:10:44 +02:00
Bitsy 85e0179590 ci(publish): use python zipfile so macOS extracts keep the exec bit
`jar --create` strips file modes to 0644 in the archive, so any .kexe
inside extracted by macOS Archive Utility (or `ditto` / `unzip -oa`)
comes out non-executable and fails to launch. Switch the packaging
step to python's `zipfile` module — it's on every runner and preserves
the source file's exec bit + marks entries as Unix-origin, so
extractors read the right permissions.

Verified locally with ditto (Archive Utility's engine): jar-produced
zips extract source `hello.txt` at 0755 down to 0644; python-produced
zips preserve 0755 through the round-trip.

Only touches the CI workflow — next release picks up the fix, no
retag for v0.1.11 (which is publishing now with jar).
2026-07-12 18:38:05 +02:00
225 changed files with 17813 additions and 4869 deletions
+93 -26
View File
@@ -18,9 +18,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
# Each host runs only the publish tasks Gradle actually generated for it. # Each host runs only the publish tasks Gradle actually generated for it.
# macOS also publishes the shared kotlinMultiplatform metadata module. # WINDOWS also publishes the shared kotlinMultiplatform metadata modules
# No library module declares jvm() today — apps do their own JVM parity # (see its matrix entry) + the jvm jar + the bridge plugin.
# build against upstream Compose, so JVM isn't part of the publication.
# `target` is the Gradle K/N target token (Capitalised) — one per host, # `target` is the Gradle K/N target token (Capitalised) — one per host,
# used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is # used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is
# skipped for the app build because SDL3 static libs on the ubuntu runner # skipped for the app build because SDL3 static libs on the ubuntu runner
@@ -32,7 +31,6 @@ jobs:
target: MacosArm64 target: MacosArm64
exe_ext: kexe exe_ext: kexe
tasks: >- tasks: >-
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
publishMacosArm64PublicationToGitHubPackagesRepository publishMacosArm64PublicationToGitHubPackagesRepository
- runner: ubuntu-22.04 - runner: ubuntu-22.04
host: linux host: linux
@@ -45,8 +43,18 @@ jobs:
host: windows host: windows
target: MingwX64 target: MingwX64
exe_ext: exe exe_ext: exe
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is
# the ONLY host that declares every target (vHostSupportsMingw is
# host-gated), so only its generated .module files carry the full
# variant table — the macOS-published roots of v0.1.15 had NO
# mingwX64 variants and Windows consumers could not resolve them.
# The host-independent jvm jar (:material-symbols) and the bridge
# plugin ride along on the same job.
tasks: >- tasks: >-
publishMingwX64PublicationToGitHubPackagesRepository publishMingwX64PublicationToGitHubPackagesRepository
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
publishJvmPublicationToGitHubPackagesRepository
:compose-desktop-native-bridge:publishAllPublicationsToGitHubPackagesRepository
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
@@ -95,23 +103,33 @@ jobs:
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at # intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
# link time via --defsym in sdl3.def. # link time via --defsym in sdl3.def.
# Build static SDL3 / SDL3_ttf / SDL3_image / FreeType from # Build the static native deps from scripts/build-sdl/build-sdl.properties.
# scripts/build-sdl/build-sdl.properties. Cached against the manifest + # Only Windows ships the SDL3 renderer (no Skiko on mingw), so only it needs
# scripts so version bumps invalidate cleanly. # SDL3_ttf / SDL3_image / FreeType (SDL3_ttf text, SDL3_image decode, FreeType
# `cache_version` is a manual bump for when the toolchain (runner OS, # icon-axis rasterisation). macOS / Linux publish the Skia renderer — Skiko
# compiler flavour, K/N sysroot expectations) changes without any # handles text / images / icons — so they only need base SDL3 for the main
# scripts/build-sdl/ file changing. Increment to force a rebuild. # loop (windowing / input / audio / filesystem). Matches the cinterop guard
# in compose/ui/ui/build.gradle.kts, which skips those three cinterops on the
# Skia targets. Cached against the manifest + scripts so version bumps
# invalidate cleanly; bump the `v3` key when the toolchain (runner OS,
# compiler flavour, K/N sysroot) changes without a scripts/build-sdl/ edit.
- name: Cache libs/ - name: Cache libs/
id: cache-libs id: cache-libs
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: libs path: libs
key: sdl-libs-v2-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }} key: sdl-libs-v3-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
- name: Build static SDL / TTF / image / FreeType # Windows: full SDL3 renderer stack. macOS / Linux (Skia): base SDL3 only.
- name: Build static native deps
if: steps.cache-libs.outputs.cache-hit != 'true' if: steps.cache-libs.outputs.cache-hit != 'true'
shell: bash shell: bash
run: bash scripts/build-sdl/build-all.sh run: |
if [ "${{ matrix.host }}" = "windows" ]; then
python3 scripts/build-sdl/build-all.py
else
python3 scripts/build-sdl/build-all.py sdl3
fi
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored # src/vendor/ trees are gitignored — a fresh checkout has zero vendored
# androidx.compose.* files. Sync copies them in from the upstream refs # androidx.compose.* files. Sync copies them in from the upstream refs
@@ -121,16 +139,65 @@ jobs:
shell: bash shell: bash
run: bash scripts/compose-fork/sync.sh run: bash scripts/compose-fork/sync.sh
# GitHub Packages sometimes returns 409 Conflict on a fresh upload when # GitHub Packages FORBIDS overwriting a file that already exists in a
# the same coordinate was recently deleted (soft-delete propagation). # version, and maven-publish uploads each publication's files
# `--continue` lets Gradle finish the rest of the publish tasks past a # SEQUENTIALLY from the first one. So when an upload dies mid-publication
# single 409, and the outer retry re-runs the whole thing (Gradle skips # (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409
# already-uploaded artifacts). Two attempts is enough for the transient # Conflict on the first already-uploaded file and aborts BEFORE reaching
# cases we've hit; the third pass falls through as a hard failure. # the missing tail files — the version is poisoned and can never
# complete. A 409 is therefore only benign when the publication had
# already completed; you can't tell from the 409 alone. The fix: between
# attempts, DELETE this host's package versions for the current release
# and re-upload from a clean slate. Each host owns DISJOINT package
# names (roots / -mingwx64 / -jvm / bridge on Windows, -macosarm64 on
# macOS, -linuxx64/-linuxarm64 on Linux), so parallel jobs never touch
# each other's uploads. Uses the run's GITHUB_TOKEN (packages: write);
# a 403 on delete just degrades to the old blind-retry behaviour.
- name: Publish to GitHub Packages - name: Publish to GitHub Packages
shell: bash shell: bash
run: | run: |
set -e set -e
VER="${PUBLISH_VERSION#v}"
OWNER="${GITHUB_REPOSITORY%%/*}"
# Keep in sync with the module list in settings.gradle.kts.
MODULES="ui ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple window material-symbols navigation3-ui components-resources"
case "${{ matrix.host }}" in
macos) SUFFIXES="-macosarm64" ;;
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
esac
host_packages() {
for m in $MODULES; do
for s in $SUFFIXES; do
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
done
done
if [ "${{ matrix.host }}" = "windows" ]; then
echo "com.bitsycore.compose.sdl.material-symbols-jvm"
echo "com.bitsycore.compose.sdl.compose-desktop-native-bridge"
echo "com.bitsycore.compose-desktop-native.bridge.com.bitsycore.compose-desktop-native.bridge.gradle.plugin"
fi
}
cleanup_host_versions() {
echo "Deleting this host's partially-published $VER package versions"
host_packages | while read -r pkg; do
vid=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions?per_page=100" \
| jq -r --arg v "$VER" '[.[]? | select(.name==$v) | .id] | first // empty')
[ -z "$vid" ] && continue
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
if [ "$code" != "204" ]; then
# Deleting the LAST version of a package is refused (400) —
# a package new in this release: drop the whole package.
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/$OWNER/packages/maven/$pkg")
fi
echo " $pkg@$VER -> HTTP $code"
done
}
for attempt in 1 2 3; do for attempt in 1 2 3; do
echo "::group::Publish attempt $attempt" echo "::group::Publish attempt $attempt"
if ./gradlew ${{ matrix.tasks }} --continue --no-daemon --stacktrace; then if ./gradlew ${{ matrix.tasks }} --continue --no-daemon --stacktrace; then
@@ -138,7 +205,11 @@ jobs:
exit 0 exit 0
fi fi
echo "::endgroup::" echo "::endgroup::"
[ $attempt -lt 3 ] && { echo "::warning::Publish attempt $attempt failed — sleeping 60s and retrying"; sleep 60; } if [ $attempt -lt 3 ]; then
echo "::warning::Publish attempt $attempt failed — cleaning this host's $VER packages and retrying"
cleanup_host_versions || true
sleep 30
fi
done done
echo "::error::Publish failed after 3 attempts" echo "::error::Publish failed after 3 attempts"
exit 1 exit 1
@@ -167,15 +238,11 @@ jobs:
target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}') target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}')
version="${GITHUB_REF_NAME#v}" version="${GITHUB_REF_NAME#v}"
mkdir -p dist mkdir -p dist
# Windows Git Bash has no `zip`; every runner has JDK 21 → use `jar cf`
# which writes a valid ZIP file. `-C dir file` = enter dir then add
# `file` at the archive root, so the archive stays flat like `zip -j`.
for app in demo apidemo; do for app in demo apidemo; do
src="$app/build/bin/$target_dir/releaseExecutable" src="$app/build/bin/$target_dir/releaseExecutable"
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip" zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
jar --create --no-manifest --file "$zip_path" \ python3 -c 'import sys,os,zipfile; z=zipfile.ZipFile(sys.argv[1],"w",zipfile.ZIP_DEFLATED,compresslevel=9); [z.write(f,os.path.basename(f)) for f in sys.argv[2:]]; z.close()' \
-C "$src" "$app.${{ matrix.exe_ext }}" \ "$zip_path" "$src/$app.${{ matrix.exe_ext }}" "$src/data.kres"
-C "$src" "data.kres"
done done
ls -la dist/ ls -la dist/
+3
View File
@@ -61,3 +61,6 @@ bin/
### Python ### ### Python ###
__pycache__/ __pycache__/
*.pyc *.pyc
# Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest
/*.bmp
+122 -42
View File
@@ -3,6 +3,21 @@
Guidance for Claude Code working in this repository. This is the primary Guidance for Claude Code working in this repository. This is the primary
context — read it first, then look at the files it points to. context — read it first, then look at the files it points to.
## Documentation map
- [README.md](README.md) — public overview + quickstart (bridge plugin,
`nativeComposeWindow`, sample apps).
- [RENDERER.md](RENDERER.md) — the rendering layer: architecture, the retained-
layer engine, convergence decisions, learnings, and open work. Read it before
touching any renderer, graphics-actual, or layer-engine code.
- [TOOLING.md](TOOLING.md) — build/vendor/verify scripts and workflows
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
verify-mac) + the version map and the ref-bump / release runbooks.
- [TODO.md](TODO.md) — audited list of no-ops, stubs, and hardcodes left in the
port, with severity, plus what's missing for a stable 1.12 release.
- This file — architecture, module layout, vendoring rules, source-set
hierarchy, density flow, conventions, and common pitfalls.
## What this project is ## What this project is
**ComposeNativeSDL3** — a Kotlin/Native port of Compose Multiplatform running **ComposeNativeSDL3** — a Kotlin/Native port of Compose Multiplatform running
@@ -42,7 +57,12 @@ compose/
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi) │ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.* │ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.* │ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
│ └── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.* │ ├── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
│ └── ui-tooling-preview/ → :ui-tooling-preview — androidx.compose.ui.tooling.preview.*
│ (the common @Preview + PreviewParameterProvider,
│ vendored verbatim; the Maven artifact ships no
│ mingwX64/linux klibs). IDE-only metadata — previews
│ render through the apps' jvm parity targets.
├── animation/ ├── animation/
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.* │ ├── animation-core/ → :animation-core — androidx.compose.animation.core.*
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core) │ ├── animation/ → :animation — androidx.compose.animation.* (non-core)
@@ -60,8 +80,21 @@ compose/
utils/ utils/
└── material-symbols/ → :material-symbols — codepoints + all three style objects └── material-symbols/ → :material-symbols — codepoints + all three style objects
(Outlined / Rounded / Sharp). Apps get one dep; (Outlined / Rounded / Sharp). COMMON API (usable from
the consumer Zip task bundles only the fonts used. shared app code) + per-stack actuals: native renders
via :foundation IconFontIcon (IconFont handles the
SDL3/Skia split), jvm() via Skiko directly
(Typeface.makeClone per axes — upstream's FontCache
drops variationSettings from its key). Its commonMain
declares official Maven compose coords — the root
build's FULL-COMMONIZATION BRIDGE substitutes the
whole ui / foundation / animation / material3 /
nav3-ui family to project modules on native configs
(:demo and :apidemo commonMains rely on the same
bridge; :compose-desktop-native-bridge ships it to
consumers). Apps get one dep; the consumer Zip task
bundles only the fonts used (native) and
jvmProcessResources stages the same fonts (jvm).
components/ components/
└── resources/library/ → :components-resources — the OFFICIAL Compose resources └── resources/library/ → :components-resources — the OFFICIAL Compose resources
@@ -93,12 +126,28 @@ demo/ → :demo — flagship showcase app (30+ screens) + th
MULTIPLATFORM: also has a jvm() target running the SAME shared MULTIPLATFORM: also has a jvm() target running the SAME shared
screens on stock JVM Compose Desktop (`./gradlew :demo:run`, screens on stock JVM Compose Desktop (`./gradlew :demo:run`,
MainJvmKt) — the parity reference; differences vs native = port bugs MainJvmKt) — the parity reference; differences vs native = port bugs
apidemo/ → :apidemo — Postman-style REST API manager apidemo/ → :apidemo — Postman-style REST API manager. MULTIPLATFORM like :demo
scripts/ → vendor-sync + python helper scripts (compose-fidelity/coverage, (`./gradlew :apidemo:run`): the whole UI lives in commonMain
material-symbols generate/subset) + compose-fork/; against the official Maven coords; SDL-backed APIs go through
scripts/build-sdl/ = Windows static-lib build scripts (bash) expect/actual seams (compat/Compat.kt — native actuals delegate
to com.compose.sdl, jvm actuals use AWT + upstream desktop).
mTLS / TLS-chain inspection stays native-only (bundled libcurl).
gradle-plugin/
└── compose-desktop-native-bridge/ → :compose-desktop-native-bridge — the CONSUMER-side bridge as a
published Gradle plugin (id com.bitsycore.compose-desktop-native
.bridge, applies to Settings or Project): substitution rules
can't ship inside Maven artifacts, so third-party apps apply the
plugin, declare OFFICIAL CMP coords in commonMain, and native
configurations swap in the published com.bitsycore.compose.sdl
klibs. Substituted version defaults to the plugin's own
(override: composeDesktopNative.version property). Published by
the macOS publish job.
scripts/ → vendor-sync + python helper scripts (compose-coverage = API
coverage/fidelity vs upstream, material-symbols generate/subset)
+ compose-fork/;
scripts/build-sdl/ = static-lib build script (python)
libs/ → gitignored per-host static SDL3 / SDL3_ttf / SDL3_image / FreeType libs/ → gitignored per-host static SDL3 / SDL3_ttf / SDL3_image / FreeType
output of scripts/build-sdl/build-*.sh on Windows output of scripts/build-sdl/build-all.py on Windows
``` ```
Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) — Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) —
@@ -194,10 +243,23 @@ Two categories of code live in each module:
NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding
`src/{commonMain,nativeMain,…}` tree, add a header comment noting `src/{commonMain,nativeMain,…}` tree, add a header comment noting
which upstream file it derived from and what changed, and comment which upstream file it derived from and what changed, and comment
its line out of `compose-fork.txt`. Now it's a project file — the its line out of `compose-fork.txt`. **ALWAYS also add the
next sync won't overwrite it, and future upstream changes to that machine-readable provenance line** so the copy can be diffed against
file need to be reconciled by hand. This is fine; do it when the its base and against later upstream versions:
edit is small and the file is unlikely to churn upstream.
```
// VENDOR-BASE: <upstream-repo-relative-path> @ <pinned-tag>
// VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files
```
`scripts/compose-fork/check-vendor-drift.py` reads these at every ref
bump: it flags any file whose recorded base lags the current pin and
(with the local clone) reports whether the upstream base ACTUALLY
changed base..pin — i.e. whether the copy needs hand-reconciling or
just a ref re-stamp. Now it's a project file — the next sync won't
overwrite it, and future upstream changes to that file need to be
reconciled by hand. This is fine; do it when the edit is small and
the file is unlikely to churn upstream.
4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`, with 4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`, with
an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream
@@ -241,11 +303,15 @@ commonMain
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3. attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
``` ```
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are declared identically `createRenderBackend(…)` + `rendererPreferredGpuMode()` are `expect`s in
in both `skikoRendererMain` and `sdlRendererMain`. `:window` calls them straight `:ui`'s nativeMain with `actual`s in BOTH `skikoRendererMain` and
from `:ui` — no `expect`/`actual`, no factory layer — and the right impl `sdlRendererMain` — unambiguous because **only one of the two renderer source
resolves because **only one of the two renderer source sets is attached to a sets is attached to a given target**. (They used to be plain duplicate
given target**. Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are declarations with no expect; that compiled per-target but shared nativeMain
METADATA couldn't see them on a host whose targets span both renderers, which
blocked the WINDOWS host from producing :window's KotlinMultiplatform
publication — and Windows must publish the root modules, see the publish
workflow.) Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
**not even created**, so Gradle has nothing to warn about and Skiko is never **not even created**, so Gradle has nothing to warn about and Skiko is never
pulled in. pulled in.
@@ -307,26 +373,19 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3 ./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared # Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
# demo screens on upstream Compose; differences vs the native build = port bugs. # screens on upstream Compose; differences vs the native build = port bugs.
./gradlew :demo:run ./gradlew :demo:run
./gradlew :apidemo:run
``` ```
### System dependencies ### System dependencies
**macOS (default Skia build):** `brew install sdl3`. Only need `sdl3_ttf` SDL3 + SDL3_ttf + SDL3_image + FreeType (+ image codecs) are **built from
if you set `-Prenderer=sdl3`. Skiko klibs come from Maven. source as static libraries on every OS** and linked straight into the
executable — no brew/apt SDL packages, no runtime .dll/.so/.dylib; a
**Linux (default Skia build):** `sudo apt install libsdl3-dev`. Same caveat distributable is `<app>` + `data.kres`. Build them once per host with
for SDL3_ttf. `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements and
the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
**Windows (mingwX64 — always uses SDL3 + SDL3_ttf + SDL3_image + FreeType):**
these four libraries + image codecs are **linked statically into the
executable** — no runtime DLLs, distributable is just `<app>.exe` +
`data.kres`. They're not downloaded — `scripts/build-sdl/build-all.sh` (from Git
Bash) builds them from source as static libs into a gitignored, in-repo `libs/`
folder. Needs: git, cmake, mingw-w64 gcc/g++ on PATH, plus curl + python for
ninja fetch. `scripts/build-sdl/build-all.sh` = `build-freetype.sh` → `build-sdl3.sh` →
`build-sdl3-image.sh` → `build-sdl3-ttf.sh`.
## Runtime bundling — data.kres ## Runtime bundling — data.kres
@@ -352,19 +411,13 @@ executable, loaded via `SDL_GetBasePath()`). Contents:
```bash ```bash
# Sync every module's compose-fork.txt against the pinned upstream ref. # Sync every module's compose-fork.txt against the pinned upstream ref.
scripts/compose-fork/sync.sh scripts/compose-fork/sync.sh
# Sync one module by direct manifest path (module names use ':' → '/' → skip
# 'compose/' prefix — nested modules like compose/sdl/window pass the full path).
scripts/compose-fork/sync.sh compose/ui/compose-fork.txt
# Re-format a manifest (align columns, group by upstream folder).
scripts/compose-fork/format-manifest.py --discover ../cmp-ref \
--manifest compose/ui/compose-fork.txt
``` ```
Upstream ref: `scripts/compose-fork/compose.properties` — set to a specific commit Upstream ref: `scripts/compose-fork/compose.properties` — set to a durable tag
of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the
build tell you what broke. build tell you what broke. Per-module sync, manifest re-formatting, and the
drift / vendor-clean guardrails are in
[TOOLING.md](TOOLING.md#vendoring-upstream-compose).
## Key files by area — start here when you need to find something ## Key files by area — start here when you need to find something
@@ -418,6 +471,21 @@ build tell you what broke.
`Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't `Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't
ship drop-in equivalents for our anchor / scrim patterns). ship drop-in equivalents for our anchor / scrim patterns).
## Tooling
The full tooling reference is [TOOLING.md](TOOLING.md): the static-lib build,
vendor sync + drift/clean guardrails, API coverage, the `verify-mac` runbook,
the native-vs-JVM parity harness, the interaction probe, and the frame
profiler. Quick rules of thumb:
- Whole-project renderer/layout change → run **parity** (`scripts/parity/parity.py`),
the broad net.
- Chasing one reported interaction → run the **probe** (`scripts/probe/`), targeted.
- Slow frame → **profiler** first (`CDN_PROFILE=1`), optimize second.
- Any renderer change → the **`verify-mac`** runbook gates both legs before commit.
See [RENDERER.md](RENDERER.md) for the renderer work these support.
## Conventions ## Conventions
Kotlin standard style — plain `camelCase` for parameters, local variables, Kotlin standard style — plain `camelCase` for parameters, local variables,
@@ -469,6 +537,18 @@ that should surface in tooling.
toggling the renderer property may not invalidate it. Delete toggling the renderer property may not invalidate it. Delete
`.gradle/configuration-cache/` between switches if you see weird `.gradle/configuration-cache/` between switches if you see weird
"couldn't find sdl3_ttf" errors. "couldn't find sdl3_ttf" errors.
- **Substituted Maven modules hide their transitives from common metadata** —
when the bridge swaps an official coord for a project module on native
configs, KGP's granular-metadata visibility check drops that Maven module's
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
'Color'` / `Cannot access class ...` in `compileCommonMainKotlinMetadata`,
while per-target compilation is fine). Declare EVERY artifact the common
code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its
own bridge rule. Note only the WINDOWS publish job compiles common metadata
(it owns the root KotlinMultiplatform publications — the only host that
declares every target, so only its .module files carry the full variant
table; macOS-published roots left v0.1.15 without mingwX64 variants) —
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
- **`Path()` in commonMain returns different actuals per renderer** — - **`Path()` in commonMain returns different actuals per renderer** —
the Skia renderer produces a `SkiaBackedPath` (wraps the Skia renderer produces a `SkiaBackedPath` (wraps
`org.jetbrains.skia.Path`), the SDL renderer produces a project `org.jetbrains.skia.Path`), the SDL renderer produces a project
+143 -159
View File
@@ -1,196 +1,180 @@
[![Kotlin](https://img.shields.io/badge/Kotlin-2.4.0-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org) [![Kotlin](https://img.shields.io/badge/Kotlin-2.4.0-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
# Compose Desktop Native # Compose Desktop Native
A Kotlin/Native port of **Compose Multiplatform** running on **SDL3** for Run [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform)
windowing and input — **no JVM**. Compiles to native binaries for macOS as a native binary, with no JVM. Compose Desktop Native compiles to a single
(arm64), Linux (x64/arm64), Windows (mingwX64). executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
[SDL3](https://libsdl.org) for windowing and input.
Rendering is pluggable behind one `RenderBackend`: <img src="screenshots/demo.png" width="100%" alt="Compose Desktop Native demo" />
- **Skia** (via Skiko klibs) on macOS + Linux — Metal / OpenGL / CPU raster. ## How it works
- **SDL3** (`SDL3_ttf` + `SDL_RenderGeometry`) on Windows, and on macOS/Linux
when `-Prenderer=sdl3` is passed.
The Compose **runtime** is the official - **The runtime is the real one.** Composition, snapshots, and the recomposer
`org.jetbrains.compose.runtime:runtime` klibs from Maven — this project are the official `org.jetbrains.compose.runtime` klibs from Maven, never
re-implements the layers on top of it (`androidx.compose.ui.*`, reimplemented.
`.foundation.*`, `.animation.*`, `.material3.*`) by **vendoring - **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
upstream Compose Multiplatform verbatim** whenever possible and hand-rolling `animation`, and `material3` are copied from Compose Multiplatform verbatim
project actuals + SDL3 / Skia glue only where needed. wherever they compile as is, with project code filling in only the native glue.
- **Rendering is pluggable** behind one `RenderBackend`: Skia (via Skiko) on
macOS and Linux, and a from-scratch SDL3 renderer (`SDL3_ttf` +
`SDL_RenderGeometry`) on Windows, or anywhere with `-Prenderer=sdl3`.
- **The platform is SDL3.** Windowing, input, audio, filesystem, file dialogs,
and clipboard all go through SDL3, so one code path covers every OS.
- **No runtime dependencies.** SDL3 and its codecs are built as static
libraries and linked in. A distributable is just the executable plus a
`data.kres` resource bundle.
## Module layout | Platform | Gradle target | Default renderer |
|----------|---------------|------------------|
| macOS arm64 | `macosArm64` | Skia (Metal) |
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) |
| Windows | `mingwX64` | SDL3 |
One Gradle module per upstream Compose artifact — the directory tree mirrors ## Quickstart
upstream's `compose/` layout, gradle paths stay short via `projectDir`
redirects.
``` ### A window
compose/
├── ui/
│ ├── ui/ → :ui (androidx.compose.ui.* + renderers + cinterops)
│ ├── ui-util/ → :ui-util
│ ├── ui-geometry/ → :ui-geometry
│ ├── ui-unit/ → :ui-unit
│ └── ui-backhandler/ → :ui-backhandler
├── animation/
│ ├── animation-core/ → :animation-core
│ ├── animation/ → :animation
│ └── animation-graphics/ → :animation-graphics
├── foundation/
│ ├── foundation/ → :foundation
│ └── foundation-layout/ → :foundation-layout
├── material3/material3/ → :material3
├── material/material-ripple/ → :material-ripple
└── sdl/ (project modules — the SDL integration layer)
└── window/ → :window (nativeComposeApp { Window(...) } + SDL3 main loop,
per-window Lifecycle/ViewModelStore/SavedState owners)
utils/
└── material-symbols/ → :material-symbols (codepoints + Outlined/Rounded/Sharp icon fonts)
components/
└── resources/library/ → :components-resources (vendored official compose resources runtime —
painterResource/stringResource/Res codegen; the Maven
artifact ships no mingwX64/linux klibs)
navigation3/
└── navigation3-ui/ → :navigation3-ui (vendored NavDisplay + scene machinery — the one
Navigation 3 layer without a K/N desktop artifact)
demo/ → :demo (showcase + CLI probe suite; multiplatform — the same screens
also build for stock JVM Compose Desktop as a parity reference)
apidemo/ → :apidemo (Postman-style REST client)
```
## demo — widget & feature showcase
<img src="screenshots/demo.png" width="100%" alt="ComposeNativeSDL3 demo" />
`:demo` is a full tour of the re-implemented Compose + Material 3 surface —
30+ sidebar screens covering buttons, text fields, layout, modifiers, shapes,
images, state & recomposition, scrolling & lazy lists, dialogs, icons, canvas,
graphics layers, custom layout, animation and gestures.
```bash
./gradlew :demo:runDebugExecutableMacosArm64 # macOS (Skia / Metal)
./gradlew :demo:runDebugExecutableLinuxX64 # Linux (Skia / OpenGL)
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (SDL3)
./gradlew :demo:run # stock JVM Compose Desktop (parity reference)
```
The same shared screens drive both stacks — the JVM target runs them on
upstream Compose Desktop unchanged, so any visual or behavioural difference
against the native build is a porting bug, not a demo artifact.
CLI: `--gpu=…`, `--screen=<Name>` (one screen, no sidebar),
`--screenshot=out.bmp --frames=N`, `--width=W --height=H`.
## apidemo — API Manager
<img src="screenshots/apidemo.png" width="100%" alt="ComposeNativeSDL3 API Manager" />
`:apidemo` is a Postman-style REST client built entirely on the library:
request collections (**packs**, nested sub-packs, linked copies), a
**Session → Pack → Request** inheritance ladder for variables / headers /
query params / client certs, syntax-highlighted JSON / XML / YAML / HTML body
editors, a response viewer with timing, size and TLS-chain inspection, mTLS
client certificates, drag-and-drop tree management, request history and
file-based sessions.
```bash
./gradlew :apidemo:runDebugExecutableMacosArm64
./gradlew :apidemo:runDebugExecutableLinuxX64
gradlew.bat :apidemo:runDebugExecutableMingwX64
```
Add `-Prenderer=sdl3` on macOS/Linux to drop Skiko and use the pure-SDL3
renderer everywhere.
## Minimal app
```kotlin ```kotlin
import androidx.compose.material3.Text import androidx.compose.material3.Text
import com.compose.sdl.nativeComposeWindow import com.compose.sdl.nativeComposeWindow
fun main() = nativeComposeWindow(title = "Hello") { fun main() = nativeComposeWindow(title = "Hello") {
Text("Hello from ComposeNativeSDL3") Text("Hello from Compose Desktop Native")
} }
``` ```
The lambda runs with a `ComposeWindowScope` receiver exposing The content lambda runs with a `ComposeWindowScope` receiver that exposes
`window: ComposeNativeWindow` (`setTitle` / `setSize` / `minimize` / `window: ComposeNativeWindow` (`setTitle`, `setSize`, `minimize`, `maximize`,
`maximize` / `setFullscreen` / `close` / …); the same handle is reachable `setFullscreen`, `close`). The same handle is reachable from any nested
from any nested composable via `LocalComposeNativeWindow.current`. composable via `LocalComposeNativeWindow.current`.
Add these to your module's `commonMain.dependencies`: For multiple windows, wrap them in `nativeComposeApp`, where the set of
`Window(...)` calls is state driven (Compose Desktop style):
```kotlin ```kotlin
implementation(project(":window")) // window + main loop fun main() = nativeComposeApp {
Window(title = "Main", onCloseRequest = ::exitApplication) { /* ... */ }
if (showInspector) {
Window(title = "Inspector", onCloseRequest = { showInspector = false }) { /* ... */ }
}
}
```
Each window gets its own `Lifecycle`, `ViewModelStore`, and `SavedStateRegistry`
owners, driven by real SDL focus and visibility events, so ViewModels and
saved state behave as they do on Android.
### Building in this repo
```kotlin
commonMain.dependencies {
implementation(project(":window")) // window shell + main loop
implementation(project(":material3")) // Material 3 widgets implementation(project(":material3")) // Material 3 widgets
implementation(project(":material-symbols")) // icon-font composables (optional) implementation(project(":material-symbols")) // icon-font composables (optional)
}
``` ```
### Building from your own project: the bridge plugin
The klibs publish to GitHub Packages under `com.bitsycore.compose.sdl:*`. Apply
the bridge Gradle plugin once, declare the **official** Compose Multiplatform
coordinates, and the plugin swaps in the port's klibs on native desktop targets
while android, jvm, iOS, and wasm keep resolving the official artifacts.
```kotlin
// build.gradle.kts, official coordinates everywhere
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("com.bitsycore.compose-desktop-native.bridge") version "<release>"
}
commonMain.dependencies {
// The plugin exposes the exact Compose versions the port tracks, so you
// never hand-match them (material3 is versioned separately upstream).
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
}
```
Repositories, credentials, and version pinning:
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
For a complete project that applies the bridge and builds one shared UI for
Android, JVM, and native desktop, see the example repo:
[bitsycore/compose-desktop-native-bridge-example](https://github.com/bitsycore/compose-desktop-native-bridge-example).
## Sample apps
Both live in `commonMain` and also build for stock JVM Compose Desktop, which
serves as the visual and behavioural reference: any difference against the
native build is a porting bug.
**`:demo`** is a tour of the re-implemented Compose and Material 3 surface,
30-plus screens covering text, layout, shapes, images, state, lazy lists,
dialogs, canvas, graphics layers, animation, and gestures.
```bash
./gradlew :demo:runDebugExecutableMacosArm64 # macOS (Skia / Metal)
./gradlew :demo:runDebugExecutableLinuxX64 # Linux (Skia / OpenGL)
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (SDL3)
./gradlew :demo:run # JVM Compose Desktop (reference)
```
**`:apidemo`** is a Postman-style REST client built entirely on the library:
request collections, a session inheritance ladder, syntax-highlighted body
editors, a response viewer with timing and TLS-chain inspection, and mTLS
client certificates.
<img src="screenshots/apidemo.png" width="100%" alt="Compose Desktop Native API Manager" />
```bash
./gradlew :apidemo:runDebugExecutableMacosArm64
./gradlew :apidemo:run # JVM Compose Desktop (reference)
```
Pass `-Prenderer=sdl3` on macOS or Linux to drop Skiko and use the pure SDL3
renderer everywhere.
## Modules
One Gradle module per upstream Compose artifact, mirroring the upstream
`compose/` tree. The renderer and cinterops live in `:ui`; `:window` is the SDL
integration layer. `:material-symbols`, `:components-resources`, and
`:navigation3-ui` are project or vendored modules that the official Maven
artifacts do not cover for Kotlin/Native desktop.
Most of the androidx architecture stack (lifecycle, viewmodel, navigation3,
savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on
the port unmodified. The full module map, dependency graph, and the list of
compatible artifacts are in [CLAUDE.md](CLAUDE.md).
## Building ## Building
- **macOS:** `brew install sdl3` (Skia is the default; Skiko klibs come from Build the native libraries once per machine, then build any app target:
Maven).
- **Linux:** `sudo apt install libsdl3-dev`.
- **Windows:** SDL3, SDL3_ttf, SDL3_image and FreeType are built from source
as **static** libraries by `scripts/build-sdl/build-all.sh` into the gitignored
in-repo `libs/`, then linked into the executable — the Windows
distributable is just `<app>.exe` + `data.kres`, no runtime DLLs.
## Vendoring
The bulk of the `androidx.compose.*` code is vendored byte-for-byte from
`JetBrains/compose-multiplatform-core` and lives under
`<module>/src/vendor/` (gitignored — you re-sync on demand). Each module
carries a `compose-fork.txt` manifest listing which upstream files it pulls
in.
```bash ```bash
scripts/compose-fork/sync.sh # sync every module python3 scripts/build-sdl/build-all.py # SDL3, SDL3_ttf, SDL3_image, FreeType (static)
scripts/compose-fork/sync.sh compose/ui/compose-fork.txt # one module ./gradlew :demo:runDebugExecutableMacosArm64
``` ```
Upstream ref pinned in `scripts/compose-fork/compose.properties`; manifests can Every host needs `git`, `cmake`, and Python 3. Per host: macOS needs the Xcode
pin additional upstream repos inline (`SET_REPO=<url>@<ref>` — the resources command line tools; Linux needs gcc/g++ and the X11 / Wayland / audio dev
runtime vendors from the compose-multiplatform umbrella repo this way). headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
the full build and verification workflow.
See [CLAUDE.md](CLAUDE.md) for the full architecture, source-set hierarchy, ## Documentation
vendoring rules, density flow (physical-pixel Option B), and per-area file
map.
## Known Compatible — official artifacts that just work - [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
- [CLAUDE.md](CLAUDE.md): architecture, source-set hierarchy, vendoring rules,
A surprising amount of the androidx architecture stack ships real density flow, and a per-area file map.
Kotlin/Native desktop klibs (mingwX64, linuxX64/arm64, macosArm64) and runs - [gradle-plugin/compose-desktop-native-bridge](gradle-plugin/compose-desktop-native-bridge/README.md):
on this port **unmodified** — no reimplementation, no vendoring. Sometimes consuming the port from another build.
the Google coordinates (`androidx.*`) carry the K/N variant, sometimes the
JetBrains ones (`org.jetbrains.*`) — check both.
Verified working in-tree:
| Artifact | Notes |
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `org.jetbrains.compose.runtime:runtime`, `runtime-saveable` | the Compose runtime itself — never reimplemented |
| `androidx.compose.runtime:runtime-retain` | google coordinates |
| `androidx.lifecycle:lifecycle-runtime-compose` | `LocalLifecycleOwner`, `rememberLifecycleOwner`, `repeatOnLifecycle` |
| `androidx.lifecycle:lifecycle-viewmodel(-compose, -savedstate)` | full ViewModel + SavedStateHandle stack |
| `androidx.lifecycle:lifecycle-viewmodel-navigation3` | per-entry ViewModel scoping for Navigation 3 |
| `androidx.savedstate:savedstate`, `savedstate-compose` | |
| `androidx.navigation3:navigation3-runtime` | backstack / NavEntry / decorators (only the UI layer needed vendoring → `:navigation3-ui`) |
| `androidx.navigationevent:navigationevent-compose` | predictive back / BackHandler plumbing |
| `androidx.collection:collection` | |
The window layer supplies what these expect from a host: per-window
`LifecycleOwner` (driven by real SDL focus/visibility events — focused →
RESUMED, unfocused → STARTED, minimized → CREATED), `ViewModelStoreOwner`
(the `activityViewModels()` analog, `SavedStateHandle`-enabled) and
`SavedStateRegistryOwner`, plus a `Dispatchers.Main` with a true `immediate`.
ViewModels scope per nav entry, per window, or anywhere in between — same
semantics as Android.
## License ## License
+263
View File
@@ -0,0 +1,263 @@
# RENDERER.md
The durable reference for the rendering layer: how it is structured, the
convergence work that shaped it, the decisions worth remembering, and what is
left. Read this before touching anything under `renderer/`, the graphics
actuals, or the layer engine.
For build and verification commands see [TOOLING.md](TOOLING.md). For module
layout, source-set hierarchy, and vendoring rules see [CLAUDE.md](CLAUDE.md).
## 1. The two renderer legs
Rendering is pluggable behind one `RenderBackend` interface. Exactly one leg is
active per target:
- **Skia leg** (`skikoRendererMain`, macOS + Linux). Draws through upstream
Compose Multiplatform's own Skia stack, vendored verbatim: `SkiaBackedCanvas`,
`SkiaBackedPaint`, `SkiaShader`, `actual class GraphicsLayer` backed by
`org.jetbrains.skiko.node.RenderNode`, `SkiaGraphicsContext`. On this leg the
layer and draw engine internals are literally upstream.
- **SDL leg** (`sdlRendererMain`, Windows always; macOS/Linux under
`-Prenderer=sdl3`). A from-scratch renderer on `SDL_RenderGeometry` +
`SDL3_ttf` + FreeType. SDL is a triangle blitter, so this leg carries a
permanent bespoke surface (about 3,800 lines): `Sdl3Canvas`,
`Sdl3TextRenderer`, `FreeTypeIcons`, and the SDL render nodes.
Both legs share one project-owned text and image pipeline (`SdlParagraph`,
`NativeTextMeasurer`, `IconFont`). Text layout, line-breaking, metrics, and
hit-test are the port's engine on both legs; only glyph rasterization differs
(Skia vs SDL3_ttf/FreeType). This is a deliberate axiom, not a gap: see the
text decision in section 4.
The seam is kept as narrow and low as possible. Code flows
`Common (upstream) -> shared native engine -> Skia actual / SDL actual`. What we
minimize is hand-rolled actual surface, not actual-side line count: a fat
vendored-upstream actual (the Skia leg carrying upstream's whole GraphicsLayer
stack) is preferred over a thin hand-written one.
## 2. The retained-layer engine (the model we copied)
Upstream skiko skips work at three levels. The port has all three.
- **L1 frame scheduling.** A frame schedules the next only if still dirty.
Ours: `ComposeWindow.shouldRender()` gates `renderFrame()`; the loop blocks on
`SDL_WaitEventTimeout` when idle.
- **L2 measure/layout only dirty nodes.** Vendored `MeasureAndLayoutDelegate`
used verbatim through `ComposeOwner.measureAndLayout()`.
- **L3 draw record-once / replay** (the expensive one). Each isolated
`LayoutNode` gets an `OwnedLayer` (`GraphicsLayerOwnerLayer`) owning a
`GraphicsLayer`. The display list re-records only when content is dirty;
otherwise it replays.
The critical property: **transform, alpha, and clip changes do NOT re-record.**
Moving, scaling, rotating, or fading a layer replays cached content under a new
transform. Only a genuine content change (a state read inside the draw block, or
a resize) re-records. Upstream replays the whole scene every frame with no
dirty-region present; the entire win is not re-recording clean layers, not
drawing less screen. Dirty-region rendering is therefore an explicit non-goal.
The compositing-strategy contract (`requiresLayer()`, from
`SkiaGraphicsLayer.skiko.kt`) must match on both legs. It decides when a layer
needs an offscreen:
| Condition | Auto | Offscreen | ModulateAlpha |
|---|---|---|---|
| `alpha < 1` | offscreen | offscreen | per-op alpha multiply (no offscreen) |
| `colorFilter != null` | offscreen | offscreen | offscreen |
| `blendMode != SrcOver` | offscreen | offscreen | offscreen |
| `renderEffect != null` | offscreen | offscreen | offscreen |
| none of the above | replay in place | always offscreen | replay in place |
## 3. Current state
- **Skia leg runs upstream's engine.** Canvas (`SkiaBackedCanvas`) and
GraphicsLayer/GraphicsContext (`org.jetbrains.skiko.node.RenderNode`) are
vendored verbatim. The Skia leg gets real display-list caching and correct
clip/shadow/renderEffect for free from upstream. After this landed, Skia
`draw` on LazyColumn fell from 1.75 ms to 0.2 ms.
- **SDL leg ships the geo node as the Windows default.** `SdlDisplayListRenderNode`
records a leaf's layer-local tessellated geometry, text runs, and icon glyphs
by params (not texture pointers), then re-emits through the layer transform.
Crisp under any transform, bit-exact, deterministic (no render-target state),
and eviction-safe (replay re-looks-up via the run LRU / FreeType glyph cache).
0.000% across a 57-screen self-consistency sweep bar a sub-0.06% rotated-edge
AA fringe. LazyColumn steady-state `draw` fell 57% versus the un-cached
baseline. Fallbacks: `CDN_LAYERCACHE=off|texture`.
- **Parity is a golden-master on the Skia leg** (about 2% median, dominated by
the shared text engine's small metric delta) and a bounded, ranked signal on
the SDL leg. See TOOLING.md for how to read it.
- **Memory is stable.** The historical composition leak is fixed (see section 6)
and guarded by the `--soaktest` gate in the verify runbook.
## 4. Decisions to remember
- **Goal is G1, cheap upstream-tracking.** The target is low per-bump
reconciliation cost when following upstream, not Windows pixel-parity or
feature completeness. The JVM Compose Desktop target (`:demo:run`) is the
documented Windows fidelity/feature tier: real Skia, full fidelity, on any
host. That makes SDL-leg fidelity work (B3) and real-Skia-on-Windows (Track A)
optional polish, not obligations.
- **Vendor, do not hand-roll.** Copy upstream verbatim wherever it compiles.
Edit-to-compile becomes a manual vendor with a `// VENDOR-BASE:` header so the
drift tripwire can track it. The litmus test for any divergence: "Is this what
upstream does? If not, what real platform constraint forces the difference?"
Valid answers name a constraint (no Skiko on Windows K/N; SDL is a triangle
blitter). "It was easier" is not valid.
- **B2 convergence is complete.** The Skia leg was migrated off the port's
hand-rolled GraphicsLayer onto upstream's own. This was a source-set migration,
not a file-flip: the SDL node cluster relocated `nativeMain -> sdlRendererMain`,
`GraphicsContext` forked per-leg behind a `createGraphicsContext()` seam, and
the upstream skiko files were un-refused in `compose-fork.txt`. Measured
sync-tax reduction on the skiko path: about 8.5x (roughly 1240 hand-rolled
lines down to about 145 lines of drift-tracked edits; roughly 1006 lines now
auto-sync verbatim on a ref bump). The beta02 -> beta03 bump proved it: zero
reconciliation, all manual-vendor bases unchanged.
- **Text stays the port's engine (B6.3 skipped).** Making Skia-leg text truly
upstream (`SkiaParagraph`) is a font-subsystem replacement, not a canvas swap:
two coexisting font-identity models (the port's name-to-bytes `IconFont` +
no-op resolver vs upstream `PlatformFont`/`FontCache`/`FontCollection`), about
13 skiko files to re-vendor, about 10 shared actuals to split, a
`data.kres`-to-`FontCache` bridge, no green intermediate, and it discards the
P3.1 metrics work. ROI is low: the Skia leg already measures text with skiko
`Font` metrics (via `currentTextMeasurer` -> `SkiaTextRenderer`), which is how
parity reached about 2%. Revisit only if complex-script/bidi fidelity on the
native Skia leg becomes a hard requirement.
- **`GraphicsLayerOwnerLayer` stays shared.** The fork point is `GraphicsLayer` /
`GraphicsContext`, not the owner layer. The shared owner layer compiles
against both `GraphicsLayer` actuals (upstream skiko + port SDL) only if both
satisfy the commonMain `expect class GraphicsLayer` API. That API-parity
invariant is enforced by `:ui:compileCommonMainKotlinMetadata` on every build.
- **Lifetime model.** The SDL leg uses GC / release-queue for layer lifetime and
does NOT vendor `ChildLayerDependenciesTracker`. The Skia leg uses upstream's
`SkiaGraphicsLayer`, which inherently uses the tracker (already vendored in
commonMain). A per-leg lifetime divergence is the class of bug that caused an
early navigation crash, so any change here goes to both legs plus a soak gate.
- **Module split (`:ui-graphics` / `:ui-text`) is shelved as infeasible.** Not
cosmetic churn as originally scoped: the `sdl3` cinterop is a shared substrate
used by 11 graphics-side files AND 12 platform/windowing/node/resources files
that stay in `:ui`. Kotlin/Native cannot cleanly share a cinterop klib across a
module boundary, which is exactly why they were merged. A real split needs a
new non-upstream `:ui-cinterop` base module plus cross-module cinterop
api-exposure, which is bigger and more divergent than the "match upstream
artifacts" goal it was meant to serve. The one keeper from the attempt:
`RenderBackend.drawRoot` now takes `(Canvas) -> Unit`, decoupling the backends
from `ComposeRootHost` (a genuine improvement, retained).
- **Track A (real Skia on Windows K/N) is shelved.** Kotlin/Native mingwX64 is
GNU-ABI; skia-pack Windows is MSVC-ABI, which cannot be statically fused into a
GNU-ABI binary. Building current C++20 Skia to a GNU/mingw static archive is an
open-ended fork (only abandoned Mozilla-era precedent). The only working route
is a runtime DLL, which breaks the no-DLL invariant and adds tens of MB on the
one platform it targets. The SDL leg is the permanent Windows renderer; JVM is
the fidelity escape hatch.
## 5. Convergence status
| Item | Status |
|------|--------|
| Guardrails (parity gate, verify-mac runbook, drift + vendor-clean checks) | Done |
| B2: Skia leg on upstream GraphicsLayer/GraphicsContext | Done |
| B6.1: Skia leg on upstream `SkiaBackedCanvas` + paint/shader | Done |
| B6.2: upstream `GraphicsLayer` + delete transient port cluster | Done |
| B6.3: upstream text (`SkiaParagraph`) on the Skia leg | Skipped (decision) |
| B5: engine-convergence deltas audit | Done (clean wins spent by B6) |
| P2.2: composition memory leak | Fixed + soak-gated |
| P2.3: outsets / blur / renderEffect | Done via upstream `SkiaGraphicsLayer` |
| P3.1: SDL text metrics parity (17% median -> 2%) | Done |
| B3: further SDL fidelity | Capped (parity-ranked wins only) |
| Module split | Shelved (infeasible as specified) |
| Track A: real Skia on Windows | Shelved |
## 6. Remaining and future work
Nothing here blocks day-to-day work. These are the open threads worth
remembering.
- **WIN-SMOKE (pre-ship, Windows only).** The Mac runbook covers both renderers
but cannot cover the shipped mingwX64 binary, the Windows-only `PrintWindow`
probe, or the common-metadata publish job. Run these on a Windows host before
any release. This is the only outstanding verification.
- **Stabilization at Compose 1.12.0 stable.** The vendored refs are pinned to
`v1.12.0-beta03+dev4483` (no clean beta03 tag exists yet, and it is not on
Maven). The native side leads the JVM parity leg (forced to beta02, the latest
published) by a documented skew. When 1.12.0 stable ships: re-pin both refs to
the clean tag, bump the `vComposeJvmVersion` forcing in demo/apidemo/
material-symbols to close the skew, then cut the release. The concrete
step-by-step (the version map, the ref-bump flow, and the release flow) lives
in [TOOLING.md](TOOLING.md#versioning-and-releasing).
- **Deferred engine deltas (D2-D6, all low ROI under G1).**
- D2: split `GraphicsLayerOwnerLayer` per-leg to restore upstream's exact
`setLightingInfo` shadow lighting. Feasible (the old RootNodeOwner-coupling
blocker was outdated) but high blast radius for a marginal gain; elevation
shadows already match JVM.
- D3: dedupe `LayerTransformationMatrix.kt` against the now-vendored
`Matrices.skiko`. Blocked on D2 (the shared owner-layer hit-test needs a
both-legs matrix fn).
- D4/D5: `SemanticsRegion` intersect/difference are stubs, and `CharHelpers`
is naive grapheme/bidi vs upstream ICU. Real fidelity gaps, gated on an
accessibility or complex-script roadmap.
- D6: `Focusability` / `PlatformVelocityTracker` are byte-equal to upstream;
vendoring them saves nothing.
- **SDL performance (opportunistic).** Shared glyph atlas to replace per-run
textures; `drawImageRect` capture in the geo node (journal rates it low-ROI).
- **SDL_GPU backend (long-term).** Real stencil clipping, pipelined batching,
shader gradients. The `NativeRenderNode` seam makes a GPU node a clean
drop-in.
- **Native-resource lifecycle.** Wire `GraphicsLayer`/RenderNode +
`SdlImageBitmap.close()` fully into cache eviction and the renderer
`destroy()` chain; demote the periodic GC nudge once ownership covers it.
## 7. Hard-won learnings (do not relearn)
- **Minimize-divergence is load-bearing.** A hand-written `notifyLayerIsDirty`
that diverged from upstream `OwnedLayerManagerImpl` removed a layer from
`dirtyLayers` mid-loop and crashed on any navigation. Screenshots missed it;
the interaction probe caught it. Match upstream verbatim; do not hand-roll
engine plumbing from a summary.
- **The composition memory leak (root cause).** The port never called
`OwnerSnapshotObserver.clearInvalidObservations()`, which upstream
`RootNodeOwner` runs after every measure. Snapshot read-observations for
scopes invalidated on dispose lingered forever, each pinning its observed
object graph via a Kotlin/Native `ExternalRCRef` (a K/N-heap leak, vmmap
Memory Tag 246). Most visible on ripple/indication draws. Fix:
`ComposeRootHost.measureAndLayout()` sweeps `clearInvalidObservations()` after
layout, plus `ComposeOwner.onDetach` clears the detached node's observations.
Found by exact live-counters, static-mode isolation, macOS `leaks`/`heap`/
`vmmap`, and component bisection. RSS alone cannot pinpoint a referenced leak;
budget heap tooling for this class.
- **Offscreen-texture caching is timing-nondeterministic on complex screens.**
This is why the geo (no-render-target) node is the robust default, not the
texture node.
- **Rounded/path layer clips must be applied or deferred on the fast path.** The
geo path submits raw geometry, which clips only to a rect; rounded clips are a
lazy offscreen mask the fast replay bypasses. Rect clip on the fast path;
rounded/generic clips fall back to block-replay.
- **Screenshots miss crashes and settle-timing.** Free-running screenshots on
animated/settling screens give false signals. Use render-to-quiescence +
virtual frame time for parity, the probe for interaction/crash coverage.
- **The profiler `present` phase is vsync-capped by the display refresh.**
Profile on the target monitor before concluding a frame-rate gap.
## 8. Explicit non-goals
- **Dirty-region / partial present.** Upstream replays the whole scene; the win
is not re-recording, not redrawing less.
- **A custom `cacheKey` API.** Superseded by the real per-node display list.
- **Vendoring `RootNodeOwner` / the `ComposeScene` stack.** Coupled to skiko's
`SkiaLayer` and windowing; it fights the `:window` SDL loop. The port borrows
only the layer engine.
## 9. Key files
- `compose/ui/ui/src/nativeMain/.../RenderBackend.kt`: the interface.
- `compose/ui/ui/src/nativeMain/.../GpuMode.kt`: renderer / driver picker.
- `compose/ui/ui/src/skikoRendererMain/.../renderer/skia/SkiaRenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3RenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/FreeTypeIcons.kt`:
variable-font axis rasterization.
- `compose/ui/ui/src/commonMain/.../node/ComposeRootHost.kt`: root host,
hit-test, event dispatch, snapshot observer sweep.
- `compose/ui/ui/src/commonMain/.../node/impl/ComposeOwner.kt`: the project
`Owner` + `GraphicsLayerOwnerLayer` bridge.
- `compose/ui/ui/src/nativeMain/.../ui/text/SdlParagraph.native.kt`: the bridged
`Paragraph` (measurement, hit-test, line metrics, span painting).
+160
View File
@@ -0,0 +1,160 @@
# TODO.md
Everything the port currently leaves as a no-op, stub, hardcode, or partial
implementation, plus the gaps worth closing before a stable 1.12 release. This
is an audit of PROJECT code only (the `*.native.kt` / `*.sdl.kt` /
`com.compose.sdl.*` actuals); vendored upstream under `src/vendor/` is out of
scope.
Severity is a rough guide, not a mandate:
- **Blocker** for stable: a capability a typical desktop app expects that is
currently absent or broken.
- **Nice-to-have**: real gap, but the app works without it.
- **Cosmetic**: minor fidelity or edge-case behavior.
Context for prioritizing: the project goal is **G1, cheap upstream-tracking**
(see [RENDERER.md](RENDERER.md)). The **JVM Compose Desktop target is the
documented Windows fidelity/feature tier**, so SDL-leg visual gaps (section J)
are lower priority than the cross-cutting platform gaps (input, locale, a11y,
cursor) that affect every backend including Skia on macOS/Linux.
Renderer-internal cleanups (the deferred D2 shadow-lighting split, D3 matrix
dedupe) live in [RENDERER.md](RENDERER.md#6-remaining-and-future-work), not
here.
---
## Release-blocking shortlist for stable 1.12
The cross-cutting items to weigh first (all detailed below):
1. **Locale is hardcoded to `en-US`** (section E). One fix unlocks the 40+
vendored Material 3 translations. Cheapest high-value win in this file.
2. **No IME / text composition** (section B). Only committed Latin text works;
CJK, dead keys, and accented input do not.
3. **No cursor changes** (section D). The OS cursor never becomes an I-beam over
text or a hand over clickables.
4. **No copy/paste context menu** (section C). Keyboard copy works; there is no
right-click or selection toolbar.
5. **Accessibility is entirely absent** (section A). Decide whether stable
requires any screen-reader support at all.
6. **Standard `Font(...)` / resource-font loading is a no-op** (section F). Only
pre-registered named fonts render. A blocker only if apps use the standard
font APIs.
---
## A. Accessibility (entirely absent)
No accessibility bridge exists. A real `SemanticsOwner` is built but never
surfaced to any OS accessibility API (no NSAccessibility / UIA / AT-SPI, no SDL
a11y). Screen readers get nothing.
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:340` · `semanticsOwner` is instantiated but never read; no traversal into a platform a11y tree. A real a11y controller would walk the semantics tree and map roles/labels/actions to a native AT bridge. **Blocker for a11y.**
- `compose/ui/ui/src/nativeMain/.../platform/CompositionLocals.native.kt:31` · `LocalPlatformScreenReader` default throws (`error(...)`); never provided by any window. Screen-reader-active queries are unavailable. **Blocker for a11y.**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:297` · `accessibilityManager` implements only `calculateRecommendedTimeoutMillis` (passthrough); no announce/focus/event surface. **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../semantics/SemanticsRegion.native.kt:14` · `StubSemanticsRegion.intersect()`/`difference()` hardcode `false`; only `set`/`bounds` work. Region math for semantics culling is inert. **Nice-to-have.**
## B. Text input / IME
Committed text, arrow/backspace/enter editing, and mouse-drag selection work.
Composition (preedit) does not.
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:407` · `textInputSession` awaits cancellation forever (`awaitCancellation()` at :414); no real `PlatformTextInputSession`. The modern `PlatformTextInputModifierNode` pipeline (BasicTextField2) has no IME start/stop, composition, or cursor-rect reporting. **Blocker.**
- `compose/ui/ui/src/nativeMain/.../SDL3EventMapper.kt:126` · maps `SDL_EVENT_TEXT_INPUT` (committed) only; `SDL_EVENT_TEXT_EDITING` (preedit/composition) is never mapped, so there is no underlined composing region for dead keys / CJK. **Blocker for CJK and dead-key input.**
- `compose/ui/ui/src/nativeMain/.../SDL3Backend.kt:111` · `SDL_StartTextInput` is called once at window init and never scoped to the focused field; `SDL_SetTextInputArea` is never called, so the IME candidate window renders at the wrong location. **Nice-to-have.**
- `compose/ui/ui/src/commonMain/.../text/input/NoOpPlatformTextInputService.kt:24,32` · legacy `PlatformTextInputService` `startInput`/`stopInput`/`updateState` + software-keyboard toggles are all NOP. Superseded by the modern path above. **Nice-to-have / cosmetic (desktop).**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:328` · `softwareKeyboardController.show/hide` NOP. **Cosmetic (desktop).**
## C. Text toolbar and context menus (copy / paste / select-all)
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:311` · `textToolbar` is a stub: `showMenu`/`hide` NOP, `status` always `Hidden`. No selection or right-click copy/paste popup anywhere. Keyboard copy still works. **Blocker for text UX.**
- `compose/foundation/.../text/selection/SelectionActuals.native.kt:42` · `addSelectionContainerTextContextMenuComponents` NOP (upstream TODO CMP-7819). No copy/select-all menu for `SelectionContainer`. **Nice-to-have.**
- `compose/foundation/.../text/selection/TextFieldSelectionManager.native.kt:24` and `compose/foundation/.../text/input/internal/selection/TextFieldSelectionState.native.kt:36` · `addBasicTextFieldTextContextMenuComponents` NOP for both the legacy and state-based `BasicTextField`. **Nice-to-have.**
## D. Pointer / cursor icons
The OS cursor never changes. No SDL cursor API is called anywhere in the port.
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:333` · `pointerIconService.getIcon()` always returns `Default`; `setIcon()` body is empty. This is the hook `Modifier.pointerHoverIcon` drives, so hover cursor changes are swallowed. Should store the requested icon and call `SDL_SetCursor`. **Blocker for UX.**
- `compose/ui/ui/src/nativeMain/.../input/pointer/PointerIcon.native.kt:18` · `pointerIconDefault|Crosshair|Text|Hand` are inert marker objects carrying only a name; nothing maps them to `SDL_SYSTEM_CURSOR_*`. Needs `SDL_CreateSystemCursor` + `SDL_SetCursor` (and cursor caching in the `:window` loop). **Blocker (paired with above).**
## E. Locale and internationalization
System-locale plumbing already exists in exactly one place:
`components/resources/.../ResourceEnvironment.native.kt:38` reads
`SDL_GetPreferredLocales()` for resource-qualifier resolution. Every other i18n
consumer ignores it and hardcodes English. Wiring `Locale.current` to that same
call is the flagship cheap win.
- `compose/ui/ui/src/nativeMain/.../text/intl/Locale.native.kt:57` · `DefaultLocale` is a fixed `Locale("en-US")`, so `Locale.current` can never change. This one value keys Material 3 string translation selection, stranding the 40+ vendored `material3/internal/l10n/*.kt` tables (they are present and reachable by tag, but the key is always en-US). Fix by querying `SDL_GetPreferredLocales()` (already bound in the `sdl3` cinterop). **Blocker; fixing it transitively localizes all of Material 3.**
- `compose/ui/ui/src/nativeMain/.../text/intl/PlatformLocale.native.kt:18` · `createPlatformLocaleDelegate().current` hardcodes `LocaleList(listOf(Locale("en-US")))`; should reflect the full ordered preferred-locale list SDL returns (the resources module currently reads only `[0]`). **Blocker.**
- `compose/material3/.../CalendarLocale.native.kt:20` · locale-agnostic stub: `toString()` fixed `"en"`, single shared instance, `equals` treats all instances as equal. A separate type from `ui.text.intl.Locale`, so it needs its own wiring so `CalendarModel`/`PlatformDateFormat` can vary by locale. **Blocker (DatePicker/TimePicker).**
- `compose/material3/.../internal/PlatformDateFormat.native.kt` · multiple hardcodes:
- `:27` `weekdayNames` hardcoded English full/short names (DatePicker shows English day initials regardless of locale). **Blocker.**
- `:32` `formatWithPattern`/`formatWithSkeleton` ignore the pattern/skeleton and always emit ISO `yyyy-MM-dd`. **Blocker.**
- `:25` `firstDayOfWeek` hardcoded `1` (Sunday); locale-dependent. **Nice-to-have.**
- `:44` `parse()` only accepts ISO `yyyy-MM-dd`, ignoring pattern/locale. **Nice-to-have.**
- `:62` `getDateInputFormat()` hardcoded `yyyy-MM-dd` / `'-'`. **Nice-to-have.**
- `:65` `is24HourFormat()` hardcoded `true`. **Cosmetic.**
- `compose/ui/ui/src/nativeMain/.../text/platform/NativeStringDelegate.native.kt:17` · `toUpperCase`/`toLowerCase` use locale-independent stdlib casing; the `locale` param is ignored (wrong for Turkish dotted/dotless i, etc.). **Cosmetic.**
- `compose/ui/ui/src/nativeMain/.../text/intl/Locale.native.kt:17` · `Locale.isRtl()` not implemented; the pipeline is LTR-only (`SdlParagraph.native.kt:333` `getBidiRunDirection` always returns `Ltr`; owners/DrawScopes default `LayoutDirection.Ltr`). No RTL shaping. **Nice-to-have.**
- No `NumberFormat` / currency / decimal-grouping actual exists; numbers render via `toString()` with `.` decimal and no grouping regardless of locale. **Cosmetic.**
**Beyond a real `getLocale`:** SDL reports language + country only (no script subtag, no calendar/clock/number preferences), and Kotlin/Native bundles no ICU. Localized month/weekday name tables, date/number/currency patterns, first-day-of-week, locale-aware casing, and RTL all still require a bundled CLDR subset or a third-party K/N i18n library. The SDL wiring is the cheap 80%; these are the expensive tail.
## F. Font resolution
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:32` · `SdlPlatformFontLoader.loadBlocking`/`awaitLoad` are NOP (return `Unit`); font bytes for `ResourceFont` / `Font(bytes)` / `LoadedFontFamily` are never loaded. Only pre-registered `NamedFont` names reach the renderer. **Blocker if the app uses standard `Font(...)` / resource-font APIs; otherwise nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:38` · `PlatformFontFamilyTypefaceAdapter.resolve` returns `TypefaceResult.Immutable(Unit)`; any code reading the resolved typeface gets `Unit` (the renderer bypasses this via name lookup, so usually harmless). **Nice-to-have.**
- `compose/ui/ui/src/commonMain/.../text/NamedFont.kt:62` · `FontFamily.projectFontName` resolves only a `FontListFontFamily` whose first font is a `NamedFont`; returns `null` for `Serif`/`SansSerif`/`Monospace`/`Cursive`/`Default`, `LoadedFontFamily`, and resource-backed families, so all of those collapse to the single default font (no serif/mono/cursive distinction). **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:69` · deprecated `createFontFamilyResolver(fontResourceLoader)` ignores the supplied loader. **Cosmetic.**
## G. Text rendering details
- `compose/ui/ui/src/nativeMain/.../text/SdlParagraph.native.kt:376` · `drawNativeText` accepts and ignores text `Shadow`, stroke `DrawStyle`, and non-`SrcOver` blend, so those never render on text. **Cosmetic.**
- `compose/foundation/.../text/StringHelpers.native.kt:31` and `compose/ui/ui/src/nativeMain/.../text/CharHelpers.native.kt:14` · `findPrecedingBreak`/`findFollowingBreak` (and `offsetByCodePoints`) do a codepoint/surrogate walk only, with no ICU grapheme clusters, so caret movement and backspace split combining marks and emoji ZWJ sequences. **Nice-to-have.**
## H. Drag and drop
Drop INTO the window (files + text) is fully wired and works. Drag OUT does not.
- `compose/ui/ui/src/nativeMain/.../draganddrop/Sdl3DragAndDropOwner.kt:38` · `requestDragAndDropTransfer` is a NOP and `isRequestDragAndDropTransferRequired` returns `false`; `Modifier.dragAndDropSource` compiles but no OS-level drag leaves the window. Needs per-OS native work (NSDraggingSession / DoDragDrop / XDND); SDL3 has no portable start-drag. **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../draganddrop/Sdl3DragAndDropOwner.kt:119` · incoming transfer carries only file paths + a text blob; no image/URL/custom-MIME payloads. **Cosmetic.**
- `compose/foundation/.../draganddrop/DragAndDropSource.native.kt:36` · no drag-shadow / drag-image feedback for internal DnD (upstream skiko renders one). **Cosmetic.**
## I. Window and platform info
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:354` · `windowInfo.containerSize`/`containerDpSize` return `IntSize.Zero`/`DpSize.Zero`; `LocalWindowInfo.containerSize` readers (popup sizing, window-size-aware layout) get 0. **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:353` · `windowInfo.isWindowFocused` hardcoded `true`; not fed by SDL focus events (which the window already tracks). Focus-reactive UI will not react. **Nice-to-have.**
- Multi-monitor: no display enumeration or per-monitor placement anywhere (`SDL_GetDisplays`/`SDL_GetDisplayBounds`/`SDL_GetDisplayForWindow` unused). **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../platform/PrefetchLocals.native.kt:18` · `LocalPlatformPrefetchScheduler` default is a NOP scheduler; lazy lists skip ahead-of-time item composition (correctness fine, possible scroll-in jank). **Nice-to-have (perf).**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:324` · `autofill`/`autofillManager` are null; `requestAutofill` NOP. **Nice-to-have.**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:279` · `hapticFeedBack.performHapticFeedback` NOP (no desktop pointer haptics API). **Cosmetic.**
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:287` · deprecated `clipboardManager` `setText`/`getText` NOP, and `clipboard.getClipEntry` null. Harmless: the real `LocalClipboard` (text + PNG image via SDL3) works. **Cosmetic.**
- `compose/foundation/.../text/input/internal/selection/TextFieldSelectionState.native.kt:44` · `ClipboardPasteState.hasClip` aliased to `hasText`, so image-only clipboard is not detected for the paste affordance. **Cosmetic.**
## J. SDL renderer graphics (Windows / `-Prenderer=sdl3` only)
These affect only the SDL leg. On macOS/Linux the Skia leg implements them
correctly, and JVM is the documented Windows fidelity tier, so these are lower
priority under G1. Listed because SDL is the shipped Windows renderer.
- `compose/ui/ui/src/sdlRendererMain/.../graphics/PathEffect.sdl.kt:15` · `dash`/`corner`/`chain`/`stamped` path effects are all NOP; dashed/dotted strokes render solid. **Nice-to-have.**
- `compose/ui/ui/src/sdlRendererMain/.../graphics/RenderEffect.sdl.kt:16` · `RenderEffect.isSupported()` is `false`; `Modifier.blur()` and `graphicsLayer{renderEffect=...}` do nothing. `compose/ui/ui/src/sdlRendererMain/.../graphics/shadow/Blur.native.kt:33` · `Paint.setBlurFilter` NOP. **Nice-to-have (Windows visual parity).**
- Gradients render real ramps, but `TileMode` is dropped: `t` is clamped, so `Repeated`/`Mirror`/`Decal` all behave as `Clamp` (`Sdl3DrawScope.kt:1070` samplers; `CanvasPaintActuals.native.kt:66`). **Nice-to-have.**
- `compose/ui/ui/src/sdlRendererMain/.../graphics/CanvasPaintActuals.native.kt:106` · `ActualImageShader`/`ActualCompositeShader` are stubs; image/composite `ShaderBrush` degrades to solid fill (white/black). **Nice-to-have.**
- `compose/ui/ui/src/sdlRendererMain/.../graphics/ColorFilter.sdl.kt:13` · tint/color-matrix/lighting color filters are empty stubs; only `BlendModeColorFilter` tint on image blits is honored (`Sdl3Canvas.kt:1251`). ColorMatrix (grayscale/saturation) and all filters on shapes are inert. **Nice-to-have.**
- BlendMode: only `SrcOver` (plus a `Clear` special case) composites; `Multiply`/`Screen`/`Plus`/etc. are ignored (`Sdl3DrawScope.kt:206`, `Sdl3Canvas.kt:926`). Note `BlendMode.isSupported()` misleadingly returns `true`. **Nice-to-have.**
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:436` · `saveLayer` has no offscreen buffer; layer alpha is multiplied into each primitive, so overlapping content double-composites (wrong group opacity) and the layer paint's colorFilter/blendMode/renderEffect are dropped. **Nice-to-have (blocker for correct group-alpha over overlapping content).**
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:509` · `clipPath` degrades an arbitrary path to its bounding box; rotated/sheared `clipRect` collapses to an AABB. Non-rect clips leak. (Rounded/difference clips are real, with feathered AA.) **Nice-to-have.**
- Stroke joins (`StrokeJoin`) are never applied, so polyline corners notch; `StrokeCap.Square` falls back to butt (`Sdl3DrawScope.kt:380,342`). **Nice-to-have / cosmetic.**
- `PathFillType` (NonZero vs EvenOdd) is mostly ignored beyond a 2-contour border case; interior holes and self-intersections are not cut out, and concave fills self-overlap (`Sdl3DrawScope.kt:366,717`). **Nice-to-have.**
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:1276` · `drawPoints`/`drawRawPoints`/`drawVertices` are NOP; point-mode drawing and custom vertex meshes render nothing. **Nice-to-have.**
- Images under a rotated layer are not rotated (`SDL_RenderTexture` is axis-aligned, `Sdl3Canvas.kt:1264`); stroked non-square ovals stroke as a circular ring (`Sdl3DrawScope.kt:527`); `SdlImageBitmap.readPixels` is a no-op (`Sdl3Offscreen.kt:117`). **Cosmetic / nice-to-have.**
- Drop shadow is approximated (9-slice / stacked rings, not a gaussian blur; ambient vs spot largely collapsed) at `Sdl3Canvas.kt:785`; AA is a ~1px geometry fringe rather than analytic coverage. **Cosmetic.**
## K. GraphicsLayer / image
- `compose/ui/ui/src/sdlRendererMain/.../graphics/CanvasPaintActuals.native.kt:151` · `createImageBitmap(bytes)` throws `UnsupportedOperationException`. (`GraphicsLayer.toImageBitmap` and `snapshotBgra` do work on both renderers.) **Nice-to-have.**
+202
View File
@@ -0,0 +1,202 @@
# Tooling
Helper scripts and workflows used to build, vendor, and verify Compose Desktop
Native. Each has a focused job; the deeper ones link to their own README.
For architecture, source-set layout, and vendoring rules, see
[CLAUDE.md](CLAUDE.md).
## Native libraries
SDL3, SDL3_ttf, SDL3_image, and FreeType are built from source as static
libraries and linked straight into the executable. One script does it on every
OS. Output lands in the gitignored `libs/`; versions are pinned in
`scripts/build-sdl/build-sdl.properties`.
```bash
python3 scripts/build-sdl/build-all.py # freetype, sdl3, sdl3-image, sdl3-ttf
python3 scripts/build-sdl/build-all.py sdl3-ttf # rebuild one step
```
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
absent). Run it once per machine, or after bumping the pinned versions.
## Vendoring upstream Compose
Most `androidx.compose.*` code is copied byte for byte from
`JetBrains/compose-multiplatform-core` into each module's gitignored
`src/vendor/`. A `compose-fork.txt` manifest per module lists which upstream
files it pulls; the pinned ref lives in `scripts/compose-fork/compose.properties`.
```bash
scripts/compose-fork/sync.sh # re-sync every module
scripts/compose-fork/sync.sh compose/ui/ui/compose-fork.txt # one module
```
Reach for this after a fresh checkout or a ref bump. Details:
[scripts/compose-fork/README.md](scripts/compose-fork/README.md).
Guardrails that keep the vendor tree honest:
```bash
python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs
python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin
```
`check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`.
`check-vendor-drift` reads the `// VENDOR-BASE:` header on every edited copy and,
using the local upstream clone, reports whether the base actually changed since
the pin (needs reconciling) or is merely stale (safe to re-stamp). Run both on
every ref bump.
### Coverage against upstream
How much of upstream's public API the port covers is measured, not guessed.
```bash
./gradlew apiDump && python3 scripts/compose-coverage.py # per-module coverage tables
python3 scripts/compose-coverage.py --missing ui-text # list uncovered declarations
```
## Verification
### One-command gate
`verify-mac.sh` is the runbook to run after any renderer or layout change. On
macOS or Linux it exercises BOTH renderer legs (Skia by default, then
`-Prenderer=sdl3`) and exits non-zero on any failure:
```bash
scripts/verify-mac.sh
```
It runs, per leg: the vendor drift and clean checks, a build of `:demo` and
`:apidemo`, the interaction probes, the parity sweep, a memory soak, and a
frame-time spot check. The Windows target is verified separately (see below).
### Parity: native vs JVM
`:demo` renders the same shared screens on two stacks: this port, and stock JVM
Compose Desktop. `parity.py` screenshots every screen on both and pixel-diffs
them, so a screen that diverges is a porting bug. It gates against per-screen
golden baselines.
```bash
python3 scripts/parity/parity.py # all screens
python3 scripts/parity/parity.py Buttons Shapes # a subset
python3 scripts/parity/parity.py --no-build # reuse the last renders
```
How to read the diff and the percentage: [scripts/parity/README.md](scripts/parity/README.md).
### Interaction probe
`probe.py` launches a native window, sends window-relative input (click, hover,
hold), and captures the client area. It packages the rigs used to reproduce
interaction bugs. Windows-only. Details:
[scripts/probe/README.md](scripts/probe/README.md).
### Frame profiler
Set `CDN_PROFILE=1` (or `CDN_PROFILE=<path>`) and run any app. Every couple of
seconds it writes per-phase main-loop timings (events, app pump, render, with
draw sub-phases and per-frame draw counters) to a file. `CDN_FORCERENDER=1`
renders every frame so steady-state timings read on an otherwise idle screen.
```bash
CDN_PROFILE=1 CDN_FORCERENDER=1 ./demo/build/bin/macosArm64/debugExecutable/demo.kexe
```
Note: `present` is vsync-blocking, so profile on the target refresh rate before
drawing conclusions about a frame-rate gap.
### Demo probes
`:demo` ships headless regression probes driven by CLI flags, used by the
verify runbook and in isolation:
```bash
demo.kexe --nav3test # Navigation 3 + ViewModel + lifecycle
demo.kexe --backtest # predictive-back / BackHandler
demo.kexe --clicktest # pointer -> upstream clickable
demo.kexe --scrolltest # wheel -> scrollable
demo.kexe --multiwintest # multi-window lifecycle
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
```
### Windows smoke
The Mac runbook cannot cover the Windows target. Before shipping, on a Windows
host: build the mingwX64 executables, run `scripts/probe/`, and run a publish
dry run (only Windows compiles the full common-metadata variant table).
```bat
gradlew.bat :demo:runDebugExecutableMingwX64
gradlew.bat :apidemo:runDebugExecutableMingwX64
```
## Consuming the port
To build a third-party app against the published klibs, apply the bridge Gradle
plugin and declare official Compose Multiplatform coordinates; the plugin swaps
in the port's klibs on native desktop targets. Setup:
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
## Versioning and releasing
### Version map
Versions live in a few places, some of which move in lockstep. There is no
single file to edit; know which axis you are changing.
| Version | Where | Notes |
|---------|-------|-------|
| Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Group is `com.bitsycore.compose.sdl`. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
| Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. |
| JVM parity forcing | `vComposeJvmVersion` in `demo`, `apidemo`, and `material-symbols` `build.gradle.kts`. | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published. |
| Skiko | `skiko` in `gradle/libs.versions.toml`. | Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses. Verify with a throwaway `skikoRendererMain` compile if unsure. |
| SDL3 / SDL3_ttf / SDL3_image / FreeType | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
| Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. |
### Bump the upstream Compose ref
Run this on each upstream bump; it is the flow that keeps the sync tax low.
1. Edit `COMPOSE_CORE_REF` / `COMPOSE_REF` in `compose.properties` (and `compose`
in `libs.versions.toml` if the coordinate version moved).
2. `scripts/compose-fork/sync.sh`
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
its `// VENDOR-BASE:` header to the new ref.
4. Build both legs and fix any fallout.
5. `scripts/verify-mac.sh` (both legs green, including the soak and parity gates).
6. If a matching Compose version is now published to Maven, bump
`vComposeJvmVersion` in demo/apidemo/material-symbols to close the skew.
7. Run WIN-SMOKE on a Windows host.
8. Re-seed parity baselines only if metrics legitimately moved
(`parity.py --update-baselines`), and record why.
### Cut a release
1. Complete the ref-bump flow above and confirm it is green. For a STABLE
release, `vComposeJvmVersion` should match the vendored ref (no skew).
2. On a Windows host, run `gradlew :window:compileCommonMainKotlinMetadata`.
Only Windows compiles the full common-metadata variant table, and the root
KotlinMultiplatform publications live there; a macOS-only publish leaves the
roots without mingwX64 variants (this bit v0.1.15).
3. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
What the publish job does, so you can read a failure:
- Each host publishes only its own target's publications to GitHub Packages
under `com.bitsycore.compose.sdl:*`. Windows additionally publishes the root
KotlinMultiplatform metadata, the `jvm` publication, and the bridge plugin
(only Windows declares every target).
- The `MODULES` list in the workflow must stay in sync with the modules in
`settings.gradle.kts`.
- A partial publish (for example a dropped connection mid-upload) deletes this
host's half-published versions for the tag and retries, so a rerun is safe.
- The demo and apidemo release binaries are zipped and attached to the GitHub
Release for the tag.
4. Verify a consumer build against the new version resolves through the bridge.
+101 -13
View File
@@ -16,12 +16,38 @@ plugins {
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts. // Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
val vHostSupportsMingw: Boolean by rootProject.extra val vHostSupportsMingw: Boolean by rootProject.extra
// JVM parity target's Compose version — the dev build matching the pinned
// COMPOSE_CORE_REF (see scripts/compose-fork/compose.properties); mirrors
// :demo's forcing (Gradle orders "+dev" BELOW the plain version, so the
// plugin's beta01 would win conflict resolution without it).
val vComposeJvmVersion = libs.versions.compose.get()
val vComposeM3JvmVersion = libs.versions.composeMaterial3.get()
val vComposeJvmForced = mapOf(
"org.jetbrains.compose.runtime" to vComposeJvmVersion,
"org.jetbrains.compose.ui" to vComposeJvmVersion,
"org.jetbrains.compose.foundation" to vComposeJvmVersion,
"org.jetbrains.compose.animation" to vComposeJvmVersion,
"org.jetbrains.compose.material" to vComposeJvmVersion,
"org.jetbrains.compose.material3" to vComposeM3JvmVersion,
)
configurations.configureEach {
if (name.startsWith("jvm")) {
resolutionStrategy.eachDependency {
vComposeJvmForced[requested.group]?.let { useVersion(it) }
}
}
}
kotlin { kotlin {
jvm()
linuxArm64() linuxArm64()
linuxX64() linuxX64()
macosArm64() macosArm64()
if (vHostSupportsMingw) mingwX64() if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
targets.withType<KotlinNativeTarget>().all { targets.withType<KotlinNativeTarget>().all {
val isMingw = name == "mingwX64" val isMingw = name == "mingwX64"
val isLinux = name.startsWith("linux") val isLinux = name.startsWith("linux")
@@ -68,9 +94,31 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
implementation(project(":window")) // Common API on both stacks — usable from shared code.
implementation(project(":material3"))
implementation(project(":material-symbols")) implementation(project(":material-symbols"))
// Official Maven coords for everything the shared UI touches,
// so commonMain metadata (and the IDE's common analysis)
// resolve. Metadata + jvm resolve them from Maven; the NATIVE
// configurations substitute each for its port module (root
// build's FULL-COMMONIZATION BRIDGE). Every artifact must be
// declared DIRECTLY: transitives of a substituted module are
// invisible to the granular-metadata visibility check. The
// runtime is the official klib everywhere, never substituted
// (jvm configs force the +dev build above).
implementation("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
implementation("org.jetbrains.compose.ui:ui:$vComposeJvmVersion")
implementation("org.jetbrains.compose.ui:ui-graphics:$vComposeJvmVersion")
implementation("org.jetbrains.compose.ui:ui-text:$vComposeJvmVersion")
implementation("org.jetbrains.compose.ui:ui-unit:$vComposeJvmVersion")
implementation("org.jetbrains.compose.ui:ui-geometry:$vComposeJvmVersion")
implementation("org.jetbrains.compose.ui:ui-util:$vComposeJvmVersion")
implementation("org.jetbrains.compose.foundation:foundation:$vComposeJvmVersion")
implementation("org.jetbrains.compose.foundation:foundation-layout:$vComposeJvmVersion")
implementation("org.jetbrains.compose.animation:animation:$vComposeJvmVersion")
implementation("org.jetbrains.compose.animation:animation-core:$vComposeJvmVersion")
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
@@ -80,17 +128,32 @@ kotlin {
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
} }
} }
// Single HTTP engine on every desktop target: Ktor's Curl engine. It nativeMain {
// bundles a static libcurl per target (Schannel on Windows, OpenSSL on dependencies {
// macOS/Linux) embedded in its cinterop klib — no runtime DLL, one copy. // The SDL window shell + main loop (native-only).
// We use Curl everywhere (not WinHttp/Darwin) so that an upcoming implementation(project(":window"))
// client-certificate (mTLS) path — which calls the same bundled libcurl // Single HTTP engine on every native target: Ktor's Curl
// directly, since Ktor's engines expose no client-cert API — shares the // engine. It bundles a static libcurl per target (Schannel on
// exact same TLS stack as ordinary requests. // Windows, OpenSSL on macOS/Linux) embedded in its cinterop
if (vHostSupportsMingw) getByName("mingwX64Main").dependencies { implementation(libs.ktor.client.curl) } // klib — no runtime DLL, one copy. Curl everywhere (not
getByName("macosArm64Main").dependencies { implementation(libs.ktor.client.curl) } // WinHttp/Darwin) so the client-certificate (mTLS) path —
getByName("linuxX64Main").dependencies { implementation(libs.ktor.client.curl) } // which calls the same bundled libcurl directly, since Ktor's
getByName("linuxArm64Main").dependencies { implementation(libs.ktor.client.curl) } // engines expose no client-cert API — shares the exact same
// TLS stack as ordinary requests.
implementation(libs.ktor.client.curl)
}
}
jvmMain {
dependencies {
// Upstream Compose Desktop — the parity stack.
implementation(compose.desktop.currentOs)
// Coroutine-based engine on JVM; native targets keep Curl.
implementation(libs.ktor.client.cio)
// Dispatchers.Main provider on desktop JVM (must match
// kotlinx-coroutines-core's version — the catalog pins both).
implementation(libs.kotlinx.coroutines.swing)
}
}
} }
compilerOptions { compilerOptions {
@@ -99,6 +162,12 @@ kotlin {
} }
} }
compose.desktop {
application {
mainClass = "apidemo.MainJvmKt"
}
}
// SDL3 / SDL3_ttf / SDL3_image / FreeType are linked statically into the // SDL3 / SDL3_ttf / SDL3_image / FreeType are linked statically into the
// executable (see the mingw linkerOpts above), so there are no runtime DLLs to // executable (see the mingw linkerOpts above), so there are no runtime DLLs to
// bundle — the distributable is just <app>.exe + data.kres. // bundle — the distributable is just <app>.exe + data.kres.
@@ -277,3 +346,22 @@ for (variant in variants) {
} }
} }
} }
// ==================
// MARK: Stage fonts onto the JVM classpath
// ==================
// The JVM analog of the data.kres font/ entries: :material-symbols' JVM actual
// loads each style's font from the classpath at font/<Style>.ttf, and the
// jvm Fonts actual loads the mono body font at font/NotoSansMono.ttf.
tasks.named<ProcessResources>("jvmProcessResources") {
from(notoMonoFile) { into("font") }
dependsOn(":ui:downloadNotoFonts")
val vSymbolsProject = rootProject.project(":material-symbols")
for (vStyle in vUsedStyles) {
@Suppress("UNCHECKED_CAST")
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as org.gradle.api.provider.Provider<org.gradle.api.file.RegularFile>
val vDownloadTask = vSymbolsProject.extra["iconFontDownloadTask$vStyle"] as TaskProvider<*>
from(vFontFile) { into("font") }
dependsOn(vDownloadTask)
}
}
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
package apidemo
import androidx.compose.ui.text.font.FontFamily
// ==================
// MARK: Monospace body font (seam)
// ==================
// Family name the body editor passes for monospace text. Resolves to the
// bundled Noto Sans Mono once registered; null means the font isn't shipped
// (data.kres on native, classpath font/ on jvm), in which case the body falls
// back to the default proportional font.
const val kMonoFamily = "noto-mono"
/* The raw family name string — used by wrappedRowCount, which measures via the
platform text pipeline (accepts a name string). Null when not bundled. */
expect val monoFontFamilyName: String?
/* The material3 Text / BasicTextField-shaped FontFamily for the mono family.
Null when not bundled. */
expect val monoFontFamily: FontFamily?
@@ -1,7 +1,6 @@
package apidemo package apidemo
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.curl.Curl
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
@@ -23,7 +22,7 @@ import kotlin.time.TimeSource
run() is a suspend fun — call it off the UI dispatcher. */ run() is a suspend fun — call it off the UI dispatcher. */
class HttpRunner { class HttpRunner {
private val fClient = HttpClient(Curl) private val fClient = createApiHttpClient()
init { init {
// Clear any temporary client certs a prior crash left in the Windows // Clear any temporary client certs a prior crash left in the Windows
@@ -140,7 +139,7 @@ private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
/* Read a file's raw bytes (used for a FILE request body). */ /* Read a file's raw bytes (used for a FILE request body). */
internal fun readFileBytes(inPath: String): ByteArray = internal fun readFileBytes(inPath: String): ByteArray =
FileSystem.SYSTEM.read(inPath.trim().toPath()) { readByteArray() } systemFileSystem.read(inPath.trim().toPath()) { readByteArray() }
/* Inflate a gzip stream to its original bytes via okio's GzipSource. */ /* Inflate a gzip stream to its original bytes via okio's GzipSource. */
private fun gunzip(inBytes: ByteArray): ByteArray { private fun gunzip(inBytes: ByteArray): ByteArray {
@@ -19,7 +19,7 @@ private val fJson = Json {
/* Write the pack as pretty JSON to inPath. Returns null on success, else the /* Write the pack as pretty JSON to inPath. Returns null on success, else the
error message. */ error message. */
fun exportPack(inPack: Pack, inPath: String): String? = try { fun exportPack(inPack: Pack, inPath: String): String? = try {
FileSystem.SYSTEM.write(inPath.trim().toPath()) { systemFileSystem.write(inPath.trim().toPath()) {
writeUtf8(fJson.encodeToString(inPack)) writeUtf8(fJson.encodeToString(inPack))
} }
null null
@@ -29,7 +29,7 @@ fun exportPack(inPack: Pack, inPath: String): String? = try {
/* Read a pack back from inPath. */ /* Read a pack back from inPath. */
fun importPack(inPath: String): Result<Pack> = try { fun importPack(inPath: String): Result<Pack> = try {
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() } val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
Result.success(fJson.decodeFromString<Pack>(vText)) Result.success(fJson.decodeFromString<Pack>(vText))
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)
@@ -37,7 +37,7 @@ fun importPack(inPath: String): Result<Pack> = try {
/* Write a whole self-contained session to inPath. Returns null on success. */ /* Write a whole self-contained session to inPath. Returns null on success. */
fun exportSession(inSession: Session, inPath: String): String? = try { fun exportSession(inSession: Session, inPath: String): String? = try {
FileSystem.SYSTEM.write(inPath.trim().toPath()) { systemFileSystem.write(inPath.trim().toPath()) {
writeUtf8(fJson.encodeToString(inSession)) writeUtf8(fJson.encodeToString(inSession))
} }
null null
@@ -47,7 +47,7 @@ fun exportSession(inSession: Session, inPath: String): String? = try {
/* Read a session back from inPath. */ /* Read a session back from inPath. */
fun importSession(inPath: String): Result<Session> = try { fun importSession(inPath: String): Result<Session> = try {
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() } val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
Result.success(fJson.decodeFromString<Session>(vText)) Result.success(fJson.decodeFromString<Session>(vText))
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)
@@ -59,7 +59,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
val vSrc = inPath.trim().toPath() val vSrc = inPath.trim().toPath()
val vLeaf = inNewName.trim().let { if (it.contains('.')) it else "$it.json" } val vLeaf = inNewName.trim().let { if (it.contains('.')) it else "$it.json" }
val vDst = vSrc.parent?.div(vLeaf) ?: vLeaf.toPath() val vDst = vSrc.parent?.div(vLeaf) ?: vLeaf.toPath()
FileSystem.SYSTEM.atomicMove(vSrc, vDst) systemFileSystem.atomicMove(vSrc, vDst)
Result.success(vDst.toString()) Result.success(vDst.toString())
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)
@@ -68,7 +68,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
/* Write arbitrary text (a response body / headers dump) to inPath. Returns null /* Write arbitrary text (a response body / headers dump) to inPath. Returns null
on success, else the error message. */ on success, else the error message. */
fun writeTextFile(inPath: String, inText: String): String? = try { fun writeTextFile(inPath: String, inText: String): String? = try {
FileSystem.SYSTEM.write(inPath.trim().toPath()) { writeUtf8(inText) } systemFileSystem.write(inPath.trim().toPath()) { writeUtf8(inText) }
null null
} catch (e: Throwable) { } catch (e: Throwable) {
e.message ?: e.toString() e.message ?: e.toString()
@@ -76,7 +76,7 @@ fun writeTextFile(inPath: String, inText: String): String? = try {
/* Write raw bytes (a binary response body, e.g. an image) to inPath. */ /* Write raw bytes (a binary response body, e.g. an image) to inPath. */
fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try { fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
FileSystem.SYSTEM.write(inPath.trim().toPath()) { write(inBytes) } systemFileSystem.write(inPath.trim().toPath()) { write(inBytes) }
null null
} catch (e: Throwable) { } catch (e: Throwable) {
e.message ?: e.toString() e.message ?: e.toString()
@@ -1,7 +1,5 @@
package apidemo package apidemo
import com.compose.sdl.appDataDir
import com.compose.sdl.revealInFileManager
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okio.FileSystem import okio.FileSystem
@@ -86,7 +84,7 @@ private fun stateFilePath(): String? {
fun loadAppState(): AppState { fun loadAppState(): AppState {
val vPath = stateFilePath() ?: return AppState() val vPath = stateFilePath() ?: return AppState()
return try { return try {
val vText = FileSystem.SYSTEM.read(vPath.toPath()) { readUtf8() } val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
fStateJson.decodeFromString<AppState>(vText) fStateJson.decodeFromString<AppState>(vText)
} catch (e: Throwable) { } catch (e: Throwable) {
AppState() AppState()
@@ -98,7 +96,7 @@ fun loadAppState(): AppState {
fun saveAppState(inState: AppState) { fun saveAppState(inState: AppState) {
val vPath = stateFilePath() ?: return val vPath = stateFilePath() ?: return
try { try {
FileSystem.SYSTEM.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) } systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
} catch (e: Throwable) { } catch (e: Throwable) {
// ignore — nothing the user can act on, and packs can still be exported // ignore — nothing the user can act on, and packs can still be exported
} }
@@ -17,10 +17,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.compose.sdl.TextLayoutConfig
import com.compose.sdl.icons.MaterialSymbols import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
import com.compose.sdl.showOpenFileDialog
// ================== // ==================
// MARK: Panel 3 — request builder (Query / Headers / Body) // MARK: Panel 3 — request builder (Query / Headers / Body)
@@ -219,7 +217,7 @@ internal fun TabSizeSelector() {
var vOpen by remember { mutableStateOf(false) } var vOpen by remember { mutableStateOf(false) }
val vHoverSrc = remember { MutableInteractionSource() } val vHoverSrc = remember { MutableInteractionSource() }
val vHover by vHoverSrc.collectIsHoveredAsState() val vHover by vHoverSrc.collectIsHoveredAsState()
val vSize = TextLayoutConfig.tabWidth val vSize = editorTabWidth
TooltipBox( TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = { PlainTooltip { Text("Editor tab size") } }, tooltip = { PlainTooltip { Text("Editor tab size") } },
@@ -243,7 +241,7 @@ internal fun TabSizeSelector() {
for (vN in listOf(2, 4, 8)) { for (vN in listOf(2, 4, 8)) {
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text("$vN spaces", color = if (vN == vSize) c.accent else c.text) Text("$vN spaces", color = if (vN == vSize) c.accent else c.text)
}, onClick = { TextLayoutConfig.tabWidth = vN; vOpen = false }) }, onClick = { editorTabWidth = vN; vOpen = false })
} }
} }
} }
@@ -25,7 +25,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.compose.sdl.icons.MaterialSymbols import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
import com.compose.sdl.layout.x
// ================== // ==================
// MARK: Request tab strip (open requests; drag a tab to reorder) // MARK: Request tab strip (open requests; drag a tab to reorder)
@@ -31,11 +31,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.compose.sdl.fileManagerName
import com.compose.sdl.icons.MaterialSymbols import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
import com.compose.sdl.layout.y
import com.compose.sdl.revealInFileManager
// ================== // ==================
// MARK: Pack colour (palette dot + picker) // MARK: Pack colour (palette dot + picker)
@@ -23,8 +23,6 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import com.compose.sdl.icons.MaterialSymbols import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
import com.compose.sdl.layout.x
import com.compose.sdl.layout.y
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -141,7 +139,7 @@ internal fun CopyButton(inText: String, inLabel: String? = null) {
.background(if (vHover) c.accent.copy(alpha = 0.18f) else Color.Transparent, RoundedCornerShape(7.dp)) .background(if (vHover) c.accent.copy(alpha = 0.18f) else Color.Transparent, RoundedCornerShape(7.dp))
.hoverable(vHoverSrc) .hoverable(vHoverSrc)
.clickable { .clickable {
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(inText)) } vScope.launch { vClipboard.setClipEntry(clipEntryOfText(inText)) }
vCopied = true vCopied = true
} }
.padding(horizontal = if (inLabel != null) 10.dp else 5.dp, vertical = 5.dp), .padding(horizontal = if (inLabel != null) 10.dp else 5.dp, vertical = 5.dp),
@@ -0,0 +1,29 @@
package apidemo
// ==================
// MARK: TLS chain / client-certificate seam
// ==================
// The real implementations drive the bundled libcurl directly (CurlMtls.kt,
// nativeMain) — Ktor's engines expose no client-cert / CERTINFO API. The jvm
// parity app has no bundled libcurl, so its actuals report the feature as
// native-only instead.
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
Issuer / the PEM under "Cert" / dates …) and whether the server actually
presented it. Derived certs — an issuer resolved from the OS store, or a
name-only placeholder — have fromServer = false and are drawn dotted. */
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
/* The server's TLS certificate chain. error is set instead when it couldn't be
fetched; fields vary by TLS backend. */
class TlsChain(val certs: List<ChainCert>, val error: String?)
/* Fetch the TLS certificate chain the request's server presents. */
expect fun inspectTlsChain(inReq: ApiRequest): TlsChain
/* Send the request with its client certificate attached (mTLS). */
expect fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
/* Remove temp client certs a previous crashed run left behind. Call once at
startup; no-op where the OS store isn't used. */
expect fun sweepTempClientCerts()
@@ -39,14 +39,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.compose.sdl.icons.MaterialSymbols import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
import com.compose.sdl.removeMemoryResource
import com.compose.sdl.res.ResourceKind
import com.compose.sdl.res.painterResource
import com.compose.sdl.scrollbar.ScrollbarStyle
import com.compose.sdl.scrollbar.VerticalScrollbar
import com.compose.sdl.scrollbar.rememberScrollbarAdapter
import com.compose.sdl.showSaveFileDialog
import com.compose.sdl.text.currentTextMeasurer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// ================== // ==================
@@ -169,8 +161,8 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
inBody = if (vRespImage) null else vBody, inBody = if (vRespImage) null else vBody,
inIsImage = vRespImage && !vLoading, inIsImage = vRespImage && !vLoading,
inImagePainter = if (vRespImage && !vLoading) { inImagePainter = if (vRespImage && !vLoading) {
val vKind = if (vResp?.contentType?.contains("svg", ignoreCase = true) == true) ResourceKind.Svg else ResourceKind.Raster val vSvg = vResp?.contentType?.contains("svg", ignoreCase = true) == true
painterResource(inRs.imageKey!!, vKind) memoryImagePainter(inRs.imageKey!!, vSvg)
} else null, } else null,
inHeadersCollapsed = vHeadersCollapsed, inHeadersCollapsed = vHeadersCollapsed,
inOnToggleCollapse = { vHeadersCollapsed = !vHeadersCollapsed }, inOnToggleCollapse = { vHeadersCollapsed = !vHeadersCollapsed },
@@ -247,7 +239,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
inResponseBody = vRespBody, inResponseBody = vRespBody,
inOnMessage = { vMsg = it }, inOnMessage = { vMsg = it },
inOnClear = { inOnClear = {
inRs.imageKey?.let { removeMemoryResource(it) } inRs.imageKey?.let { removeMemoryImage(it) }
inRs.imageKey = null inRs.imageKey = null
inRs.response = null inRs.response = null
inRs.sentReq = null inRs.sentReq = null
@@ -508,7 +500,7 @@ internal fun HttpFlowView(
val vS = vSel val vS = vSel
if (vS != null && !vS.isEmpty) { if (vS != null && !vS.isEmpty) {
val vText = vBody.substring(vS.start, vS.end) val vText = vBody.substring(vS.start, vS.end)
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vText)) } vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vText)) }
true true
} else false } else false
} }
@@ -974,18 +966,18 @@ internal fun BodyView(
// also use it for the first frame, before the body reports its width). // also use it for the first frame, before the body reports its width).
val vNumColor = c.dim.copy(alpha = 0.45f) val vNumColor = c.dim.copy(alpha = 0.45f)
val vNumbers = remember(inText, vBodyWidthPx, inSoftWrap, vFontPx) { val vNumbers = remember(inText, vBodyWidthPx, inSoftWrap, vFontPx) {
val vM = if (inSoftWrap && vBodyWidthPx > 0) currentTextMeasurer else null val vWrapRows = inSoftWrap && vBodyWidthPx > 0
buildString { buildString {
for ((vIdx, vSrc) in vLines.withIndex()) { for ((vIdx, vSrc) in vLines.withIndex()) {
if (vIdx > 0) append('\n') if (vIdx > 0) append('\n')
append(vIdx + 1) append(vIdx + 1)
if (vM != null) { if (vWrapRows) {
// Pass the density-scaled font size so this wrap matches the body's // Pass the density-scaled font size so this wrap matches the body's
// actual glyph pixel widths — passing 12 (sp) here counted each // actual glyph pixel widths — passing 12 (sp) here counted each
// char as half its rendered size, so long lines that visually // char as half its rendered size, so long lines that visually
// wrapped into 3 rows in the body were estimated as 1 or 2 rows // wrapped into 3 rows in the body were estimated as 1 or 2 rows
// here and the gutter numbers drifted upward. // here and the gutter numbers drifted upward.
val vRows = vM.wrap(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).lines.size.coerceAtLeast(1) val vRows = wrappedRowCount(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).coerceAtLeast(1)
repeat(vRows - 1) { append('\n') } repeat(vRows - 1) { append('\n') }
} }
} }
@@ -1309,18 +1301,18 @@ internal fun ViewerOverflowMenu(
} }
DropdownMenu(expanded = vOpen, onDismissRequest = { vOpen = false }) { DropdownMenu(expanded = vOpen, onDismissRequest = { vOpen = false }) {
DropdownMenuItem(text = { Text("Copy all") }, onClick = { DropdownMenuItem(text = { Text("Copy all") }, onClick = {
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(copyAll())) } vScope.launch { vClipboard.setClipEntry(clipEntryOfText(copyAll())) }
inOnMessage("Copied all.") inOnMessage("Copied all.")
vOpen = false vOpen = false
}) })
DropdownMenuItem(text = { Text("Copy headers") }, onClick = { DropdownMenuItem(text = { Text("Copy headers") }, onClick = {
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vHeadersText)) } vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vHeadersText)) }
inOnMessage("Copied headers.") inOnMessage("Copied headers.")
vOpen = false vOpen = false
}) })
if (vCanCopyBody) { if (vCanCopyBody) {
DropdownMenuItem(text = { Text("Copy body") }, onClick = { DropdownMenuItem(text = { Text("Copy body") }, onClick = {
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vBodyText)) } vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vBodyText)) }
inOnMessage("Copied body.") inOnMessage("Copied body.")
vOpen = false vOpen = false
}) })
@@ -0,0 +1,77 @@
package apidemo
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.ClipEntry
import io.ktor.client.HttpClient
import okio.FileSystem
// ==================
// MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop)
// ==================
// The port modules are native-only, so shared code can't call com.compose.sdl
// directly — every SDL-backed API the app uses goes through these expects.
// Signatures mirror the native originals so the native actuals are pure
// delegation (same pattern as :demo's demo.shim package).
/* Per-user app-data dir (created on demand, ends with a separator) or null. */
expect fun appDataDir(inOrg: String, inApp: String): String?
/* Open the OS file manager showing the given path. Fire-and-forget. */
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
/* "Explorer" / "Finder" / "Files" — for menu labels. */
expect fun fileManagerName(): String
/* Async save-file dialog; the callback gets the chosen absolute path or null. */
expect fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> Unit)
/* Async open-file dialog; the callback gets the chosen absolute path or null. */
expect fun showOpenFileDialog(inOnResult: (String?) -> Unit)
/* A plain-text clipboard entry (ClipEntry construction differs per stack). */
expect fun clipEntryOfText(inText: String): ClipEntry
// ============
// In-memory response images
/* Register/replace decoded-image source bytes under a key (resp-image://N). */
expect fun registerMemoryImage(inKey: String, inBytes: ByteArray)
/* Drop a registered image's bytes. */
expect fun removeMemoryImage(inKey: String)
/* Painter for a registered image key (raster or SVG). */
@Composable
expect fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter
// ============
// Text layout
/* Rows the given line occupies when soft-wrapped to inMaxWidthPx at
inFontPx (already density-scaled). Used by the body gutter so its line
numbers stay aligned with the wrapped body text. */
expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int
/* Editor tab width in spaces — how wide a typed '\t' renders. On the native
stack this drives the project text pipeline (TextLayoutConfig); the jvm
parity app only stores the preference. */
expect var editorTabWidth: Int
// ============
// Infrastructure
/* okio's FileSystem.SYSTEM is declared per-platform, not in its common
metadata — surface it through a seam. */
internal expect val systemFileSystem: FileSystem
/* The app's Ktor client: Curl on native (bundled libcurl — same TLS stack as
the mTLS path), CIO on the jvm parity target. */
expect fun createApiHttpClient(): HttpClient
/* Window-shell integration: an intercepting close handler (return true to
allow the close) and an app-wide key-shortcut handler; both cleared when
the composition leaves. */
@Composable
expect fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean)
@@ -0,0 +1,16 @@
package apidemo
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
// COPY of the port's com.compose.sdl.layout LayoutCoordinates sugar so the
// same helpers resolve on the jvm parity target (the port modules are
// native-only). Guards `isAttached`: a menu/tooltip anchor can be read while
// its node is mid-recycle, and positionInWindow() throws on a detached
// coordinator.
/* Logical-point X of this layout in the window. */
val LayoutCoordinates.x: Int get() = if (isAttached) positionInWindow().x.toInt() else 0
/* Logical-point Y of this layout in the window. */
val LayoutCoordinates.y: Int get() = if (isAttached) positionInWindow().y.toInt() else 0
@@ -0,0 +1,333 @@
// COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME
// implementation runs on the jvm parity target — the port modules are
// native-only, so apidemo's commonMain cannot reference them directly.
// Keep in sync with the original when it changes.
package apidemo
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.hoverable
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
// ==================
// MARK: Scrollbar (Compose Desktop-style)
// ==================
/* A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
it a ScrollbarAdapter built from the same ScrollState. This project's Box has
no BoxScope (so no Modifier.align) — pin the bar with the Box's
contentAlignment; the fillMaxSize content fills the rest:
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
val state = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(state)) { ... }
VerticalScrollbar(
adapter = rememberScrollbarAdapter(state),
modifier = Modifier.fillMaxHeight(),
)
}
Always visible (no auto-hide); the thumb tweens unhoverColor -> hoverColor on
hover over hoverDurationMillis, matching upstream. Drag the thumb to scroll;
click the track to page. */
@Immutable
class ScrollbarStyle(
val minimalHeight: Dp,
val thickness: Dp,
val shape: Shape,
val hoverDurationMillis: Int,
val unhoverColor: Color,
val hoverColor: Color,
)
/* Upstream default: an 8dp black thumb that's faint until hovered. Dark UIs
provide a white-tinted style via LocalScrollbarStyle or the `style` param. */
fun defaultScrollbarStyle(): ScrollbarStyle = ScrollbarStyle(
minimalHeight = 16.dp,
thickness = 8.dp,
shape = RoundedCornerShape(4.dp),
hoverDurationMillis = 300,
unhoverColor = Color.Black.copy(alpha = 0.12f),
hoverColor = Color.Black.copy(alpha = 0.50f),
)
val LocalScrollbarStyle = compositionLocalOf { defaultScrollbarStyle() }
// ==================
// MARK: ScrollbarAdapter
// ==================
/* Bridges a scrollable's state to the scrollbar in pixels. scrollOffset /
contentSize / viewportSize are along the scroll axis; the thumb is sized by
viewportSize / contentSize and positioned by scrollOffset / (content - viewport). */
interface ScrollbarAdapter {
val scrollOffset: Double
val contentSize: Double
val viewportSize: Double
suspend fun scrollTo(scrollOffset: Double)
}
// (The port also ships a ScrollState-based adapter; it reads foundation
// internals (ScrollState.contentSize/viewportSize) that a copy outside
// :foundation cannot access. apidemo only scrollbars a LazyColumn.)
/* Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the
size of its VISIBLE items, so exact pixel geometry of off-screen content is
unknowable — we estimate it from the average visible item size × total item
count (the same approach as Compose Desktop). Accurate enough for a thumb
whose length/position track the scroll position; the estimate self-corrects
as items of different sizes scroll through the viewport. */
@Composable
fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter =
remember(scrollState) { LazyListScrollbarAdapter(scrollState) }
private class LazyListScrollbarAdapter(private val state: LazyListState) : ScrollbarAdapter {
// Mean main-axis size of the currently-visible items, in px (>= 1 to avoid /0).
private fun averageItemSize(): Double {
val vVisible = state.layoutInfo.visibleItemsInfo
if (vVisible.isEmpty()) return 0.0
val vSpan = (vVisible.last().offset + vVisible.last().size) - vVisible.first().offset
return (vSpan.toDouble() / vVisible.size).coerceAtLeast(1.0)
}
override val scrollOffset: Double
get() = averageItemSize() * state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset
override val contentSize: Double
get() = averageItemSize() * state.layoutInfo.totalItemsCount
override val viewportSize: Double
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
override suspend fun scrollTo(scrollOffset: Double) {
val vDistance = scrollOffset - this.scrollOffset
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
// list's pixel geometry is only ESTIMATED (average visible item size), and
// that estimate drifts as differently-sized items scroll through the
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
// sidesteps the estimate entirely. This is what Compose Desktop's own
// LazyListScrollbarAdapter does, and for the same reason.
if (abs(vDistance) <= viewportSize) {
state.scrollBy(vDistance.toFloat())
return
}
// Far jump (track-click paging past a viewport): no exact geometry to lean
// on, so estimate an index/offset from the average and snap to it.
val vAvg = averageItemSize()
if (vAvg <= 0.0) return
val vTarget = scrollOffset.coerceAtLeast(0.0)
val vTotal = state.layoutInfo.totalItemsCount
val vIndex = (vTarget / vAvg).toInt().coerceIn(0, (vTotal - 1).coerceAtLeast(0))
val vItemOffset = (vTarget - vIndex * vAvg).toInt().coerceAtLeast(0)
state.scrollToItem(vIndex, vItemOffset)
}
}
// ==================
// MARK: VerticalScrollbar / HorizontalScrollbar
// ==================
@Composable
fun VerticalScrollbar(
adapter: ScrollbarAdapter,
modifier: Modifier = Modifier,
reverseLayout: Boolean = false,
style: ScrollbarStyle = LocalScrollbarStyle.current,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = true)
@Composable
fun HorizontalScrollbar(
adapter: ScrollbarAdapter,
modifier: Modifier = Modifier,
reverseLayout: Boolean = false,
style: ScrollbarStyle = LocalScrollbarStyle.current,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = false)
/* Shared impl. The root Box is the track (fixed thickness on the cross axis,
filling the main axis from the caller's modifier); a child Box is the thumb,
positioned with an offset. Drag + track-click are handled on the track in
track-relative coordinates (a stable origin), so the grab point stays under
the pointer with no feedback from the thumb moving. interactionSource is
accepted for API parity; hover here is driven by Modifier.hoverable. */
@Composable
private fun Scrollbar(
adapter: ScrollbarAdapter,
modifier: Modifier,
reverseLayout: Boolean,
style: ScrollbarStyle,
@Suppress("UNUSED_PARAMETER") interactionSource: MutableInteractionSource,
isVertical: Boolean,
) {
val vScope = rememberCoroutineScope()
var vTrackPx by remember { mutableStateOf(0) }
val vHoverSource = remember { MutableInteractionSource() }
val vHovered by vHoverSource.collectIsHoveredAsState()
var vDraggingThumb by remember { mutableStateOf(false) }
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
// physical pixels. Passing them into `.dp` doubles again through
// density.toPx() on Retina, which visibly bloated the thumb (filled the
// whole track) and made drag/track-click positions land 2× too far down.
// Route pixel-sized modifiers through the current density so `.dp` and the
// pixel value stay in the same coord space.
val vDensity = LocalDensity.current
val vContent = adapter.contentSize
val vViewport = adapter.viewportSize
val vOffset = adapter.scrollOffset
val vMaxScroll = vContent - vViewport
val vScrollable = vMaxScroll > 0.5 && vViewport > 0.0
val vColor by animateColorAsState(
targetValue = if (vHovered || vDraggingThumb) style.hoverColor else style.unhoverColor,
animationSpec = tween(style.hoverDurationMillis),
)
val vRoot = (if (isVertical) modifier.width(style.thickness) else modifier.height(style.thickness))
.onSizeChanged { vTrackPx = if (isVertical) it.height else it.width }
.hoverable(vHoverSource)
Box(modifier = vRoot) {
if (!vScrollable || vTrackPx <= 0) return@Box
// ============
// Thumb metrics (px)
val vMinLen = style.minimalHeight.value.toInt()
val vThumbLen = (vTrackPx * (vViewport / vContent)).roundToInt()
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
val vTravel = vTrackPx - vThumbLen
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
val vThumbMod = (
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
else Modifier.offset { IntOffset(vThumbPos, 0) }.height(style.thickness).width(vThumbLenDp)
)
.clip(style.shape)
.background(vColor)
Box(modifier = vThumbMod)
// ============
// Drag the thumb / page on track click — handled on the track so the
// coordinates have a stable (non-moving) origin. The press is CONSUMED:
// a scrollbar owns the pointer input it handles, so content sharing this
// region (the scrollable beneath, or a parent that also reads pointer
// input) never reacts to the same press. Geometry is read LIVE from the
// adapter at press time — not from the captured render metrics above,
// which go stale between pointerInput relaunches — so a grab after a
// wheel-scroll still hit-tests against the thumb's current position.
Box(
modifier = Modifier
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
.pointerInput(isVertical, adapter, vTrackPx, reverseLayout) {
awaitEachGesture {
val vDown = awaitFirstDown(requireUnconsumed = false)
vDown.consume()
// Live geometry — stable for the drag's duration (content /
// viewport / track sizes don't change mid-drag).
val vContentPx = adapter.contentSize
val vViewportPx = adapter.viewportSize
val vMaxScrollPx = vContentPx - vViewportPx
if (vMaxScrollPx <= 0.0 || vTrackPx <= 0) return@awaitEachGesture
val vMinLen = style.minimalHeight.value.toInt()
val vLen = (vTrackPx * (vViewportPx / vContentPx)).roundToInt()
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
val vTravelPx = vTrackPx - vLen
val vRaw = if (vTravelPx <= 0) 0
else (vTravelPx * (adapter.scrollOffset / vMaxScrollPx)).roundToInt()
val vPos = (if (reverseLayout) vTravelPx - vRaw else vRaw).coerceIn(0, vTravelPx)
// Map a track-relative thumb-top to a scroll offset. The
// target uses the LIVE max scroll (read fresh each move),
// not the press-time value — the adapter compares it against
// its live scrollOffset, and mixing a stale scale with a live
// one makes the delta run away (grab at the bottom, drag up,
// and the list overshoots to the middle).
fun scrollToTrackPos(inThumbTop: Int) {
if (vTravelPx <= 0) return
val vLiveMax = adapter.contentSize - adapter.viewportSize
if (vLiveMax <= 0.0) return
val vClamped = inThumbTop.coerceIn(0, vTravelPx)
val vFrac = (if (reverseLayout) vTravelPx - vClamped else vClamped).toDouble() / vTravelPx
vScope.launch { adapter.scrollTo(vFrac * vLiveMax) }
}
val vPress = (if (isVertical) vDown.position.y else vDown.position.x).toInt()
val vGrab: Int
if (vPress in vPos..(vPos + vLen)) {
vDraggingThumb = true
vGrab = vPress - vPos
} else {
// Page toward the click by one viewport.
vDraggingThumb = false
vGrab = 0
val vDir = if (vPress < vPos) -1.0 else 1.0
vScope.launch { adapter.scrollTo(adapter.scrollOffset + vDir * vViewportPx) }
}
// Follow the pressed pointer to release, consuming every
// change so the interaction can't leak. Only a thumb grab
// drags-to-scroll; a track press just consumes.
do {
val vEvent = awaitPointerEvent()
val vChange = vEvent.changes.firstOrNull { it.id == vDown.id } ?: break
if (vDraggingThumb) {
val vCur = (if (isVertical) vChange.position.y else vChange.position.x).toInt()
scrollToTrackPos(vCur - vGrab)
}
vChange.consume()
} while (vEvent.changes.any { it.id == vDown.id && it.pressed })
vDraggingThumb = false
}
}
)
}
}
/* Sizes the invisible drag-capture overlay to the full track. `inTrackDp`
comes from the caller pre-converted via LocalDensity (Option-B: layout
coords are physical px, so px-to-dp goes through the current density). */
private fun Modifier.matchTrackSize(inVertical: Boolean, inTrackDp: Dp, inThickness: Dp): Modifier =
if (inVertical) this.width(inThickness).height(inTrackDp)
else this.height(inThickness).width(inTrackDp)
@@ -0,0 +1,172 @@
// COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME
// implementation runs on the jvm parity target — the port modules are
// native-only, so apidemo's commonMain cannot reference them directly.
// Keep in sync with the original when it changes.
package apidemo
import androidx.compose.foundation.background
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
// ==================
// MARK: SplitPane
// ==================
/** Two-pane resizable split. `initialFirstSize` seeds the first pane's width
* (horizontal split) or height (vertical split); after first layout it tracks
* the user's drag. `minFirstSize` / `minSecondSize` clamp so neither pane can
* shrink past its minimum.
*
* Internal sizing is in **pixels** to match this port's pixel-space layout
* tree (Option-B density flow). `Modifier.width((px / density).dp)` converts
* back at the modifier boundary. */
@Composable
fun HorizontalSplitPane(
modifier: Modifier = Modifier,
initialFirstSize: Dp = 200.dp,
minFirstSize: Dp = 50.dp,
minSecondSize: Dp = 50.dp,
dividerColor: Color = Color(0x33FFFFFFL),
dividerHoverColor: Color = Color(0x66FFFFFFL),
first: @Composable () -> Unit,
second: @Composable () -> Unit,
) {
val vDensity = LocalDensity.current
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
var vTotalWidthPx by remember { mutableStateOf(0) }
var vFirstWidthPx by remember { mutableStateOf(vInitialFirstPx) }
val vDividerSource = remember { MutableInteractionSource() }
val vDividerHover by vDividerSource.collectIsHoveredAsState()
var vDragging by remember { mutableStateOf(false) }
Row(
modifier = modifier
.fillMaxSize()
.onSizeChanged { vTotalWidthPx = it.width },
) {
val vClampedFirstPx = if (vTotalWidthPx > 0)
vFirstWidthPx.coerceIn(vMinFirstPx, (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
else vFirstWidthPx
val vSecondWidthPx = (vTotalWidthPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
// Solid divider that fills its own slot — what you see is exactly what
// you can grab. Hover or an in-progress drag just changes the colour (no
// size change → no layout shift, and no flicker when the pointer briefly
// leaves the slot mid-drag).
val vActive = vDividerHover || vDragging
Box(
modifier = Modifier
.width(SplitPaneDefaults.DividerThickness)
.fillMaxHeight()
.background(if (vActive) dividerHoverColor else dividerColor)
.hoverable(vDividerSource)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { vDragging = true },
onDragEnd = { vDragging = false },
onDragCancel = { vDragging = false },
onDrag = { _, delta ->
val vMax = (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
vFirstWidthPx = (vFirstWidthPx + delta.x.toInt()).coerceIn(vMinFirstPx, vMax)
},
)
},
)
Box(modifier = Modifier.width(with(vDensity) { vSecondWidthPx.toDp() }).fillMaxHeight()) { second() }
}
}
@Composable
fun VerticalSplitPane(
modifier: Modifier = Modifier,
initialFirstSize: Dp = 150.dp,
minFirstSize: Dp = 50.dp,
minSecondSize: Dp = 50.dp,
dividerColor: Color = Color(0x33FFFFFFL),
dividerHoverColor: Color = Color(0x66FFFFFFL),
first: @Composable () -> Unit,
second: @Composable () -> Unit,
) {
val vDensity = LocalDensity.current
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
var vTotalHeightPx by remember { mutableStateOf(0) }
var vFirstHeightPx by remember { mutableStateOf(vInitialFirstPx) }
val vDividerSource = remember { MutableInteractionSource() }
val vDividerHover by vDividerSource.collectIsHoveredAsState()
var vDragging by remember { mutableStateOf(false) }
var vPressStartFirstPx by remember { mutableStateOf(0) }
Column(
modifier = modifier
.fillMaxSize()
.onSizeChanged { vTotalHeightPx = it.height },
) {
val vClampedFirstPx = if (vTotalHeightPx > 0)
vFirstHeightPx.coerceIn(vMinFirstPx, (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
else vFirstHeightPx
val vSecondHeightPx = (vTotalHeightPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
Box(modifier = Modifier.height(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxWidth()) { first() }
// Solid divider that fills its slot; hover/drag only changes the colour
// (no size change → no layout shift, no flicker mid-drag).
val vActive = vDividerHover || vDragging
Box(
modifier = Modifier
.height(SplitPaneDefaults.DividerThickness)
.fillMaxWidth()
.background(if (vActive) dividerHoverColor else dividerColor)
.hoverable(vDividerSource)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { vDragging = true },
onDragEnd = { vDragging = false },
onDragCancel = { vDragging = false },
onDrag = { _, delta ->
val vMax = (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
vFirstHeightPx = (vFirstHeightPx + delta.y.toInt()).coerceIn(vMinFirstPx, vMax)
},
)
},
)
Box(modifier = Modifier.height(with(vDensity) { vSecondHeightPx.toDp() }).fillMaxWidth()) { second() }
}
}
object SplitPaneDefaults {
val DividerThickness: Dp = 4.dp // divider width = hit/drag area (solid; what you see is what you grab)
}
+162
View File
@@ -0,0 +1,162 @@
@file:OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
package apidemo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp
import java.awt.Desktop
import java.awt.EventQueue
import java.awt.FileDialog
import java.awt.Frame
import java.awt.datatransfer.StringSelection
import java.io.File
import org.jetbrains.skia.Image
// ==================
// MARK: JVM actuals — AWT + upstream Compose Desktop
// ==================
private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
private val kIsMac = System.getProperty("os.name").startsWith("Mac")
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
// ============
// Window hooks — the shared App installs them; MainJvm wires them into the
// upstream Window() (onCloseRequest / onPreviewKeyEvent).
internal var jvmOnCloseRequest: (() -> Boolean)? = null
internal var jvmOnKeyShortcut: ((KeyEvent) -> Boolean)? = null
@Composable
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
androidx.compose.runtime.DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
jvmOnCloseRequest = inOnCloseRequest
jvmOnKeyShortcut = inOnKeyShortcut
onDispose { jvmOnCloseRequest = null; jvmOnKeyShortcut = null }
}
}
/* Mirrors SDL_GetPrefPath's layout: <os user-data dir>/<org>/<app>/ with a
trailing separator, created on demand. */
actual fun appDataDir(inOrg: String, inApp: String): String? {
val vHome = System.getProperty("user.home") ?: return null
val vBase = when {
kIsWindows -> System.getenv("APPDATA") ?: "$vHome/AppData/Roaming"
kIsMac -> "$vHome/Library/Application Support"
else -> System.getenv("XDG_DATA_HOME") ?: "$vHome/.local/share"
}
val vDir = File(File(vBase, inOrg), inApp)
if (!vDir.exists() && !vDir.mkdirs()) return null
return vDir.absolutePath + File.separator
}
/* Opens the path's containing folder (AWT has no cross-platform "select in
file manager"); fire-and-forget off the EDT. */
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) {
Thread {
val vOk = runCatching {
val vFile = File(inPath)
Desktop.getDesktop().open(if (vFile.isDirectory) vFile else vFile.parentFile ?: vFile)
}.isSuccess
inOnResult?.invoke(vOk)
}.apply { isDaemon = true }.start()
}
actual fun fileManagerName(): String = when {
kIsWindows -> "Explorer"
kIsMac -> "Finder"
else -> "Files"
}
/* AWT FileDialog runs its own nested event loop, so invoking from the EDT
(where Compose Desktop also runs) keeps the UI serviced while modal. */
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) {
EventQueue.invokeLater {
val vDlg = FileDialog(null as Frame?, "Save", FileDialog.SAVE)
inDefaultName?.let { vDlg.file = it }
vDlg.isVisible = true
inOnResult(vDlg.file?.let { vDlg.directory + it })
}
}
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) {
EventQueue.invokeLater {
val vDlg = FileDialog(null as Frame?, "Open", FileDialog.LOAD)
vDlg.isVisible = true
inOnResult(vDlg.file?.let { vDlg.directory + it })
}
}
/* Upstream desktop's ClipEntry wraps an AWT Transferable. */
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry(StringSelection(inText))
// ============
// In-memory response images
private val kMemoryImages = HashMap<String, ByteArray>()
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) { kMemoryImages[inKey] = inBytes }
actual fun removeMemoryImage(inKey: String) { kMemoryImages.remove(inKey) }
@Composable
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter {
val vDensity = LocalDensity.current
return remember(inKey, inSvg) {
val vBytes = kMemoryImages[inKey]
runCatching {
if (vBytes == null) error("no bytes for $inKey")
if (inSvg) loadSvgPainter(vBytes.inputStream(), vDensity)
else BitmapPainter(Image.makeFromEncoded(vBytes).toComposeImageBitmap())
}.getOrElse { BitmapPainter(ImageBitmap(1, 1)) }
}
}
// ============
// Text layout
/* Upstream TextMeasurer at density 1 so the px-valued font size maps 1:1
(the caller passes an already density-scaled size). */
private val kRowMeasurer: TextMeasurer by lazy {
TextMeasurer(createFontFamilyResolver(), Density(1f), LayoutDirection.Ltr)
}
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
runCatching {
kRowMeasurer.measure(
AnnotatedString(inText),
TextStyle(fontSize = inFontPx.sp, fontFamily = monoFontFamily ?: FontFamily.Monospace),
constraints = Constraints(maxWidth = inMaxWidthPx),
).lineCount
}.getOrDefault(1)
/* The jvm parity app only stores the preference — upstream's text pipeline has
no global tab-width knob. Backed by state so menu checkmarks recompose. */
private var fTabWidth by mutableStateOf(4)
actual var editorTabWidth: Int
get() = fTabWidth
set(value) { fTabWidth = value }
+23
View File
@@ -0,0 +1,23 @@
package apidemo
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.Font
// ==================
// MARK: Monospace body font — JVM actuals (classpath font/ staged by
// jvmProcessResources)
// ==================
actual val monoFontFamily: FontFamily? by lazy {
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
if (vBytes == null) {
println("apidemo: NotoSansMono.ttf not on the classpath — body uses the default font")
null
} else {
FontFamily(Font(identity = kMonoFamily, data = vBytes))
}
}
actual val monoFontFamilyName: String? by lazy {
if (monoFontFamily != null) kMonoFamily else null
}
+22
View File
@@ -0,0 +1,22 @@
package apidemo
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
// The JVM comparison app's entry point. Runs the SAME shared App() as the
// native :apidemo, but on Compose Desktop (JVM) against upstream
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only
// (they drive the bundled libcurl); the jvm actuals report that instead.
fun main() = application {
Window(
// The shared App installs a persist-then-close hook (InstallWindowHooks).
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
title = "API Manager — JVM (upstream Compose)",
state = rememberWindowState(width = 1240.dp, height = 820.dp),
) {
App()
}
}
+26
View File
@@ -0,0 +1,26 @@
package apidemo
// ==================
// MARK: TLS chain / client-certificate — JVM actuals (native-only feature)
// ==================
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
// there is no libcurl on the jvm parity stack, so both entry points report
// the limitation instead of half-implementing it over JSSE.
private const val kNativeOnly =
"Client-certificate (mTLS) features need the native build — they drive the bundled libcurl directly."
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly)
actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse = ApiResponse(
ok = false,
status = 0,
statusText = "",
timeMs = 0,
sizeBytes = 0,
headers = emptyList(),
body = "",
error = kNativeOnly,
)
actual fun sweepTempClientCerts() {}
@@ -0,0 +1,63 @@
package apidemo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.ClipEntry
import com.compose.sdl.LocalComposeNativeWindow
import com.compose.sdl.TextLayoutConfig
import com.compose.sdl.registerMemoryResource
import com.compose.sdl.removeMemoryResource
import com.compose.sdl.res.ResourceKind
import com.compose.sdl.text.currentTextMeasurer
// ==================
// MARK: Native actuals — pure delegation to the port's SDL-backed APIs
// ==================
actual fun appDataDir(inOrg: String, inApp: String): String? =
com.compose.sdl.appDataDir(inOrg, inApp)
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) =
com.compose.sdl.revealInFileManager(inPath, inOnResult)
actual fun fileManagerName(): String = com.compose.sdl.fileManagerName()
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) =
com.compose.sdl.showSaveFileDialog(inDefaultName, inOnResult)
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) =
com.compose.sdl.showOpenFileDialog(inOnResult)
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry.withPlainText(inText)
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) = registerMemoryResource(inKey, inBytes)
actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
@Composable
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
com.compose.sdl.res.painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
currentTextMeasurer.wrap(inText, inFontPx, inMaxWidthPx, inFamilyName).lines.size
actual var editorTabWidth: Int
get() = TextLayoutConfig.tabWidth
set(value) { TextLayoutConfig.tabWidth = value }
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
io.ktor.client.HttpClient(io.ktor.client.engine.curl.Curl)
@Composable
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
val vWindow = LocalComposeNativeWindow.current
DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
vWindow.setOnCloseRequest(inOnCloseRequest)
vWindow.setOnKeyShortcut(inOnKeyShortcut)
onDispose { vWindow.setOnCloseRequest(null); vWindow.setOnKeyShortcut(null) }
}
}
+6 -17
View File
@@ -39,21 +39,10 @@ class PreparedCert(
Throws with a user-facing message if the cert/key can't be loaded. */ Throws with a user-facing message if the cert/key can't be loaded. */
expect fun prepareClientCert(inReq: ApiRequest): PreparedCert expect fun prepareClientCert(inReq: ApiRequest): PreparedCert
/* Remove any temporary client certs (and their key containers) a previous run // ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
left behind in the Windows store after a crash — identified by our prefix, so // curlSendWithClientCert expects live in commonMain (TlsCommon.kt) — the UI
the user's own certificates are never touched. No-op off Windows. Call once // consumes them from shared code; the per-OS ClientCert*.kt actuals below the
at startup. */ // native tree actualize sweepTempClientCerts directly.
expect fun sweepTempClientCerts()
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
Issuer / the PEM under "Cert" / dates …) and whether the server actually
presented it. Derived certs — an issuer resolved from the OS store, or a
name-only placeholder — have fromServer = false and are drawn dotted. */
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
/* The server's TLS certificate chain. error is set instead when it couldn't be
fetched; fields vary by TLS backend. */
class TlsChain(val certs: List<ChainCert>, val error: String?)
/* Continue a server-presented chain with its issuer(s). Where possible the /* Continue a server-presented chain with its issuer(s). Where possible the
issuer is resolved from the OS certificate store with full info; otherwise the issuer is resolved from the OS certificate store with full info; otherwise the
@@ -120,7 +109,7 @@ private fun onCurlDiscard(inBuffer: CPointer<ByteVar>, inSize: size_t, inCount:
request's client certificate if it has one (so mTLS endpoints work too). request's client certificate if it has one (so mTLS endpoints work too).
inReq is already var-resolved. */ inReq is already var-resolved. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
fun inspectTlsChain(inReq: ApiRequest): TlsChain actual fun inspectTlsChain(inReq: ApiRequest): TlsChain
{ {
val vCert = try { if (inReq.hasClientCert) prepareClientCert(inReq) else null } val vCert = try { if (inReq.hasClientCert) prepareClientCert(inReq) else null }
catch (e: Throwable) { return TlsChain(emptyList(), e.message ?: "Client certificate error") } catch (e: Throwable) { return TlsChain(emptyList(), e.message ?: "Client certificate error") }
@@ -185,7 +174,7 @@ private fun readCertInfo(inCurl: COpaquePointer): List<List<Pair<String, String>
/* Send a client-certificate request through libcurl and adapt the result into /* Send a client-certificate request through libcurl and adapt the result into
the same ApiResponse the Ktor path produces. inReq is already var-resolved. */ the same ApiResponse the Ktor path produces. inReq is already var-resolved. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
{ {
val vMark = TimeSource.Monotonic.markNow() val vMark = TimeSource.Monotonic.markNow()
val vCert = try { prepareClientCert(inReq) } val vCert = try { prepareClientCert(inReq) }
+3 -13
View File
@@ -6,17 +6,10 @@ import com.compose.sdl.loadComposeResourceBytes
import com.compose.sdl.text.namedFontFamily import com.compose.sdl.text.namedFontFamily
// ================== // ==================
// MARK: Monospace body font // MARK: Monospace body font — native actuals (data.kres + IconFont)
// ================== // ==================
// Family name the body editor passes for monospace text. Resolves to the bundled actual val monoFontFamilyName: String? by lazy {
// Noto Sans Mono once registered; null means the font isn't in data.kres, in
// which case the body falls back to the default (proportional Noto Sans).
const val kMonoFamily = "noto-mono"
// The raw project family name string — used by the SyntaxHighlight tokeniser's
// wrap() call which measures via NativeTextMeasurer (accepts a name string).
val monoFontFamilyName: String? by lazy {
val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf") val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf")
if (vBytes == null) { if (vBytes == null) {
println("apidemo: NotoSansMono.ttf not bundled — body uses the default font") println("apidemo: NotoSansMono.ttf not bundled — body uses the default font")
@@ -27,9 +20,6 @@ val monoFontFamilyName: String? by lazy {
} }
} }
// The material3 Text / BasicTextField-shaped FontFamily wrapper. Wraps the actual val monoFontFamily: FontFamily? by lazy {
// registered project family in a `NamedFont`-backed FontFamily so the m3 Text
// signature (which takes `FontFamily?` proper) can be given the mono family.
val monoFontFamily: FontFamily? by lazy {
monoFontFamilyName?.let { namedFontFamily(it) } monoFontFamilyName?.let { namedFontFamily(it) }
} }
File diff suppressed because it is too large Load Diff
+41 -4
View File
@@ -5,11 +5,12 @@
// apply the multiplatform plugin. Each subproject still applies the ones it needs. // apply the multiplatform plugin. Each subproject still applies the ones it needs.
plugins { plugins {
alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.plugin.compose) apply false alias(libs.plugins.kotlin.plugin.compose) apply false
alias(libs.plugins.kotlin.plugin.serialization) apply false alias(libs.plugins.kotlin.plugin.serialization) apply false
alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.compose.multiplatform) apply false
// Public-ABI dumps (api/<module>.klib.api). Used by scripts/compose-fidelity-check.py // Public-ABI dumps (api/<module>.klib.api). Used by scripts/compose-coverage.py
// to diff our androidx.compose.* surface against the official Compose klib ABI. // to diff our mirrored API surface against the official upstream klib ABI dumps.
alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.binary.compatibility.validator)
} }
@@ -17,7 +18,8 @@ plugins {
// surface worth tracking, so skip them. // surface worth tracking, so skip them.
apiValidation { apiValidation {
klib { enabled = true } klib { enabled = true }
ignoredProjects.addAll(listOf("demo", "apidemo")) // compose-desktop-native-bridge is a plain JVM Gradle plugin — no klib ABI to track.
ignoredProjects.addAll(listOf("demo", "apidemo", "compose-desktop-native-bridge"))
} }
// Project coordinates. `group` is baked into every module's Kotlin/Native klib // Project coordinates. `group` is baked into every module's Kotlin/Native klib
@@ -47,6 +49,7 @@ allprojects {
val kAppModules = setOf(":demo", ":apidemo") val kAppModules = setOf(":demo", ":apidemo")
val kPublishedLibs = setOf( val kPublishedLibs = setOf(
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler", ":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
":ui-tooling-preview",
":animation-core", ":animation", ":animation-graphics", ":animation-core", ":animation", ":animation-graphics",
":foundation", ":foundation-layout", ":foundation", ":foundation-layout",
":material3", ":material-ripple", ":material3", ":material-ripple",
@@ -108,10 +111,44 @@ subprojects {
} }
} }
// FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL
// Maven Compose artifacts in its commonMain so metadata + jvm resolve them
// (e.g. :material-symbols' common API); every NATIVE target configuration
// swaps those modules for the port's project equivalents — the Maven
// artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is
// deliberately NOT here: the port uses the official runtime klibs everywhere.
// (:demo carries the same pattern locally for components-resources.)
val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64")
allprojects {
configurations.configureEach {
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
// ui's split artifacts: ui-graphics + ui-text are merged INTO
// :ui in the port; ui-unit / ui-geometry have their own modules.
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui"))
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui"))
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit"))
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":ui-tooling-preview"))
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":foundation"))
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":foundation-layout"))
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":animation"))
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":animation-core"))
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":material3"))
// navigation3-ui: the JB Maven artifact has no K/N desktop
// klibs — the port vendors it as :navigation3-ui.
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3-ui"))
}
}
}
}
// Whether the current host can build the mingwX64 target. Kotlin/Native can // Whether the current host can build the mingwX64 target. Kotlin/Native can
// only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf / // only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf /
// sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by // sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by
// scripts/build-sdl/build-*.sh, which only runs on Windows). Declaring `mingwX64()` on // scripts/build-sdl/build-all.py run on a Windows host). Declaring `mingwX64()` on
// a non-Windows host is safe for pure-Kotlin modules but blows up the moment // a non-Windows host is safe for pure-Kotlin modules but blows up the moment
// a `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h. // a `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h.
// Override with `-PforceMingw=true` if you actually have the headers wired. // Override with `-PforceMingw=true` if you actually have the headers wired.
@@ -0,0 +1,238 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:components-resources>
open annotation class org.jetbrains.compose.resources/ExperimentalResourceApi : kotlin/Annotation { // org.jetbrains.compose.resources/ExperimentalResourceApi|null[0]
constructor <init>() // org.jetbrains.compose.resources/ExperimentalResourceApi.<init>|<init>(){}[0]
}
open annotation class org.jetbrains.compose.resources/InternalResourceApi : kotlin/Annotation { // org.jetbrains.compose.resources/InternalResourceApi|null[0]
constructor <init>() // org.jetbrains.compose.resources/InternalResourceApi.<init>|<init>(){}[0]
}
open annotation class org.jetbrains.compose.resources/ResourceContentHash : kotlin/Annotation { // org.jetbrains.compose.resources/ResourceContentHash|null[0]
constructor <init>(kotlin/Int) // org.jetbrains.compose.resources/ResourceContentHash.<init>|<init>(kotlin.Int){}[0]
final val hash // org.jetbrains.compose.resources/ResourceContentHash.hash|{}hash[0]
final fun <get-hash>(): kotlin/Int // org.jetbrains.compose.resources/ResourceContentHash.hash.<get-hash>|<get-hash>(){}[0]
}
final enum class org.jetbrains.compose.resources/DensityQualifier : kotlin/Enum<org.jetbrains.compose.resources/DensityQualifier>, org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/DensityQualifier|null[0]
enum entry HDPI // org.jetbrains.compose.resources/DensityQualifier.HDPI|null[0]
enum entry LDPI // org.jetbrains.compose.resources/DensityQualifier.LDPI|null[0]
enum entry MDPI // org.jetbrains.compose.resources/DensityQualifier.MDPI|null[0]
enum entry XHDPI // org.jetbrains.compose.resources/DensityQualifier.XHDPI|null[0]
enum entry XXHDPI // org.jetbrains.compose.resources/DensityQualifier.XXHDPI|null[0]
enum entry XXXHDPI // org.jetbrains.compose.resources/DensityQualifier.XXXHDPI|null[0]
final val dpi // org.jetbrains.compose.resources/DensityQualifier.dpi|{}dpi[0]
final fun <get-dpi>(): kotlin/Int // org.jetbrains.compose.resources/DensityQualifier.dpi.<get-dpi>|<get-dpi>(){}[0]
final val entries // org.jetbrains.compose.resources/DensityQualifier.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<org.jetbrains.compose.resources/DensityQualifier> // org.jetbrains.compose.resources/DensityQualifier.entries.<get-entries>|<get-entries>#static(){}[0]
final fun valueOf(kotlin/String): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<org.jetbrains.compose.resources/DensityQualifier> // org.jetbrains.compose.resources/DensityQualifier.values|values#static(){}[0]
final object Companion { // org.jetbrains.compose.resources/DensityQualifier.Companion|null[0]
final fun selectByDensity(kotlin/Float): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.Companion.selectByDensity|selectByDensity(kotlin.Float){}[0]
final fun selectByValue(kotlin/Int): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.Companion.selectByValue|selectByValue(kotlin.Int){}[0]
}
}
final enum class org.jetbrains.compose.resources/ThemeQualifier : kotlin/Enum<org.jetbrains.compose.resources/ThemeQualifier>, org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/ThemeQualifier|null[0]
enum entry DARK // org.jetbrains.compose.resources/ThemeQualifier.DARK|null[0]
enum entry LIGHT // org.jetbrains.compose.resources/ThemeQualifier.LIGHT|null[0]
final val entries // org.jetbrains.compose.resources/ThemeQualifier.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<org.jetbrains.compose.resources/ThemeQualifier> // org.jetbrains.compose.resources/ThemeQualifier.entries.<get-entries>|<get-entries>#static(){}[0]
final fun valueOf(kotlin/String): org.jetbrains.compose.resources/ThemeQualifier // org.jetbrains.compose.resources/ThemeQualifier.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<org.jetbrains.compose.resources/ThemeQualifier> // org.jetbrains.compose.resources/ThemeQualifier.values|values#static(){}[0]
final object Companion { // org.jetbrains.compose.resources/ThemeQualifier.Companion|null[0]
final fun selectByValue(kotlin/Boolean): org.jetbrains.compose.resources/ThemeQualifier // org.jetbrains.compose.resources/ThemeQualifier.Companion.selectByValue|selectByValue(kotlin.Boolean){}[0]
}
}
abstract interface org.jetbrains.compose.resources/Qualifier // org.jetbrains.compose.resources/Qualifier|null[0]
abstract interface org.jetbrains.compose.resources/ResourceReader { // org.jetbrains.compose.resources/ResourceReader|null[0]
abstract fun getUri(kotlin/String): kotlin/String // org.jetbrains.compose.resources/ResourceReader.getUri|getUri(kotlin.String){}[0]
abstract suspend fun read(kotlin/String): kotlin/ByteArray // org.jetbrains.compose.resources/ResourceReader.read|read(kotlin.String){}[0]
abstract suspend fun readPart(kotlin/String, kotlin/Long, kotlin/Long): kotlin/ByteArray // org.jetbrains.compose.resources/ResourceReader.readPart|readPart(kotlin.String;kotlin.Long;kotlin.Long){}[0]
}
final class org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException : kotlin/Exception { // org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException|null[0]
constructor <init>(kotlin/String?) // org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException.<init>|<init>(kotlin.String?){}[0]
}
final class org.jetbrains.compose.resources/DrawableResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/DrawableResource|null[0]
constructor <init>(kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/DrawableResource.<init>|<init>(kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
}
final class org.jetbrains.compose.resources/FontResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/FontResource|null[0]
constructor <init>(kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/FontResource.<init>|<init>(kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
}
final class org.jetbrains.compose.resources/LanguageQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/LanguageQualifier|null[0]
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/LanguageQualifier.<init>|<init>(kotlin.String){}[0]
final val language // org.jetbrains.compose.resources/LanguageQualifier.language|{}language[0]
final fun <get-language>(): kotlin/String // org.jetbrains.compose.resources/LanguageQualifier.language.<get-language>|<get-language>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/LanguageQualifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/LanguageQualifier.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // org.jetbrains.compose.resources/LanguageQualifier.toString|toString(){}[0]
}
final class org.jetbrains.compose.resources/MissingResourceException : kotlin/Exception { // org.jetbrains.compose.resources/MissingResourceException|null[0]
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/MissingResourceException.<init>|<init>(kotlin.String){}[0]
}
final class org.jetbrains.compose.resources/PluralStringResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/PluralStringResource|null[0]
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/PluralStringResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
final val key // org.jetbrains.compose.resources/PluralStringResource.key|{}key[0]
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/PluralStringResource.key.<get-key>|<get-key>(){}[0]
}
final class org.jetbrains.compose.resources/RegionQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/RegionQualifier|null[0]
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/RegionQualifier.<init>|<init>(kotlin.String){}[0]
final val region // org.jetbrains.compose.resources/RegionQualifier.region|{}region[0]
final fun <get-region>(): kotlin/String // org.jetbrains.compose.resources/RegionQualifier.region.<get-region>|<get-region>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/RegionQualifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/RegionQualifier.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // org.jetbrains.compose.resources/RegionQualifier.toString|toString(){}[0]
}
final class org.jetbrains.compose.resources/ResourceEnvironment { // org.jetbrains.compose.resources/ResourceEnvironment|null[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ResourceEnvironment.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ResourceEnvironment.hashCode|hashCode(){}[0]
}
final class org.jetbrains.compose.resources/ResourceItem { // org.jetbrains.compose.resources/ResourceItem|null[0]
constructor <init>(kotlin.collections/Set<org.jetbrains.compose.resources/Qualifier>, kotlin/String, kotlin/Long, kotlin/Long) // org.jetbrains.compose.resources/ResourceItem.<init>|<init>(kotlin.collections.Set<org.jetbrains.compose.resources.Qualifier>;kotlin.String;kotlin.Long;kotlin.Long){}[0]
final fun copy(kotlin.collections/Set<org.jetbrains.compose.resources/Qualifier> = ..., kotlin/String = ..., kotlin/Long = ..., kotlin/Long = ...): org.jetbrains.compose.resources/ResourceItem // org.jetbrains.compose.resources/ResourceItem.copy|copy(kotlin.collections.Set<org.jetbrains.compose.resources.Qualifier>;kotlin.String;kotlin.Long;kotlin.Long){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ResourceItem.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ResourceItem.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // org.jetbrains.compose.resources/ResourceItem.toString|toString(){}[0]
}
final class org.jetbrains.compose.resources/ScriptQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/ScriptQualifier|null[0]
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/ScriptQualifier.<init>|<init>(kotlin.String){}[0]
final val script // org.jetbrains.compose.resources/ScriptQualifier.script|{}script[0]
final fun <get-script>(): kotlin/String // org.jetbrains.compose.resources/ScriptQualifier.script.<get-script>|<get-script>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ScriptQualifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ScriptQualifier.hashCode|hashCode(){}[0]
final fun isEmpty(): kotlin/Boolean // org.jetbrains.compose.resources/ScriptQualifier.isEmpty|isEmpty(){}[0]
final fun toString(): kotlin/String // org.jetbrains.compose.resources/ScriptQualifier.toString|toString(){}[0]
}
final class org.jetbrains.compose.resources/StringArrayResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/StringArrayResource|null[0]
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/StringArrayResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
final val key // org.jetbrains.compose.resources/StringArrayResource.key|{}key[0]
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/StringArrayResource.key.<get-key>|<get-key>(){}[0]
}
final class org.jetbrains.compose.resources/StringResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/StringResource|null[0]
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/StringResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
final val key // org.jetbrains.compose.resources/StringResource.key|{}key[0]
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/StringResource.key.<get-key>|<get-key>(){}[0]
}
sealed class org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/Resource|null[0]
open fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/Resource.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/Resource.hashCode|hashCode(){}[0]
}
final object org.jetbrains.compose.resources/ResourceCaches // org.jetbrains.compose.resources/ResourceCaches|null[0]
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop[0]
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop[0]
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop[0]
final val org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop // org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop|#static{}org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop[0]
final val org.jetbrains.compose.resources/LocalResourceReader // org.jetbrains.compose.resources/LocalResourceReader|{}LocalResourceReader[0]
final fun <get-LocalResourceReader>(): androidx.compose.runtime/ProvidableCompositionLocal<org.jetbrains.compose.resources/ResourceReader> // org.jetbrains.compose.resources/LocalResourceReader.<get-LocalResourceReader>|<get-LocalResourceReader>(){}[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop|#static{}org_jetbrains_compose_resources_DrawableResource$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop|#static{}org_jetbrains_compose_resources_FontResource$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Svg$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Vector$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop|#static{}org_jetbrains_compose_resources_LanguageQualifier$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop|#static{}org_jetbrains_compose_resources_MissingResourceException$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop|#static{}org_jetbrains_compose_resources_PluralStringResource$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop|#static{}org_jetbrains_compose_resources_RegionQualifier$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop|#static{}org_jetbrains_compose_resources_Resource$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop|#static{}org_jetbrains_compose_resources_ResourceCaches$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop|#static{}org_jetbrains_compose_resources_ResourceEnvironment$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop|#static{}org_jetbrains_compose_resources_ResourceItem$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop|#static{}org_jetbrains_compose_resources_ScriptQualifier$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop|#static{}org_jetbrains_compose_resources_StringArrayResource$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Array$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Plurals$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Value$stableprop[0]
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop|#static{}org_jetbrains_compose_resources_StringResource$stableprop[0]
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // org.jetbrains.compose.resources/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0]
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToImageVector(androidx.compose.ui.unit/Density): androidx.compose.ui.graphics.vector/ImageVector // org.jetbrains.compose.resources/decodeToImageVector|decodeToImageVector@kotlin.ByteArray(androidx.compose.ui.unit.Density){}[0]
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToSvgPainter(androidx.compose.ui.unit/Density): androidx.compose.ui.graphics.painter/Painter // org.jetbrains.compose.resources/decodeToSvgPainter|decodeToSvgPainter@kotlin.ByteArray(androidx.compose.ui.unit.Density){}[0]
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter|org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/Font(org.jetbrains.compose.resources/FontResource, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontStyle, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.ui.text.font/Font // org.jetbrains.compose.resources/Font|Font(org.jetbrains.compose.resources.FontResource;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontStyle;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/Font(org.jetbrains.compose.resources/FontResource, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontStyle, androidx.compose.ui.text.font/FontVariation.Settings?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.ui.text.font/Font // org.jetbrains.compose.resources/Font|Font(org.jetbrains.compose.resources.FontResource;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/getResourceUri(kotlin/String): kotlin/String // org.jetbrains.compose.resources/getResourceUri|getResourceUri(kotlin.String){}[0]
final fun org.jetbrains.compose.resources/getSystemResourceEnvironment(): org.jetbrains.compose.resources/ResourceEnvironment // org.jetbrains.compose.resources/getSystemResourceEnvironment|getSystemResourceEnvironment(){}[0]
final fun org.jetbrains.compose.resources/imageResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/ImageBitmap // org.jetbrains.compose.resources/imageResource|imageResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop_getter|org_jetbrains_compose_resources_DrawableResource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop_getter|org_jetbrains_compose_resources_FontResource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter|org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop_getter|org_jetbrains_compose_resources_MissingResourceException$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop_getter|org_jetbrains_compose_resources_PluralStringResource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop_getter|org_jetbrains_compose_resources_RegionQualifier$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop_getter|org_jetbrains_compose_resources_Resource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop_getter|org_jetbrains_compose_resources_ResourceCaches$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter|org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop_getter|org_jetbrains_compose_resources_ResourceItem$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter|org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop_getter|org_jetbrains_compose_resources_StringArrayResource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop_getter|org_jetbrains_compose_resources_StringItem_Array$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter|org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop_getter|org_jetbrains_compose_resources_StringItem_Value$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop_getter|org_jetbrains_compose_resources_StringResource$stableprop_getter(){}[0]
final fun org.jetbrains.compose.resources/painterResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics.painter/Painter // org.jetbrains.compose.resources/painterResource|painterResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/pluralStringResource(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/pluralStringResource|pluralStringResource(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/pluralStringResource(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>..., androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/pluralStringResource|pluralStringResource(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/rememberResourceEnvironment(androidx.compose.runtime/Composer?, kotlin/Int): org.jetbrains.compose.resources/ResourceEnvironment // org.jetbrains.compose.resources/rememberResourceEnvironment|rememberResourceEnvironment(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/stringArrayResource(org.jetbrains.compose.resources/StringArrayResource, androidx.compose.runtime/Composer?, kotlin/Int): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/stringArrayResource|stringArrayResource(org.jetbrains.compose.resources.StringArrayResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/stringResource(org.jetbrains.compose.resources/StringResource, androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/stringResource|stringResource(org.jetbrains.compose.resources.StringResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/stringResource(org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>..., androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/stringResource|stringResource(org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun org.jetbrains.compose.resources/vectorResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics.vector/ImageVector // org.jetbrains.compose.resources/vectorResource|vectorResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final suspend fun org.jetbrains.compose.resources/getDrawableResourceBytes(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/DrawableResource): kotlin/ByteArray // org.jetbrains.compose.resources/getDrawableResourceBytes|getDrawableResourceBytes(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.DrawableResource){}[0]
final suspend fun org.jetbrains.compose.resources/getFontResourceBytes(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/FontResource): kotlin/ByteArray // org.jetbrains.compose.resources/getFontResourceBytes|getFontResourceBytes(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.FontResource){}[0]
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int){}[0]
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...){}[0]
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/PluralStringResource, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.PluralStringResource;kotlin.Int){}[0]
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...){}[0]
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringResource): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringResource){}[0]
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...){}[0]
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/StringResource): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.StringResource){}[0]
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...){}[0]
final suspend fun org.jetbrains.compose.resources/getStringArray(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringArrayResource): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/getStringArray|getStringArray(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringArrayResource){}[0]
final suspend fun org.jetbrains.compose.resources/getStringArray(org.jetbrains.compose.resources/StringArrayResource): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/getStringArray|getStringArray(org.jetbrains.compose.resources.StringArrayResource){}[0]
final suspend fun org.jetbrains.compose.resources/readResourceBytes(kotlin/String): kotlin/ByteArray // org.jetbrains.compose.resources/readResourceBytes|readResourceBytes(kotlin.String){}[0]
@@ -16,6 +16,8 @@ package org.jetbrains.compose.resources.vector.xmldom
CDATA skipping, and the five predefined entities plus numeric character CDATA skipping, and the five predefined entities plus numeric character
references. No DTD expansion, no processing-instruction handling beyond references. No DTD expansion, no processing-instruction handling beyond
skipping — malformed input throws MalformedXMLException like upstream. */ skipping — malformed input throws MalformedXMLException like upstream. */
// VENDOR-BASE(COMPOSE_REF): components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @ v1.12.0-beta03+dev4483
// (fresh REIMPL, not a copy-edit — the base ref marks the upstream API it tracks)
internal fun parse(xml: String): Element { internal fun parse(xml: String): Element {
val vParser = XmlDomParser(xml) val vParser = XmlDomParser(xml)
@@ -21,7 +21,7 @@ import com.compose.sdl.graphics.decodeEncodedImageBitmap
drawables ship at a single density, so no decode-time rescale applies. */ drawables ship at a single density, so no decode-time rescale applies. */
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap = internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
decodeEncodedImageBitmap(this) decodeEncodedImageBitmap(this)
?: error("Image decode failed — is the SDL renderer initialised before painterResource ran?") ?: error("Image decode failed — is the render backend initialised before painterResource ran?")
/* SVG on SDL: SDL3_image rasterises SVG at its intrinsic size (IMG_LoadSVG), /* SVG on SDL: SDL3_image rasterises SVG at its intrinsic size (IMG_LoadSVG),
so the "element" is just the raw bytes and the painter is the rasterised so the "element" is just the raw bytes and the painter is the rasterised
@@ -34,5 +34,5 @@ internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
internal actual fun SvgElement.toSvgPainter(density: Density): Painter = internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
BitmapPainter( BitmapPainter(
decodeEncodedImageBitmap(bytes) decodeEncodedImageBitmap(bytes)
?: error("SVG rasterisation failed — is the SDL renderer initialised before painterResource ran?") ?: error("SVG rasterisation failed — is the render backend initialised before painterResource ran?")
) )
@@ -2078,17 +2078,6 @@ final object com.compose.sdl.icons/IconDefaults { // com.compose.sdl.icons/IconD
final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0] final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0]
} }
final object com.compose.sdl.icons/MaterialIconAxisDefaults { // com.compose.sdl.icons/MaterialIconAxisDefaults|null[0]
final const val Fill // com.compose.sdl.icons/MaterialIconAxisDefaults.Fill|{}Fill[0]
final fun <get-Fill>(): kotlin/Float // com.compose.sdl.icons/MaterialIconAxisDefaults.Fill.<get-Fill>|<get-Fill>(){}[0]
final const val Grade // com.compose.sdl.icons/MaterialIconAxisDefaults.Grade|{}Grade[0]
final fun <get-Grade>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.Grade.<get-Grade>|<get-Grade>(){}[0]
final const val OpticalSize // com.compose.sdl.icons/MaterialIconAxisDefaults.OpticalSize|{}OpticalSize[0]
final fun <get-OpticalSize>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.OpticalSize.<get-OpticalSize>|<get-OpticalSize>(){}[0]
final const val Weight // com.compose.sdl.icons/MaterialIconAxisDefaults.Weight|{}Weight[0]
final fun <get-Weight>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.Weight.<get-Weight>|<get-Weight>(){}[0]
}
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0] final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0] final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0] final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
@@ -2220,7 +2209,6 @@ final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefault
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0] final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0]
final val com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop|#static{}com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop[0]
final val com.compose.sdl.scrollbar/LocalScrollbarStyle // com.compose.sdl.scrollbar/LocalScrollbarStyle|{}LocalScrollbarStyle[0] final val com.compose.sdl.scrollbar/LocalScrollbarStyle // com.compose.sdl.scrollbar/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.scrollbar/ScrollbarStyle> // com.compose.sdl.scrollbar/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0] final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.scrollbar/ScrollbarStyle> // com.compose.sdl.scrollbar/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
final val com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop|#static{}com_compose_sdl_scrollbar_ScrollbarStyle$stableprop[0] final val com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop|#static{}com_compose_sdl_scrollbar_ScrollbarStyle$stableprop[0]
@@ -2294,6 +2282,8 @@ final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foun
final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foundation.gestures/calculateZoom(): kotlin/Float // androidx.compose.foundation.gestures/calculateZoom|calculateZoom@androidx.compose.ui.input.pointer.PointerEvent(){}[0] final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foundation.gestures/calculateZoom(): kotlin/Float // androidx.compose.foundation.gestures/calculateZoom|calculateZoom@androidx.compose.ui.input.pointer.PointerEvent(){}[0]
final fun (androidx.compose.ui.text/AnnotatedString.Builder).androidx.compose.foundation.text/appendInlineContent(kotlin/String, kotlin/String = ...) // androidx.compose.foundation.text/appendInlineContent|appendInlineContent@androidx.compose.ui.text.AnnotatedString.Builder(kotlin.String;kotlin.String){}[0] final fun (androidx.compose.ui.text/AnnotatedString.Builder).androidx.compose.foundation.text/appendInlineContent(kotlin/String, kotlin/String = ...) // androidx.compose.foundation.text/appendInlineContent|appendInlineContent@androidx.compose.ui.text.AnnotatedString.Builder(kotlin.String;kotlin.String){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.content/contentReceiver(androidx.compose.foundation.content/ReceiveContentListener): androidx.compose.ui/Modifier // androidx.compose.foundation.content/contentReceiver|contentReceiver@androidx.compose.ui.Modifier(androidx.compose.foundation.content.ReceiveContentListener){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.content/contentReceiver(androidx.compose.foundation.content/ReceiveContentListener): androidx.compose.ui/Modifier // androidx.compose.foundation.content/contentReceiver|contentReceiver@androidx.compose.ui.Modifier(androidx.compose.foundation.content.ReceiveContentListener){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropSource(kotlin/Function1<androidx.compose.ui.geometry/Offset, androidx.compose.ui.draganddrop/DragAndDropTransferData?>): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropSource|dragAndDropSource@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?>){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropSource(kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, kotlin/Function1<androidx.compose.ui.geometry/Offset, androidx.compose.ui.draganddrop/DragAndDropTransferData?>): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropSource|dragAndDropSource@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?>){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropTarget(kotlin/Function1<androidx.compose.ui.draganddrop/DragAndDropEvent, kotlin/Boolean>, androidx.compose.ui.draganddrop/DragAndDropTarget): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropTarget|dragAndDropTarget@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.draganddrop.DragAndDropEvent,kotlin.Boolean>;androidx.compose.ui.draganddrop.DragAndDropTarget){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropTarget(kotlin/Function1<androidx.compose.ui.draganddrop/DragAndDropEvent, kotlin/Boolean>, androidx.compose.ui.draganddrop/DragAndDropTarget): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropTarget|dragAndDropTarget@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.draganddrop.DragAndDropEvent,kotlin.Boolean>;androidx.compose.ui.draganddrop.DragAndDropTarget){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable(androidx.compose.foundation.gestures/DraggableState, androidx.compose.foundation.gestures/Orientation, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, kotlin/Float, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable|draggable@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.DraggableState;androidx.compose.foundation.gestures.Orientation;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,kotlin.Float,kotlin.Unit>;kotlin.Boolean){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable(androidx.compose.foundation.gestures/DraggableState, androidx.compose.foundation.gestures/Orientation, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, kotlin/Float, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable|draggable@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.DraggableState;androidx.compose.foundation.gestures.Orientation;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,kotlin.Float,kotlin.Unit>;kotlin.Boolean){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable2D(androidx.compose.foundation.gestures/Draggable2DState, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin/Function1<androidx.compose.ui.unit/Velocity, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable2D|draggable2D@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.Draggable2DState;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.unit.Velocity,kotlin.Unit>;kotlin.Boolean){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable2D(androidx.compose.foundation.gestures/Draggable2DState, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin/Function1<androidx.compose.ui.unit/Velocity, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable2D|draggable2D@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.Draggable2DState;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.unit.Velocity,kotlin.Unit>;kotlin.Boolean){}[0]
@@ -2620,14 +2610,13 @@ final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean,
final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.icons/MaterialIconAxes(kotlin/Float = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ...): kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting> // com.compose.sdl.icons/MaterialIconAxes|MaterialIconAxes(kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0] final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0]
final fun com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter|com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter(){}[0]
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/HorizontalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/HorizontalScrollbar|HorizontalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.scrollbar/HorizontalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/HorizontalScrollbar|HorizontalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/VerticalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/VerticalScrollbar|VerticalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.scrollbar/VerticalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/VerticalScrollbar|VerticalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(): kotlin/Int // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter|com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(){}[0] final fun com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(): kotlin/Int // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter|com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(){}[0]
final fun com.compose.sdl.scrollbar/defaultScrollbarStyle(): com.compose.sdl.scrollbar/ScrollbarStyle // com.compose.sdl.scrollbar/defaultScrollbarStyle|defaultScrollbarStyle(){}[0] final fun com.compose.sdl.scrollbar/defaultScrollbarStyle(): com.compose.sdl.scrollbar/ScrollbarStyle // com.compose.sdl.scrollbar/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
@@ -18,6 +18,7 @@
// MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/ // MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/
// androidx/compose/foundation/text/selection/SelectionLayout.kt. // androidx/compose/foundation/text/selection/SelectionLayout.kt.
// VENDOR-BASE: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt @ v1.12.0-beta03+dev4483
// //
// Change vs upstream: MultiSelectionLayout.crossStatus and slotToIndex clamp their // Change vs upstream: MultiSelectionLayout.crossStatus and slotToIndex clamp their
// infoList index to the valid range. Upstream assumes that if the drag position never // infoList index to the valid range. Upstream assumes that if the drag position never
@@ -30,9 +30,12 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
// ================== // ==================
@@ -141,6 +144,19 @@ private class LazyListScrollbarAdapter(private val state: LazyListState) : Scrol
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble() get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
override suspend fun scrollTo(scrollOffset: Double) { override suspend fun scrollTo(scrollOffset: Double) {
val vDistance = scrollOffset - this.scrollOffset
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
// list's pixel geometry is only ESTIMATED (average visible item size), and
// that estimate drifts as differently-sized items scroll through the
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
// sidesteps the estimate entirely. This is what Compose Desktop's own
// LazyListScrollbarAdapter does, and for the same reason.
if (abs(vDistance) <= viewportSize) {
state.scrollBy(vDistance.toFloat())
return
}
// Far jump (track-click paging past a viewport): no exact geometry to lean
// on, so estimate an index/offset from the average and snap to it.
val vAvg = averageItemSize() val vAvg = averageItemSize()
if (vAvg <= 0.0) return if (vAvg <= 0.0) return
val vTarget = scrollOffset.coerceAtLeast(0.0) val vTarget = scrollOffset.coerceAtLeast(0.0)
@@ -193,7 +209,6 @@ private fun Scrollbar(
val vHoverSource = remember { MutableInteractionSource() } val vHoverSource = remember { MutableInteractionSource() }
val vHovered by vHoverSource.collectIsHoveredAsState() val vHovered by vHoverSource.collectIsHoveredAsState()
var vDraggingThumb by remember { mutableStateOf(false) } var vDraggingThumb by remember { mutableStateOf(false) }
var vGrab by remember { mutableStateOf(0) }
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every // onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in // int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
// physical pixels. Passing them into `.dp` doubles again through // physical pixels. Passing them into `.dp` doubles again through
@@ -230,15 +245,6 @@ private fun Scrollbar(
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt() val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel) val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
// ============
// Map a track-relative position to a scroll offset and apply it.
fun scrollToTrackPos(inThumbTop: Int) {
if (vTravel <= 0) return
val vPos = inThumbTop.coerceIn(0, vTravel)
val vFrac = (if (reverseLayout) vTravel - vPos else vPos).toDouble() / vTravel
vScope.launch { adapter.scrollTo(vFrac * vMaxScroll) }
}
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() } val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
val vThumbMod = ( val vThumbMod = (
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp) if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
@@ -251,33 +257,78 @@ private fun Scrollbar(
// ============ // ============
// Drag the thumb / page on track click — handled on the track so the // Drag the thumb / page on track click — handled on the track so the
// coordinates have a stable (non-moving) origin. // coordinates have a stable (non-moving) origin. The press is CONSUMED:
// a scrollbar owns the pointer input it handles, so content sharing this
// region (the scrollable beneath, or a parent that also reads pointer
// input) never reacts to the same press. Geometry is read LIVE from the
// adapter at press time — not from the captured render metrics above,
// which go stale between pointerInput relaunches — so a grab after a
// wheel-scroll still hit-tests against the thumb's current position.
Box( Box(
modifier = Modifier modifier = Modifier
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness) .matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
.pointerInput(isVertical, adapter, vTrackPx) { .pointerInput(isVertical, adapter, vTrackPx, reverseLayout) {
detectDragGestures( awaitEachGesture {
onDragStart = { offset -> val vDown = awaitFirstDown(requireUnconsumed = false)
val vPress = (if (isVertical) offset.y else offset.x).toInt() vDown.consume()
if (vPress in vThumbPos..(vThumbPos + vThumbLen)) {
// Live geometry — stable for the drag's duration (content /
// viewport / track sizes don't change mid-drag).
val vContentPx = adapter.contentSize
val vViewportPx = adapter.viewportSize
val vMaxScrollPx = vContentPx - vViewportPx
if (vMaxScrollPx <= 0.0 || vTrackPx <= 0) return@awaitEachGesture
val vMinLen = style.minimalHeight.value.toInt()
val vLen = (vTrackPx * (vViewportPx / vContentPx)).roundToInt()
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
val vTravelPx = vTrackPx - vLen
val vRaw = if (vTravelPx <= 0) 0
else (vTravelPx * (adapter.scrollOffset / vMaxScrollPx)).roundToInt()
val vPos = (if (reverseLayout) vTravelPx - vRaw else vRaw).coerceIn(0, vTravelPx)
// Map a track-relative thumb-top to a scroll offset. The
// target uses the LIVE max scroll (read fresh each move),
// not the press-time value — the adapter compares it against
// its live scrollOffset, and mixing a stale scale with a live
// one makes the delta run away (grab at the bottom, drag up,
// and the list overshoots to the middle).
fun scrollToTrackPos(inThumbTop: Int) {
if (vTravelPx <= 0) return
val vLiveMax = adapter.contentSize - adapter.viewportSize
if (vLiveMax <= 0.0) return
val vClamped = inThumbTop.coerceIn(0, vTravelPx)
val vFrac = (if (reverseLayout) vTravelPx - vClamped else vClamped).toDouble() / vTravelPx
vScope.launch { adapter.scrollTo(vFrac * vLiveMax) }
}
val vPress = (if (isVertical) vDown.position.y else vDown.position.x).toInt()
val vGrab: Int
if (vPress in vPos..(vPos + vLen)) {
vDraggingThumb = true vDraggingThumb = true
vGrab = vPress - vThumbPos vGrab = vPress - vPos
} else { } else {
// Page toward the click by one viewport. // Page toward the click by one viewport.
vDraggingThumb = false vDraggingThumb = false
val vDir = if (vPress < vThumbPos) -1.0 else 1.0 vGrab = 0
vScope.launch { adapter.scrollTo(vOffset + vDir * vViewport) } val vDir = if (vPress < vPos) -1.0 else 1.0
vScope.launch { adapter.scrollTo(adapter.scrollOffset + vDir * vViewportPx) }
} }
},
onDrag = { change, _ -> // Follow the pressed pointer to release, consuming every
// change so the interaction can't leak. Only a thumb grab
// drags-to-scroll; a track press just consumes.
do {
val vEvent = awaitPointerEvent()
val vChange = vEvent.changes.firstOrNull { it.id == vDown.id } ?: break
if (vDraggingThumb) { if (vDraggingThumb) {
val vCur = (if (isVertical) change.position.y else change.position.x).toInt() val vCur = (if (isVertical) vChange.position.y else vChange.position.x).toInt()
scrollToTrackPos(vCur - vGrab) scrollToTrackPos(vCur - vGrab)
} }
}, vChange.consume()
onDragEnd = { vDraggingThumb = false }, } while (vEvent.changes.any { it.id == vDown.id && it.pressed })
onDragCancel = { vDraggingThumb = false },
) vDraggingThumb = false
}
} }
) )
} }
@@ -0,0 +1,42 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:material-ripple>
abstract interface androidx.compose.material.ripple/RippleTheme { // androidx.compose.material.ripple/RippleTheme|null[0]
abstract fun defaultColor(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color // androidx.compose.material.ripple/RippleTheme.defaultColor|defaultColor(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
abstract fun rippleAlpha(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.material.ripple/RippleAlpha // androidx.compose.material.ripple/RippleTheme.rippleAlpha|rippleAlpha(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final object Companion { // androidx.compose.material.ripple/RippleTheme.Companion|null[0]
final fun defaultRippleAlpha(androidx.compose.ui.graphics/Color, kotlin/Boolean): androidx.compose.material.ripple/RippleAlpha // androidx.compose.material.ripple/RippleTheme.Companion.defaultRippleAlpha|defaultRippleAlpha(androidx.compose.ui.graphics.Color;kotlin.Boolean){}[0]
final fun defaultRippleColor(androidx.compose.ui.graphics/Color, kotlin/Boolean): androidx.compose.ui.graphics/Color // androidx.compose.material.ripple/RippleTheme.Companion.defaultRippleColor|defaultRippleColor(androidx.compose.ui.graphics.Color;kotlin.Boolean){}[0]
}
}
final class androidx.compose.material.ripple/RippleAlpha { // androidx.compose.material.ripple/RippleAlpha|null[0]
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // androidx.compose.material.ripple/RippleAlpha.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
final val draggedAlpha // androidx.compose.material.ripple/RippleAlpha.draggedAlpha|{}draggedAlpha[0]
final fun <get-draggedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.draggedAlpha.<get-draggedAlpha>|<get-draggedAlpha>(){}[0]
final val focusedAlpha // androidx.compose.material.ripple/RippleAlpha.focusedAlpha|{}focusedAlpha[0]
final fun <get-focusedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.focusedAlpha.<get-focusedAlpha>|<get-focusedAlpha>(){}[0]
final val hoveredAlpha // androidx.compose.material.ripple/RippleAlpha.hoveredAlpha|{}hoveredAlpha[0]
final fun <get-hoveredAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.hoveredAlpha.<get-hoveredAlpha>|<get-hoveredAlpha>(){}[0]
final val pressedAlpha // androidx.compose.material.ripple/RippleAlpha.pressedAlpha|{}pressedAlpha[0]
final fun <get-pressedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.pressedAlpha.<get-pressedAlpha>|<get-pressedAlpha>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.material.ripple/RippleAlpha.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // androidx.compose.material.ripple/RippleAlpha.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // androidx.compose.material.ripple/RippleAlpha.toString|toString(){}[0]
}
final val androidx.compose.material.ripple/LocalRippleTheme // androidx.compose.material.ripple/LocalRippleTheme|{}LocalRippleTheme[0]
final fun <get-LocalRippleTheme>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.material.ripple/RippleTheme> // androidx.compose.material.ripple/LocalRippleTheme.<get-LocalRippleTheme>|<get-LocalRippleTheme>(){}[0]
final val androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop // androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop|#static{}androidx_compose_material_ripple_RippleAlpha$stableprop[0]
final fun androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop_getter(): kotlin/Int // androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop_getter|androidx_compose_material_ripple_RippleAlpha$stableprop_getter(){}[0]
final fun androidx.compose.material.ripple/createRippleModifierNode(androidx.compose.foundation.interaction/InteractionSource, kotlin/Boolean, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/ColorProducer, kotlin/Function0<androidx.compose.material.ripple/RippleAlpha>): androidx.compose.ui.node/DelegatableNode // androidx.compose.material.ripple/createRippleModifierNode|createRippleModifierNode(androidx.compose.foundation.interaction.InteractionSource;kotlin.Boolean;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.ColorProducer;kotlin.Function0<androidx.compose.material.ripple.RippleAlpha>){}[0]
final fun androidx.compose.material.ripple/rememberRipple(kotlin/Boolean, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/Indication // androidx.compose.material.ripple/rememberRipple|rememberRipple(kotlin.Boolean;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
File diff suppressed because it is too large Load Diff
@@ -32,9 +32,13 @@ import com.compose.sdl.window.createPopupHostState
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import kotlinx.cinterop.toKString
import kotlinx.cinterop.reinterpret import kotlinx.cinterop.reinterpret
import sdl3.SDL_Delay import sdl3.SDL_Delay
import sdl3.SDL_GetPerformanceCounter
import sdl3.SDL_GetPerformanceFrequency
import sdl3.SDL_GetTicks import sdl3.SDL_GetTicks
import sdl3.SDL_GetTicksNS
import sdl3.SDL_Quit import sdl3.SDL_Quit
import sdl3.SDL_SetWindowTitle import sdl3.SDL_SetWindowTitle
import sdl3.SDL_WaitEventTimeout import sdl3.SDL_WaitEventTimeout
@@ -141,12 +145,18 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
appComposition.setContent { appScope.content() } appComposition.setContent { appScope.content() }
// Compose the initial Window() declarations before entering the loop. // Compose the initial Window() declarations before entering the loop.
appClock.sendFrame() appClock.sendFrame(frameClockNanos())
yield() yield()
// ============ // ============
// Main loop // Main loop
var vGcLastTicks = SDL_GetTicks()
var vRenderedSinceGc = false
while (!runtime.exitRequested) { while (!runtime.exitRequested) {
FrameProfiler.mark()
// One virtual 16.6ms tick per loop iteration (no-op unless useVirtualFrameTime) —
// all windows' clocks share the same timestamp this iteration.
advanceVirtualFrame()
Snapshot.sendApplyNotifications() Snapshot.sendApplyNotifications()
// ============ // ============
@@ -174,11 +184,12 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
} }
mainDispatcher.drainPending() mainDispatcher.drainPending()
FrameProfiler.phase("events")
// ============ // ============
// App composition pump — Window()s may appear / disappear here. // App composition pump — Window()s may appear / disappear here.
Snapshot.sendApplyNotifications() Snapshot.sendApplyNotifications()
appClock.sendFrame() appClock.sendFrame(frameClockNanos())
yield() yield()
runtime.reapDestroyed() runtime.reapDestroyed()
@@ -193,6 +204,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
// Exit when the last window is gone (after at least one existed). // Exit when the last window is gone (after at least one existed).
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
if (runtime.exitRequested) break if (runtime.exitRequested) break
FrameProfiler.phase("app")
// ============ // ============
// Per-window pump + render. // Per-window pump + render.
@@ -201,18 +213,29 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
val vAppPending = appRecomposer.hasPendingWork val vAppPending = appRecomposer.hasPendingWork
for (vW in runtime.windows.toList()) { for (vW in runtime.windows.toList()) {
vW.installGlobals() vW.installGlobals()
vW.host.sendAnimationFrame(SDL_GetTicks().toLong() * 1_000_000L) vW.host.sendAnimationFrame(animationClockNanos())
mainDispatcher.drainPending() mainDispatcher.drainPending()
Snapshot.sendApplyNotifications() Snapshot.sendApplyNotifications()
vW.frameClock.sendFrame() vW.frameClock.sendFrame(frameClockNanos())
yield() yield()
FrameProfiler.phase("pump")
if (vW.shouldRender()) { if (vW.shouldRender()) {
vW.renderFrame() vW.renderFrame()
vAnyRendered = true vAnyRendered = true
if (!vW.backend.vsyncEnabled) vAllVsync = false if (!vW.backend.vsyncEnabled) vAllVsync = false
} }
FrameProfiler.phase("render")
} }
runtime.reapDestroyed() runtime.reapDestroyed()
FrameProfiler.frameDone(vAnyRendered)
// ============
// Drain deferred native-resource disposals on the MAIN thread —
// textures/surfaces whose owner closed them or whose Cleaner fired
// on a GC worker enqueue here (SDL calls aren't thread-safe). This
// is the ownership path that makes the GC nudge below a mere
// backstop (ROADMAP.md item 1).
com.compose.sdl.graphics.NativeReleaseQueue.drain()
// ============ // ============
// Pace / idle-skip. // Pace / idle-skip.
@@ -224,6 +247,23 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
SDL_WaitEventTimeout(null, 10) SDL_WaitEventTimeout(null, 10)
for (vW in runtime.windows) vW.resetFpsWindow() for (vW in runtime.windows) vW.resetFpsWindow()
} }
// ============
// Native-memory nudge. Renderer resources (Skia surfaces / images /
// fonts, SDL textures) are freed by Cleaners that only run when the
// Kotlin/Native GC collects — and a Compose app's Kotlin heap is
// small enough that the allocation-driven scheduler can starve them
// for minutes while the NATIVE heap balloons (issue #2: memory
// "never released" while navigating). Collect periodically, only
// after rendering activity, between frames; costs ~ms at these
// heap sizes and keeps RSS tracking real usage.
if (vAnyRendered) vRenderedSinceGc = true
val vNowTicks = SDL_GetTicks()
if (vRenderedSinceGc && vNowTicks - vGcLastTicks >= 10_000uL) {
vGcLastTicks = vNowTicks
vRenderedSinceGc = false
collectNativeGarbage()
}
} }
// ============ // ============
@@ -231,6 +271,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
snapshotHandle.dispose() snapshotHandle.dispose()
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW) for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
runtime.reapDestroyed() runtime.reapDestroyed()
com.compose.sdl.graphics.NativeReleaseQueue.drain()
appComposition.dispose() appComposition.dispose()
appRecomposer.cancel() appRecomposer.cancel()
appRecomposeJob.cancelAndJoin() appRecomposeJob.cancelAndJoin()
@@ -243,6 +284,132 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
SDL_Quit() SDL_Quit()
} }
/* CDN_PROFILE=1 — per-phase timings, printed every ~2s of rendered frames.
A named-phase SINGLETON so both the main loop (events / app / pump / render)
AND renderFrame's sub-steps (render.layout / render.draw / render.present)
report into one line. `mark()` resets the stopwatch; `phase(name)` charges
the elapsed since the last mark/phase to that name. Measure first, optimize
second — see ROADMAP.md. */
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
internal object FrameProfiler {
// null until first checked; then true/false for the run's lifetime.
private var fEnabled: Boolean? = null
// Output file — resolved once from CDN_PROFILE. Writing to a file (not
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
// the cwd; CDN_PROFILE=<path> → that path.
private var fPath: String = "cdn_profile.log"
val enabled: Boolean
get() = fEnabled ?: run {
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv
(vEnv != null).also { fEnabled = it }
}
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
// Insertion-ordered so the printed line follows the call order.
private val fSum = LinkedHashMap<String, Double>()
private val fMax = LinkedHashMap<String, Double>()
private var fFrames = 0
private var fLastPrintMs = SDL_GetTicks()
private var fMark = 0uL
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() }
fun phase(inName: String) {
if (!enabled) return
val vNow = SDL_GetPerformanceCounter()
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
fSum[inName] = (fSum[inName] ?: 0.0) + vMs
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs
fMark = vNow
}
fun frameDone(inRendered: Boolean) {
if (!enabled) return
if (inRendered) fFrames++
val vNowMs = SDL_GetTicks()
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
val vParts = fSum.keys.map { vName ->
val vAvg = (fSum[vName] ?: 0.0) / fFrames
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
}
// Per-frame draw-work averages (see DrawStats): what's inside `draw`.
val vStats = com.compose.sdl.graphics.DrawStats
val vDraw = "geo=${vStats.geometrySubmits / fFrames} verts=${vStats.vertices / fFrames} " +
"masks=${vStats.maskRealizations / fFrames} text=${vStats.textDraws / fFrames} img=${vStats.imageBlits / fFrames}"
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + " | per-frame " + vDraw + "\n"
val vFile = platform.posix.fopen(fPath, "a")
if (vFile != null) {
platform.posix.fputs(vLine, vFile)
platform.posix.fclose(vFile)
}
vStats.reset()
fSum.clear(); fMax.clear()
fFrames = 0
fLastPrintMs = vNowMs
}
}
}
/* Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
their native memory (see the main loop's native-memory nudge). */
private val kForceRender: Boolean = platform.posix.getenv("CDN_FORCERENDER") != null
// ==================
// MARK: Probe / screenshot support (render-to-quiescence)
// ==================
/* Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
rememberInfiniteTransition & co. freeze at their initial value — the same mechanism
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
reach quiescence and capture deterministically. */
var disableInfiniteAnimations: Boolean = false
/* The cancelling policy: a coroutine that ends in CancellationException counts as
cancelled (not failed), so only the animation coroutine stops — nothing propagates. */
private object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
}
/* Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
real-time ticks — the native mirror of the JVM parity leg's render(nanos) stepping.
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
screenshots become deterministic. Input timestamps and FPS stay on real time. */
var useVirtualFrameTime: Boolean = false
private var virtualFrameNanos = 0L
private fun advanceVirtualFrame() {
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
}
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
private fun frameClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
// Timestamp for the owner's node-animation clock — real path keeps the pre-existing
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
private fun animationClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
// The window currently inside renderFrame — the loop is single-threaded, so a plain
// var is enough for onFrame probes to address "the window I'm being called for".
private var renderingWindow: WindowInstance? = null
/* From inside an onFrame callback: true while the just-rendered window still has pending
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
Screenshot probes capture once this stays false for a few consecutive frames instead of
at a fixed frame count. Outside onFrame it answers false. */
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
/* Single-window compatibility wrapper — the pre-multi-window entry point. /* Single-window compatibility wrapper — the pre-multi-window entry point.
Closing the window exits the app, exactly as before. */ Closing the window exits the app, exactly as before. */
fun nativeComposeWindow( fun nativeComposeWindow(
@@ -378,8 +545,9 @@ internal class WindowInstance(
private var hasMousePos = false private var hasMousePos = false
private var frameIndex = 0 private var frameIndex = 0
private var fpsFrames = 0 private var fpsEma = 0.0
private var fpsLastMs = SDL_GetTicks() private var fpsLastFrameMs = SDL_GetTicks()
private var fpsLastTitleMs = 0uL
// Escape → back (upstream desktop's BackNavigationEventInput): unconsumed // Escape → back (upstream desktop's BackNavigationEventInput): unconsumed
// Escape completes a back navigation on THIS window's dispatcher. // Escape completes a back navigation on THIS window's dispatcher.
@@ -433,6 +601,10 @@ internal class WindowInstance(
host = ComposeRootHost(inDensity = backend.pixelDensity) host = ComposeRootHost(inDensity = backend.pixelDensity)
host.attach() host.attach()
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame
// even when nothing else (recompose / relayout) is pending — retained layers
// need this so a draw-only state change still repaints.
host.setInvalidationCallback { needsFrame = true }
facade = ComposeNativeWindow(backend, gpuMode, initialTitle) facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput) navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
@@ -441,7 +613,11 @@ internal class WindowInstance(
// setContent below). // setContent below).
installGlobals() installGlobals()
val vRecomposer = Recomposer(inScope.coroutineContext + frameClock) // Effect context for this window's composition — the screenshot flag injects the
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
var vEffectContext = inScope.coroutineContext + frameClock
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
val vRecomposer = Recomposer(vEffectContext)
recomposer = vRecomposer recomposer = vRecomposer
recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() } recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() }
val vComposition = Composition(host.applier, vRecomposer) val vComposition = Composition(host.applier, vRecomposer)
@@ -640,7 +816,18 @@ internal class WindowInstance(
// Frame pump // Frame pump
fun shouldRender(): Boolean = fun shouldRender(): Boolean =
needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null || kForceRender
/* True while this window still has pending work: a state/layout/draw invalidation
(needsFrame), recomposer work, or a composition/node animation awaiting the next
frame. Sampled by windowHasInvalidations() from onFrame probes — the quiescence
signal for render-to-settle screenshot capture. */
fun hasInvalidations(): Boolean =
needsFrame || recomposer?.hasPendingWork == true ||
frameClock.hasAwaiters || host.hasAnimationAwaiters()
// TEMP measurement: CDN_FORCERENDER=1 forces every frame to render so
// sustained steady-state timings can be measured on otherwise-idle screens.
fun renderFrame() { fun renderFrame() {
val vRender = renderBackend ?: return val vRender = renderBackend ?: return
@@ -651,6 +838,7 @@ internal class WindowInstance(
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight) vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
host.setConstraints(backend.pixelWidth, backend.pixelHeight) host.setConstraints(backend.pixelWidth, backend.pixelHeight)
host.measureAndLayout() host.measureAndLayout()
FrameProfiler.phase(" layout")
// Hover refresh after layout (upstream skiko: SyntheticEventSender). // Hover refresh after layout (upstream skiko: SyntheticEventSender).
if (hasMousePos) { if (hasMousePos) {
@@ -658,30 +846,48 @@ internal class WindowInstance(
} }
vRender.beginFrame(1f) vRender.beginFrame(1f)
vRender.drawRoot(host) vRender.drawRoot { canvas -> host.drawRoot(canvas) }
if (onFrame != null && !onFrame.invoke(vRender, frameIndex)) { if (onFrame != null) {
renderingWindow = this
val vContinue = onFrame.invoke(vRender, frameIndex)
renderingWindow = null
// Probe consumers end the app when their scenario completes. // Probe consumers end the app when their scenario completes.
facade.close() if (!vContinue) facade.close()
} }
FrameProfiler.phase(" draw")
vRender.endFrame() vRender.endFrame()
FrameProfiler.phase(" present")
frameIndex++ frameIndex++
// FPS — refreshed ~once a second, per window. // FPS — instantaneous inter-frame rate, EMA-smoothed, title refreshed
fpsFrames++ // ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
// than waiting for a full second of unbroken rendering to accumulate
// (the old fixed-window counter never got its first update: bursty
// interaction kept idling before 1s elapsed, and the idle-skip reset the
// window each time — so FPS only appeared during a long enough page
// transition). A dt outside 1..100ms is the first frame or a resume from
// idle (the gap isn't a real frame interval), so it's not sampled — the
// title just holds the last active rate while idle.
val vNowMs = SDL_GetTicks() val vNowMs = SDL_GetTicks()
val vElapsed = (vNowMs - fpsLastMs).toInt() val vDt = (vNowMs - fpsLastFrameMs).toInt()
if (vElapsed >= 1000) { fpsLastFrameMs = vNowMs
val vFps = fpsFrames * 1000 / vElapsed if (vDt in 1..100) {
val vInst = 1000.0 / vDt
fpsEma = if (fpsEma <= 0.0) vInst else fpsEma * 0.9 + vInst * 0.1
if ((vNowMs - fpsLastTitleMs).toInt() >= 250) {
val vFps = (fpsEma + 0.5).toInt()
facade.updateFps(vFps) facade.updateFps(vFps)
SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS") SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS")
fpsFrames = 0 fpsLastTitleMs = vNowMs
fpsLastMs = vNowMs }
} }
} }
// Idle-skip calls this: drop the frame timer so the resume-from-idle frame's
// dt (the whole idle gap) isn't sampled as a real interval. The EMA is left
// intact so the title keeps showing the last active rate while idle.
fun resetFpsWindow() { fun resetFpsWindow() {
fpsFrames = 0 fpsLastFrameMs = SDL_GetTicks()
fpsLastMs = SDL_GetTicks()
} }
fun destroy() { fun destroy() {
@@ -26,7 +26,12 @@ internal class SDL3FrameClock : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
broadcast.withFrameNanos(onFrame) broadcast.withFrameNanos(onFrame)
fun sendFrame() { fun sendFrame(inNanos: Long = SDL_GetTicksNS().toLong()) {
broadcast.sendFrame(SDL_GetTicksNS().toLong()) broadcast.sendFrame(inNanos)
} }
// Coroutines (recomposer / composition animations) currently suspended in withFrameNanos
// awaiting the next sendFrame — the "an animation is still running" half of the window's
// quiescence signal (see WindowInstance.hasInvalidations).
val hasAwaiters: Boolean get() = broadcast.hasAwaiters
} }
+1 -1
View File
@@ -13,7 +13,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("org.jetbrains.compose.runtime:runtime:1.11.1") api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api("androidx.navigationevent:navigationevent-compose:1.1.2") api("androidx.navigationevent:navigationevent-compose:1.1.2")
api(project(":ui-util")) api(project(":ui-util"))
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
+1 -1
View File
@@ -13,7 +13,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("org.jetbrains.compose.runtime:runtime:1.11.1") api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api(project(":ui-util")) api(project(":ui-util"))
} }
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
@@ -0,0 +1,255 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-tooling-preview>
open annotation class androidx.compose.ui.tooling.preview/AndroidUiMode : kotlin/Annotation { // androidx.compose.ui.tooling.preview/AndroidUiMode|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/AndroidUiMode.<init>|<init>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/Preview : kotlin/Annotation { // androidx.compose.ui.tooling.preview/Preview|null[0]
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Float = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Long = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Int = ...) // androidx.compose.ui.tooling.preview/Preview.<init>|<init>(kotlin.String;kotlin.String;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.String;kotlin.Float;kotlin.Boolean;kotlin.Boolean;kotlin.Long;kotlin.Int;kotlin.String;kotlin.Int){}[0]
final val apiLevel // androidx.compose.ui.tooling.preview/Preview.apiLevel|{}apiLevel[0]
final fun <get-apiLevel>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.apiLevel.<get-apiLevel>|<get-apiLevel>(){}[0]
final val backgroundColor // androidx.compose.ui.tooling.preview/Preview.backgroundColor|{}backgroundColor[0]
final fun <get-backgroundColor>(): kotlin/Long // androidx.compose.ui.tooling.preview/Preview.backgroundColor.<get-backgroundColor>|<get-backgroundColor>(){}[0]
final val device // androidx.compose.ui.tooling.preview/Preview.device|{}device[0]
final fun <get-device>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.device.<get-device>|<get-device>(){}[0]
final val fontScale // androidx.compose.ui.tooling.preview/Preview.fontScale|{}fontScale[0]
final fun <get-fontScale>(): kotlin/Float // androidx.compose.ui.tooling.preview/Preview.fontScale.<get-fontScale>|<get-fontScale>(){}[0]
final val group // androidx.compose.ui.tooling.preview/Preview.group|{}group[0]
final fun <get-group>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.group.<get-group>|<get-group>(){}[0]
final val heightDp // androidx.compose.ui.tooling.preview/Preview.heightDp|{}heightDp[0]
final fun <get-heightDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.heightDp.<get-heightDp>|<get-heightDp>(){}[0]
final val locale // androidx.compose.ui.tooling.preview/Preview.locale|{}locale[0]
final fun <get-locale>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.locale.<get-locale>|<get-locale>(){}[0]
final val name // androidx.compose.ui.tooling.preview/Preview.name|{}name[0]
final fun <get-name>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.name.<get-name>|<get-name>(){}[0]
final val showBackground // androidx.compose.ui.tooling.preview/Preview.showBackground|{}showBackground[0]
final fun <get-showBackground>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showBackground.<get-showBackground>|<get-showBackground>(){}[0]
final val showSystemUi // androidx.compose.ui.tooling.preview/Preview.showSystemUi|{}showSystemUi[0]
final fun <get-showSystemUi>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showSystemUi.<get-showSystemUi>|<get-showSystemUi>(){}[0]
final val uiMode // androidx.compose.ui.tooling.preview/Preview.uiMode|{}uiMode[0]
final fun <get-uiMode>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.uiMode.<get-uiMode>|<get-uiMode>(){}[0]
final val wallpaper // androidx.compose.ui.tooling.preview/Preview.wallpaper|{}wallpaper[0]
final fun <get-wallpaper>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.wallpaper.<get-wallpaper>|<get-wallpaper>(){}[0]
final val widthDp // androidx.compose.ui.tooling.preview/Preview.widthDp|{}widthDp[0]
final fun <get-widthDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.widthDp.<get-widthDp>|<get-widthDp>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewDynamicColors : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewDynamicColors|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewDynamicColors.<init>|<init>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewFontScale : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewFontScale|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewFontScale.<init>|<init>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewLightDark : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewLightDark|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewLightDark.<init>|<init>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewParameter : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewParameter|null[0]
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>>, kotlin/Int = ...) // androidx.compose.ui.tooling.preview/PreviewParameter.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewParameterProvider<*>>;kotlin.Int){}[0]
final val limit // androidx.compose.ui.tooling.preview/PreviewParameter.limit|{}limit[0]
final fun <get-limit>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameter.limit.<get-limit>|<get-limit>(){}[0]
final val provider // androidx.compose.ui.tooling.preview/PreviewParameter.provider|{}provider[0]
final fun <get-provider>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>> // androidx.compose.ui.tooling.preview/PreviewParameter.provider.<get-provider>|<get-provider>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewScreenSizes : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewScreenSizes|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewScreenSizes.<init>|<init>(){}[0]
}
open annotation class androidx.compose.ui.tooling.preview/PreviewWrapper : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewWrapper|null[0]
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider>) // androidx.compose.ui.tooling.preview/PreviewWrapper.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewWrapperProvider>){}[0]
final val wrapper // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper|{}wrapper[0]
final fun <get-wrapper>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider> // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper.<get-wrapper>|<get-wrapper>(){}[0]
}
abstract interface <#A: kotlin/Any?> androidx.compose.ui.tooling.preview/PreviewParameterProvider { // androidx.compose.ui.tooling.preview/PreviewParameterProvider|null[0]
abstract val values // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values|{}values[0]
abstract fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
open val count // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count|{}count[0]
open fun <get-count>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count.<get-count>|<get-count>(){}[0]
open fun getDisplayName(kotlin/Int): kotlin/String? // androidx.compose.ui.tooling.preview/PreviewParameterProvider.getDisplayName|getDisplayName(kotlin.Int){}[0]
}
abstract interface androidx.compose.ui.tooling.preview/PreviewWrapperProvider { // androidx.compose.ui.tooling.preview/PreviewWrapperProvider|null[0]
abstract fun Wrap(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.ui.tooling.preview/PreviewWrapperProvider.Wrap|Wrap(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
}
open class <#A: kotlin/Any?> androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider : androidx.compose.ui.tooling.preview/PreviewParameterProvider<#A> { // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider|null[0]
constructor <init>(kotlin.collections/Collection<#A>) // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.<init>|<init>(kotlin.collections.Collection<1:0>){}[0]
open val values // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values|{}values[0]
open fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
}
open class androidx.compose.ui.tooling.preview.datasource/LoremIpsum : androidx.compose.ui.tooling.preview/PreviewParameterProvider<kotlin/String> { // androidx.compose.ui.tooling.preview.datasource/LoremIpsum|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(){}[0]
constructor <init>(kotlin/Int) // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(kotlin.Int){}[0]
open val values // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values|{}values[0]
open fun <get-values>(): kotlin.sequences/Sequence<kotlin/String> // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values.<get-values>|<get-values>(){}[0]
}
final object androidx.compose.ui.tooling.preview/AndroidUiModes { // androidx.compose.ui.tooling.preview/AndroidUiModes|null[0]
final const val UI_MODE_NIGHT_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK|{}UI_MODE_NIGHT_MASK[0]
final fun <get-UI_MODE_NIGHT_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK.<get-UI_MODE_NIGHT_MASK>|<get-UI_MODE_NIGHT_MASK>(){}[0]
final const val UI_MODE_NIGHT_NO // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO|{}UI_MODE_NIGHT_NO[0]
final fun <get-UI_MODE_NIGHT_NO>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO.<get-UI_MODE_NIGHT_NO>|<get-UI_MODE_NIGHT_NO>(){}[0]
final const val UI_MODE_NIGHT_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED|{}UI_MODE_NIGHT_UNDEFINED[0]
final fun <get-UI_MODE_NIGHT_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED.<get-UI_MODE_NIGHT_UNDEFINED>|<get-UI_MODE_NIGHT_UNDEFINED>(){}[0]
final const val UI_MODE_NIGHT_YES // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES|{}UI_MODE_NIGHT_YES[0]
final fun <get-UI_MODE_NIGHT_YES>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES.<get-UI_MODE_NIGHT_YES>|<get-UI_MODE_NIGHT_YES>(){}[0]
final const val UI_MODE_TYPE_APPLIANCE // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE|{}UI_MODE_TYPE_APPLIANCE[0]
final fun <get-UI_MODE_TYPE_APPLIANCE>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE.<get-UI_MODE_TYPE_APPLIANCE>|<get-UI_MODE_TYPE_APPLIANCE>(){}[0]
final const val UI_MODE_TYPE_CAR // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR|{}UI_MODE_TYPE_CAR[0]
final fun <get-UI_MODE_TYPE_CAR>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR.<get-UI_MODE_TYPE_CAR>|<get-UI_MODE_TYPE_CAR>(){}[0]
final const val UI_MODE_TYPE_DESK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK|{}UI_MODE_TYPE_DESK[0]
final fun <get-UI_MODE_TYPE_DESK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK.<get-UI_MODE_TYPE_DESK>|<get-UI_MODE_TYPE_DESK>(){}[0]
final const val UI_MODE_TYPE_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK|{}UI_MODE_TYPE_MASK[0]
final fun <get-UI_MODE_TYPE_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK.<get-UI_MODE_TYPE_MASK>|<get-UI_MODE_TYPE_MASK>(){}[0]
final const val UI_MODE_TYPE_NORMAL // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL|{}UI_MODE_TYPE_NORMAL[0]
final fun <get-UI_MODE_TYPE_NORMAL>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL.<get-UI_MODE_TYPE_NORMAL>|<get-UI_MODE_TYPE_NORMAL>(){}[0]
final const val UI_MODE_TYPE_TELEVISION // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION|{}UI_MODE_TYPE_TELEVISION[0]
final fun <get-UI_MODE_TYPE_TELEVISION>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION.<get-UI_MODE_TYPE_TELEVISION>|<get-UI_MODE_TYPE_TELEVISION>(){}[0]
final const val UI_MODE_TYPE_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED|{}UI_MODE_TYPE_UNDEFINED[0]
final fun <get-UI_MODE_TYPE_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED.<get-UI_MODE_TYPE_UNDEFINED>|<get-UI_MODE_TYPE_UNDEFINED>(){}[0]
final const val UI_MODE_TYPE_VR_HEADSET // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET|{}UI_MODE_TYPE_VR_HEADSET[0]
final fun <get-UI_MODE_TYPE_VR_HEADSET>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET.<get-UI_MODE_TYPE_VR_HEADSET>|<get-UI_MODE_TYPE_VR_HEADSET>(){}[0]
final const val UI_MODE_TYPE_WATCH // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH|{}UI_MODE_TYPE_WATCH[0]
final fun <get-UI_MODE_TYPE_WATCH>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH.<get-UI_MODE_TYPE_WATCH>|<get-UI_MODE_TYPE_WATCH>(){}[0]
}
final object androidx.compose.ui.tooling.preview/Devices { // androidx.compose.ui.tooling.preview/Devices|null[0]
final const val AUTOMOTIVE_1024p // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p|{}AUTOMOTIVE_1024p[0]
final fun <get-AUTOMOTIVE_1024p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p.<get-AUTOMOTIVE_1024p>|<get-AUTOMOTIVE_1024p>(){}[0]
final const val DEFAULT // androidx.compose.ui.tooling.preview/Devices.DEFAULT|{}DEFAULT[0]
final fun <get-DEFAULT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
final const val DESKTOP // androidx.compose.ui.tooling.preview/Devices.DESKTOP|{}DESKTOP[0]
final fun <get-DESKTOP>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DESKTOP.<get-DESKTOP>|<get-DESKTOP>(){}[0]
final const val FOLDABLE // androidx.compose.ui.tooling.preview/Devices.FOLDABLE|{}FOLDABLE[0]
final fun <get-FOLDABLE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.FOLDABLE.<get-FOLDABLE>|<get-FOLDABLE>(){}[0]
final const val NEXUS_10 // androidx.compose.ui.tooling.preview/Devices.NEXUS_10|{}NEXUS_10[0]
final fun <get-NEXUS_10>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_10.<get-NEXUS_10>|<get-NEXUS_10>(){}[0]
final const val NEXUS_5 // androidx.compose.ui.tooling.preview/Devices.NEXUS_5|{}NEXUS_5[0]
final fun <get-NEXUS_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5.<get-NEXUS_5>|<get-NEXUS_5>(){}[0]
final const val NEXUS_5X // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X|{}NEXUS_5X[0]
final fun <get-NEXUS_5X>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X.<get-NEXUS_5X>|<get-NEXUS_5X>(){}[0]
final const val NEXUS_6 // androidx.compose.ui.tooling.preview/Devices.NEXUS_6|{}NEXUS_6[0]
final fun <get-NEXUS_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6.<get-NEXUS_6>|<get-NEXUS_6>(){}[0]
final const val NEXUS_6P // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P|{}NEXUS_6P[0]
final fun <get-NEXUS_6P>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P.<get-NEXUS_6P>|<get-NEXUS_6P>(){}[0]
final const val NEXUS_7 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7|{}NEXUS_7[0]
final fun <get-NEXUS_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7.<get-NEXUS_7>|<get-NEXUS_7>(){}[0]
final const val NEXUS_7_2013 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013|{}NEXUS_7_2013[0]
final fun <get-NEXUS_7_2013>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013.<get-NEXUS_7_2013>|<get-NEXUS_7_2013>(){}[0]
final const val NEXUS_9 // androidx.compose.ui.tooling.preview/Devices.NEXUS_9|{}NEXUS_9[0]
final fun <get-NEXUS_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_9.<get-NEXUS_9>|<get-NEXUS_9>(){}[0]
final const val PHONE // androidx.compose.ui.tooling.preview/Devices.PHONE|{}PHONE[0]
final fun <get-PHONE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PHONE.<get-PHONE>|<get-PHONE>(){}[0]
final const val PIXEL // androidx.compose.ui.tooling.preview/Devices.PIXEL|{}PIXEL[0]
final fun <get-PIXEL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL.<get-PIXEL>|<get-PIXEL>(){}[0]
final const val PIXEL_2 // androidx.compose.ui.tooling.preview/Devices.PIXEL_2|{}PIXEL_2[0]
final fun <get-PIXEL_2>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2.<get-PIXEL_2>|<get-PIXEL_2>(){}[0]
final const val PIXEL_2_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL|{}PIXEL_2_XL[0]
final fun <get-PIXEL_2_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL.<get-PIXEL_2_XL>|<get-PIXEL_2_XL>(){}[0]
final const val PIXEL_3 // androidx.compose.ui.tooling.preview/Devices.PIXEL_3|{}PIXEL_3[0]
final fun <get-PIXEL_3>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3.<get-PIXEL_3>|<get-PIXEL_3>(){}[0]
final const val PIXEL_3A // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A|{}PIXEL_3A[0]
final fun <get-PIXEL_3A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A.<get-PIXEL_3A>|<get-PIXEL_3A>(){}[0]
final const val PIXEL_3A_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL|{}PIXEL_3A_XL[0]
final fun <get-PIXEL_3A_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL.<get-PIXEL_3A_XL>|<get-PIXEL_3A_XL>(){}[0]
final const val PIXEL_3_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL|{}PIXEL_3_XL[0]
final fun <get-PIXEL_3_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL.<get-PIXEL_3_XL>|<get-PIXEL_3_XL>(){}[0]
final const val PIXEL_4 // androidx.compose.ui.tooling.preview/Devices.PIXEL_4|{}PIXEL_4[0]
final fun <get-PIXEL_4>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4.<get-PIXEL_4>|<get-PIXEL_4>(){}[0]
final const val PIXEL_4A // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A|{}PIXEL_4A[0]
final fun <get-PIXEL_4A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A.<get-PIXEL_4A>|<get-PIXEL_4A>(){}[0]
final const val PIXEL_4_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL|{}PIXEL_4_XL[0]
final fun <get-PIXEL_4_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL.<get-PIXEL_4_XL>|<get-PIXEL_4_XL>(){}[0]
final const val PIXEL_5 // androidx.compose.ui.tooling.preview/Devices.PIXEL_5|{}PIXEL_5[0]
final fun <get-PIXEL_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_5.<get-PIXEL_5>|<get-PIXEL_5>(){}[0]
final const val PIXEL_6 // androidx.compose.ui.tooling.preview/Devices.PIXEL_6|{}PIXEL_6[0]
final fun <get-PIXEL_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6.<get-PIXEL_6>|<get-PIXEL_6>(){}[0]
final const val PIXEL_6A // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A|{}PIXEL_6A[0]
final fun <get-PIXEL_6A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A.<get-PIXEL_6A>|<get-PIXEL_6A>(){}[0]
final const val PIXEL_6_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO|{}PIXEL_6_PRO[0]
final fun <get-PIXEL_6_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO.<get-PIXEL_6_PRO>|<get-PIXEL_6_PRO>(){}[0]
final const val PIXEL_7 // androidx.compose.ui.tooling.preview/Devices.PIXEL_7|{}PIXEL_7[0]
final fun <get-PIXEL_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7.<get-PIXEL_7>|<get-PIXEL_7>(){}[0]
final const val PIXEL_7A // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A|{}PIXEL_7A[0]
final fun <get-PIXEL_7A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A.<get-PIXEL_7A>|<get-PIXEL_7A>(){}[0]
final const val PIXEL_7_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO|{}PIXEL_7_PRO[0]
final fun <get-PIXEL_7_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO.<get-PIXEL_7_PRO>|<get-PIXEL_7_PRO>(){}[0]
final const val PIXEL_8 // androidx.compose.ui.tooling.preview/Devices.PIXEL_8|{}PIXEL_8[0]
final fun <get-PIXEL_8>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8.<get-PIXEL_8>|<get-PIXEL_8>(){}[0]
final const val PIXEL_8A // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A|{}PIXEL_8A[0]
final fun <get-PIXEL_8A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A.<get-PIXEL_8A>|<get-PIXEL_8A>(){}[0]
final const val PIXEL_8_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO|{}PIXEL_8_PRO[0]
final fun <get-PIXEL_8_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO.<get-PIXEL_8_PRO>|<get-PIXEL_8_PRO>(){}[0]
final const val PIXEL_9 // androidx.compose.ui.tooling.preview/Devices.PIXEL_9|{}PIXEL_9[0]
final fun <get-PIXEL_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9.<get-PIXEL_9>|<get-PIXEL_9>(){}[0]
final const val PIXEL_9_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO|{}PIXEL_9_PRO[0]
final fun <get-PIXEL_9_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO.<get-PIXEL_9_PRO>|<get-PIXEL_9_PRO>(){}[0]
final const val PIXEL_9_PRO_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD|{}PIXEL_9_PRO_FOLD[0]
final fun <get-PIXEL_9_PRO_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD.<get-PIXEL_9_PRO_FOLD>|<get-PIXEL_9_PRO_FOLD>(){}[0]
final const val PIXEL_9_PRO_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL|{}PIXEL_9_PRO_XL[0]
final fun <get-PIXEL_9_PRO_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL.<get-PIXEL_9_PRO_XL>|<get-PIXEL_9_PRO_XL>(){}[0]
final const val PIXEL_C // androidx.compose.ui.tooling.preview/Devices.PIXEL_C|{}PIXEL_C[0]
final fun <get-PIXEL_C>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_C.<get-PIXEL_C>|<get-PIXEL_C>(){}[0]
final const val PIXEL_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD|{}PIXEL_FOLD[0]
final fun <get-PIXEL_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD.<get-PIXEL_FOLD>|<get-PIXEL_FOLD>(){}[0]
final const val PIXEL_TABLET // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET|{}PIXEL_TABLET[0]
final fun <get-PIXEL_TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET.<get-PIXEL_TABLET>|<get-PIXEL_TABLET>(){}[0]
final const val PIXEL_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL|{}PIXEL_XL[0]
final fun <get-PIXEL_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL.<get-PIXEL_XL>|<get-PIXEL_XL>(){}[0]
final const val TABLET // androidx.compose.ui.tooling.preview/Devices.TABLET|{}TABLET[0]
final fun <get-TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TABLET.<get-TABLET>|<get-TABLET>(){}[0]
final const val TV_1080p // androidx.compose.ui.tooling.preview/Devices.TV_1080p|{}TV_1080p[0]
final fun <get-TV_1080p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_1080p.<get-TV_1080p>|<get-TV_1080p>(){}[0]
final const val TV_720p // androidx.compose.ui.tooling.preview/Devices.TV_720p|{}TV_720p[0]
final fun <get-TV_720p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_720p.<get-TV_720p>|<get-TV_720p>(){}[0]
final const val WEAR_OS_LARGE_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND|{}WEAR_OS_LARGE_ROUND[0]
final fun <get-WEAR_OS_LARGE_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND.<get-WEAR_OS_LARGE_ROUND>|<get-WEAR_OS_LARGE_ROUND>(){}[0]
final const val WEAR_OS_RECT // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT|{}WEAR_OS_RECT[0]
final fun <get-WEAR_OS_RECT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT.<get-WEAR_OS_RECT>|<get-WEAR_OS_RECT>(){}[0]
final const val WEAR_OS_SMALL_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND|{}WEAR_OS_SMALL_ROUND[0]
final fun <get-WEAR_OS_SMALL_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND.<get-WEAR_OS_SMALL_ROUND>|<get-WEAR_OS_SMALL_ROUND>(){}[0]
final const val WEAR_OS_SQUARE // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE|{}WEAR_OS_SQUARE[0]
final fun <get-WEAR_OS_SQUARE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE.<get-WEAR_OS_SQUARE>|<get-WEAR_OS_SQUARE>(){}[0]
}
final object androidx.compose.ui.tooling.preview/Wallpapers { // androidx.compose.ui.tooling.preview/Wallpapers|null[0]
final const val BLUE_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE|{}BLUE_DOMINATED_EXAMPLE[0]
final fun <get-BLUE_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE.<get-BLUE_DOMINATED_EXAMPLE>|<get-BLUE_DOMINATED_EXAMPLE>(){}[0]
final const val GREEN_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE|{}GREEN_DOMINATED_EXAMPLE[0]
final fun <get-GREEN_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE.<get-GREEN_DOMINATED_EXAMPLE>|<get-GREEN_DOMINATED_EXAMPLE>(){}[0]
final const val NONE // androidx.compose.ui.tooling.preview/Wallpapers.NONE|{}NONE[0]
final fun <get-NONE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.NONE.<get-NONE>|<get-NONE>(){}[0]
final const val RED_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE|{}RED_DOMINATED_EXAMPLE[0]
final fun <get-RED_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE.<get-RED_DOMINATED_EXAMPLE>|<get-RED_DOMINATED_EXAMPLE>(){}[0]
final const val YELLOW_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE|{}YELLOW_DOMINATED_EXAMPLE[0]
final fun <get-YELLOW_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE.<get-YELLOW_DOMINATED_EXAMPLE>|<get-YELLOW_DOMINATED_EXAMPLE>(){}[0]
}
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop[0]
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop[0]
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop|#static{}androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop[0]
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop|#static{}androidx_compose_ui_tooling_preview_Devices$stableprop[0]
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop|#static{}androidx_compose_ui_tooling_preview_Wallpapers$stableprop[0]
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(){}[0]
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(){}[0]
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter|androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(){}[0]
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter|androidx_compose_ui_tooling_preview_Devices$stableprop_getter(){}[0]
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter|androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(){}[0]
@@ -0,0 +1,34 @@
// :ui-tooling-preview — androidx.compose.ui.tooling.preview.* (the common
// @Preview annotation + PreviewParameterProvider), vendored VERBATIM from
// upstream (compose/ui/ui-tooling-preview). The Maven artifact ships no
// mingwX64/linux klibs — apps declare the OFFICIAL coords in commonMain and
// the root bridge substitutes this module on native configurations. The
// annotations are IDE-only metadata: previews render through the app's JVM
// target against upstream Compose Desktop.
// Never hand-edit src/vendor/ — change the manifest + `python scripts/compose-fork/sync.py compose/ui/ui-tooling-preview`.
// Publication artifactId: ui-tooling-preview.
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.plugin.compose)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
}
kotlin.srcDir("src/vendor/common/kotlin")
}
nativeMain { kotlin.srcDir("src/vendor/native/kotlin") }
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
)
}
}
@@ -0,0 +1,23 @@
SET_REPO=https://github.com/JetBrains/compose-multiplatform-core@<COMPOSE_CORE_REF>
SET_FOLDER=compose/ui/ui-tooling-preview/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Device.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt
nonJvmMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.native.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# desktopMain (1)
# | desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt -> src/vendor/desktop/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt
# jvmAndAndroidMain (1)
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
+1 -1
View File
@@ -13,7 +13,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("org.jetbrains.compose.runtime:runtime:1.11.1") api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api(project(":ui-geometry")) api(project(":ui-geometry"))
api(project(":ui-util")) api(project(":ui-util"))
} }
+1 -1
View File
@@ -15,7 +15,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("org.jetbrains.compose.runtime:runtime:1.11.1") api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api("androidx.collection:collection:1.5.0") api("androidx.collection:collection:1.5.0")
} }
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
+93 -189
View File
@@ -1834,6 +1834,10 @@ abstract interface androidx.compose.ui/UiMediaScope { // androidx.compose.ui/UiM
} }
} }
abstract interface com.compose.sdl.graphics/EncodedImageDecoder { // com.compose.sdl.graphics/EncodedImageDecoder|null[0]
abstract fun decode(kotlin/ByteArray): androidx.compose.ui.graphics/ImageBitmap? // com.compose.sdl.graphics/EncodedImageDecoder.decode|decode(kotlin.ByteArray){}[0]
}
abstract interface com.compose.sdl.graphics/NativePainterCanvas { // com.compose.sdl.graphics/NativePainterCanvas|null[0] abstract interface com.compose.sdl.graphics/NativePainterCanvas { // com.compose.sdl.graphics/NativePainterCanvas|null[0]
abstract fun drawNativePainter(kotlin/String, com.compose.sdl.res/ResourceKind, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, androidx.compose.ui.layout/ContentScale, kotlin/Float) // com.compose.sdl.graphics/NativePainterCanvas.drawNativePainter|drawNativePainter(kotlin.String;com.compose.sdl.res.ResourceKind;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.layout.ContentScale;kotlin.Float){}[0] abstract fun drawNativePainter(kotlin/String, com.compose.sdl.res/ResourceKind, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, androidx.compose.ui.layout/ContentScale, kotlin/Float) // com.compose.sdl.graphics/NativePainterCanvas.drawNativePainter|drawNativePainter(kotlin.String;com.compose.sdl.res.ResourceKind;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.layout.ContentScale;kotlin.Float){}[0]
} }
@@ -2522,9 +2526,23 @@ final class androidx.compose.ui.autofill/AutofillTree { // androidx.compose.ui.a
final fun plusAssign(androidx.compose.ui.autofill/AutofillNode) // androidx.compose.ui.autofill/AutofillTree.plusAssign|plusAssign(androidx.compose.ui.autofill.AutofillNode){}[0] final fun plusAssign(androidx.compose.ui.autofill/AutofillNode) // androidx.compose.ui.autofill/AutofillTree.plusAssign|plusAssign(androidx.compose.ui.autofill.AutofillNode){}[0]
} }
final class androidx.compose.ui.draganddrop/DragAndDropEvent // androidx.compose.ui.draganddrop/DragAndDropEvent|null[0] final class androidx.compose.ui.draganddrop/DragAndDropEvent { // androidx.compose.ui.draganddrop/DragAndDropEvent|null[0]
final val position // androidx.compose.ui.draganddrop/DragAndDropEvent.position|{}position[0]
final fun <get-position>(): androidx.compose.ui.geometry/Offset // androidx.compose.ui.draganddrop/DragAndDropEvent.position.<get-position>|<get-position>(){}[0]
final val transferData // androidx.compose.ui.draganddrop/DragAndDropEvent.transferData|{}transferData[0]
final fun <get-transferData>(): androidx.compose.ui.draganddrop/DragAndDropTransferData // androidx.compose.ui.draganddrop/DragAndDropEvent.transferData.<get-transferData>|<get-transferData>(){}[0]
}
final class androidx.compose.ui.draganddrop/DragAndDropTransferData // androidx.compose.ui.draganddrop/DragAndDropTransferData|null[0] final class androidx.compose.ui.draganddrop/DragAndDropTransferData { // androidx.compose.ui.draganddrop/DragAndDropTransferData|null[0]
final val filePaths // androidx.compose.ui.draganddrop/DragAndDropTransferData.filePaths|{}filePaths[0]
final fun <get-filePaths>(): kotlin.collections/List<kotlin/String> // androidx.compose.ui.draganddrop/DragAndDropTransferData.filePaths.<get-filePaths>|<get-filePaths>(){}[0]
final val hasFiles // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasFiles|{}hasFiles[0]
final fun <get-hasFiles>(): kotlin/Boolean // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasFiles.<get-hasFiles>|<get-hasFiles>(){}[0]
final val hasText // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasText|{}hasText[0]
final fun <get-hasText>(): kotlin/Boolean // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasText.<get-hasText>|<get-hasText>(){}[0]
final val text // androidx.compose.ui.draganddrop/DragAndDropTransferData.text|{}text[0]
final fun <get-text>(): kotlin/String? // androidx.compose.ui.draganddrop/DragAndDropTransferData.text.<get-text>|<get-text>(){}[0]
}
final class androidx.compose.ui.draw/CacheDrawScope : androidx.compose.ui.unit/Density { // androidx.compose.ui.draw/CacheDrawScope|null[0] final class androidx.compose.ui.draw/CacheDrawScope : androidx.compose.ui.unit/Density { // androidx.compose.ui.draw/CacheDrawScope|null[0]
final val density // androidx.compose.ui.draw/CacheDrawScope.density|{}density[0] final val density // androidx.compose.ui.draw/CacheDrawScope.density|{}density[0]
@@ -3654,9 +3672,11 @@ final class androidx.compose.ui.platform/ClipEntry { // androidx.compose.ui.plat
final val clipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata|{}clipMetadata[0] final val clipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata|{}clipMetadata[0]
final fun <get-clipMetadata>(): androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata.<get-clipMetadata>|<get-clipMetadata>(){}[0] final fun <get-clipMetadata>(): androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata.<get-clipMetadata>|<get-clipMetadata>(){}[0]
final fun getImage(): kotlin/ByteArray? // androidx.compose.ui.platform/ClipEntry.getImage|getImage(){}[0]
final fun getPlainText(): kotlin/String? // androidx.compose.ui.platform/ClipEntry.getPlainText|getPlainText(){}[0] final fun getPlainText(): kotlin/String? // androidx.compose.ui.platform/ClipEntry.getPlainText|getPlainText(){}[0]
final object Companion { // androidx.compose.ui.platform/ClipEntry.Companion|null[0] final object Companion { // androidx.compose.ui.platform/ClipEntry.Companion|null[0]
final fun withImage(kotlin/ByteArray): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withImage|withImage(kotlin.ByteArray){}[0]
final fun withPlainText(kotlin/String): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withPlainText|withPlainText(kotlin.String){}[0] final fun withPlainText(kotlin/String): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withPlainText|withPlainText(kotlin.String){}[0]
} }
} }
@@ -5348,136 +5368,6 @@ final class com.compose.sdl.element/GraphicsLayerNode : androidx.compose.ui/Modi
final fun <set-translationY>(kotlin/Float) // com.compose.sdl.element/GraphicsLayerNode.translationY.<set-translationY>|<set-translationY>(kotlin.Float){}[0] final fun <set-translationY>(kotlin/Float) // com.compose.sdl.element/GraphicsLayerNode.translationY.<set-translationY>|<set-translationY>(kotlin.Float){}[0]
} }
final class com.compose.sdl.element/MiddleClickModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/MiddleClickNode> { // com.compose.sdl.element/MiddleClickModifier|null[0]
constructor <init>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickModifier.<init>|<init>(kotlin.Function0<kotlin.Unit>){}[0]
final val onClick // com.compose.sdl.element/MiddleClickModifier.onClick|{}onClick[0]
final fun <get-onClick>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/MiddleClickModifier.onClick.<get-onClick>|<get-onClick>(){}[0]
final fun create(): com.compose.sdl.element/MiddleClickNode // com.compose.sdl.element/MiddleClickModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/MiddleClickModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/MiddleClickModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/MiddleClickNode) // com.compose.sdl.element/MiddleClickModifier.update|update(com.compose.sdl.element.MiddleClickNode){}[0]
}
final class com.compose.sdl.element/MiddleClickNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/MiddleClickNode|null[0]
constructor <init>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickNode.<init>|<init>(kotlin.Function0<kotlin.Unit>){}[0]
final var onClick // com.compose.sdl.element/MiddleClickNode.onClick|{}onClick[0]
final fun <get-onClick>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/MiddleClickNode.onClick.<get-onClick>|<get-onClick>(){}[0]
final fun <set-onClick>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickNode.onClick.<set-onClick>|<set-onClick>(kotlin.Function0<kotlin.Unit>){}[0]
}
final class com.compose.sdl.element/OnDragModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnDragNode> { // com.compose.sdl.element/OnDragModifier|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
final val onDrag // com.compose.sdl.element/OnDragModifier.onDrag|{}onDrag[0]
final fun <get-onDrag>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onDrag.<get-onDrag>|<get-onDrag>(){}[0]
final val onEnd // com.compose.sdl.element/OnDragModifier.onEnd|{}onEnd[0]
final fun <get-onEnd>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onEnd.<get-onEnd>|<get-onEnd>(){}[0]
final val onStart // com.compose.sdl.element/OnDragModifier.onStart|{}onStart[0]
final fun <get-onStart>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onStart.<get-onStart>|<get-onStart>(){}[0]
final fun create(): com.compose.sdl.element/OnDragNode // com.compose.sdl.element/OnDragModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnDragModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnDragModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/OnDragNode) // com.compose.sdl.element/OnDragModifier.update|update(com.compose.sdl.element.OnDragNode){}[0]
}
final class com.compose.sdl.element/OnDragNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnDragNode|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
final var onDrag // com.compose.sdl.element/OnDragNode.onDrag|{}onDrag[0]
final fun <get-onDrag>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragNode.onDrag.<get-onDrag>|<get-onDrag>(){}[0]
final fun <set-onDrag>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onDrag.<set-onDrag>|<set-onDrag>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final var onEnd // com.compose.sdl.element/OnDragNode.onEnd|{}onEnd[0]
final fun <get-onEnd>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/OnDragNode.onEnd.<get-onEnd>|<get-onEnd>(){}[0]
final fun <set-onEnd>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onEnd.<set-onEnd>|<set-onEnd>(kotlin.Function0<kotlin.Unit>){}[0]
final var onStart // com.compose.sdl.element/OnDragNode.onStart|{}onStart[0]
final fun <get-onStart>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragNode.onStart.<get-onStart>|<get-onStart>(){}[0]
final fun <set-onStart>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onStart.<set-onStart>|<set-onStart>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
}
final class com.compose.sdl.element/OnPressedModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnPressedNode> { // com.compose.sdl.element/OnPressedModifier|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final val handler // com.compose.sdl.element/OnPressedModifier.handler|{}handler[0]
final fun <get-handler>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnPressedModifier.handler.<get-handler>|<get-handler>(){}[0]
final fun create(): com.compose.sdl.element/OnPressedNode // com.compose.sdl.element/OnPressedModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnPressedModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnPressedModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/OnPressedNode) // com.compose.sdl.element/OnPressedModifier.update|update(com.compose.sdl.element.OnPressedNode){}[0]
}
final class com.compose.sdl.element/OnPressedNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnPressedNode|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final var handler // com.compose.sdl.element/OnPressedNode.handler|{}handler[0]
final fun <get-handler>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnPressedNode.handler.<get-handler>|<get-handler>(){}[0]
final fun <set-handler>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedNode.handler.<set-handler>|<set-handler>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
}
final class com.compose.sdl.element/OnTextInputModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnTextInputNode> { // com.compose.sdl.element/OnTextInputModifier|null[0]
constructor <init>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputModifier.<init>|<init>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
final val handler // com.compose.sdl.element/OnTextInputModifier.handler|{}handler[0]
final fun <get-handler>(): kotlin/Function1<kotlin/String, kotlin/Unit> // com.compose.sdl.element/OnTextInputModifier.handler.<get-handler>|<get-handler>(){}[0]
final fun create(): com.compose.sdl.element/OnTextInputNode // com.compose.sdl.element/OnTextInputModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnTextInputModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnTextInputModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/OnTextInputNode) // com.compose.sdl.element/OnTextInputModifier.update|update(com.compose.sdl.element.OnTextInputNode){}[0]
}
final class com.compose.sdl.element/OnTextInputNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnTextInputNode|null[0]
constructor <init>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputNode.<init>|<init>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
final var handler // com.compose.sdl.element/OnTextInputNode.handler|{}handler[0]
final fun <get-handler>(): kotlin/Function1<kotlin/String, kotlin/Unit> // com.compose.sdl.element/OnTextInputNode.handler.<get-handler>|<get-handler>(){}[0]
final fun <set-handler>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputNode.handler.<set-handler>|<set-handler>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
}
final class com.compose.sdl.element/PressableModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/PressableNode> { // com.compose.sdl.element/PressableModifier|null[0]
constructor <init>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableModifier.<init>|<init>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
final val onChange // com.compose.sdl.element/PressableModifier.onChange|{}onChange[0]
final fun <get-onChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit> // com.compose.sdl.element/PressableModifier.onChange.<get-onChange>|<get-onChange>(){}[0]
final fun create(): com.compose.sdl.element/PressableNode // com.compose.sdl.element/PressableModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/PressableModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/PressableModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/PressableNode) // com.compose.sdl.element/PressableModifier.update|update(com.compose.sdl.element.PressableNode){}[0]
}
final class com.compose.sdl.element/PressableNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/PressableNode|null[0]
constructor <init>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableNode.<init>|<init>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
final var onChange // com.compose.sdl.element/PressableNode.onChange|{}onChange[0]
final fun <get-onChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit> // com.compose.sdl.element/PressableNode.onChange.<get-onChange>|<get-onChange>(){}[0]
final fun <set-onChange>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableNode.onChange.<set-onChange>|<set-onChange>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
}
final class com.compose.sdl.element/SecondaryClickModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/SecondaryClickNode> { // com.compose.sdl.element/SecondaryClickModifier|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final val onClick // com.compose.sdl.element/SecondaryClickModifier.onClick|{}onClick[0]
final fun <get-onClick>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/SecondaryClickModifier.onClick.<get-onClick>|<get-onClick>(){}[0]
final fun create(): com.compose.sdl.element/SecondaryClickNode // com.compose.sdl.element/SecondaryClickModifier.create|create(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/SecondaryClickModifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl.element/SecondaryClickModifier.hashCode|hashCode(){}[0]
final fun update(com.compose.sdl.element/SecondaryClickNode) // com.compose.sdl.element/SecondaryClickModifier.update|update(com.compose.sdl.element.SecondaryClickNode){}[0]
}
final class com.compose.sdl.element/SecondaryClickNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/SecondaryClickNode|null[0]
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final var onClick // com.compose.sdl.element/SecondaryClickNode.onClick|{}onClick[0]
final fun <get-onClick>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/SecondaryClickNode.onClick.<get-onClick>|<get-onClick>(){}[0]
final fun <set-onClick>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickNode.onClick.<set-onClick>|<set-onClick>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
}
final class com.compose.sdl.graphics/ProjectCanvas : androidx.compose.ui.graphics/Canvas { // com.compose.sdl.graphics/ProjectCanvas|null[0] final class com.compose.sdl.graphics/ProjectCanvas : androidx.compose.ui.graphics/Canvas { // com.compose.sdl.graphics/ProjectCanvas|null[0]
constructor <init>() // com.compose.sdl.graphics/ProjectCanvas.<init>|<init>(){}[0] constructor <init>() // com.compose.sdl.graphics/ProjectCanvas.<init>|<init>(){}[0]
@@ -5649,8 +5539,6 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
final val applier // com.compose.sdl.node/ComposeRootHost.applier|{}applier[0] final val applier // com.compose.sdl.node/ComposeRootHost.applier|{}applier[0]
final fun <get-applier>(): androidx.compose.runtime/Applier<*> // com.compose.sdl.node/ComposeRootHost.applier.<get-applier>|<get-applier>(){}[0] final fun <get-applier>(): androidx.compose.runtime/Applier<*> // com.compose.sdl.node/ComposeRootHost.applier.<get-applier>|<get-applier>(){}[0]
final val autofillTree // com.compose.sdl.node/ComposeRootHost.autofillTree|{}autofillTree[0]
final fun <get-autofillTree>(): androidx.compose.ui.autofill/AutofillTree // com.compose.sdl.node/ComposeRootHost.autofillTree.<get-autofillTree>|<get-autofillTree>(){}[0]
final val density // com.compose.sdl.node/ComposeRootHost.density|{}density[0] final val density // com.compose.sdl.node/ComposeRootHost.density|{}density[0]
final fun <get-density>(): androidx.compose.ui.unit/Density // com.compose.sdl.node/ComposeRootHost.density.<get-density>|<get-density>(){}[0] final fun <get-density>(): androidx.compose.ui.unit/Density // com.compose.sdl.node/ComposeRootHost.density.<get-density>|<get-density>(){}[0]
final val focusManager // com.compose.sdl.node/ComposeRootHost.focusManager|{}focusManager[0] final val focusManager // com.compose.sdl.node/ComposeRootHost.focusManager|{}focusManager[0]
@@ -5665,8 +5553,6 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
final fun <get-layoutDirection>(): androidx.compose.ui.unit/LayoutDirection // com.compose.sdl.node/ComposeRootHost.layoutDirection.<get-layoutDirection>|<get-layoutDirection>(){}[0] final fun <get-layoutDirection>(): androidx.compose.ui.unit/LayoutDirection // com.compose.sdl.node/ComposeRootHost.layoutDirection.<get-layoutDirection>|<get-layoutDirection>(){}[0]
final val softwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController|{}softwareKeyboardController[0] final val softwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController|{}softwareKeyboardController[0]
final fun <get-softwareKeyboardController>(): androidx.compose.ui.platform/SoftwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController.<get-softwareKeyboardController>|<get-softwareKeyboardController>(){}[0] final fun <get-softwareKeyboardController>(): androidx.compose.ui.platform/SoftwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController.<get-softwareKeyboardController>|<get-softwareKeyboardController>(){}[0]
final val textInputService // com.compose.sdl.node/ComposeRootHost.textInputService|{}textInputService[0]
final fun <get-textInputService>(): androidx.compose.ui.text.input/TextInputService // com.compose.sdl.node/ComposeRootHost.textInputService.<get-textInputService>|<get-textInputService>(){}[0]
final val textToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar|{}textToolbar[0] final val textToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar|{}textToolbar[0]
final fun <get-textToolbar>(): androidx.compose.ui.platform/TextToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar.<get-textToolbar>|<get-textToolbar>(){}[0] final fun <get-textToolbar>(): androidx.compose.ui.platform/TextToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar.<get-textToolbar>|<get-textToolbar>(){}[0]
final val viewConfiguration // com.compose.sdl.node/ComposeRootHost.viewConfiguration|{}viewConfiguration[0] final val viewConfiguration // com.compose.sdl.node/ComposeRootHost.viewConfiguration|{}viewConfiguration[0]
@@ -5676,9 +5562,12 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
final fun attach() // com.compose.sdl.node/ComposeRootHost.attach|attach(){}[0] final fun attach() // com.compose.sdl.node/ComposeRootHost.attach|attach(){}[0]
final fun dispatchKeyEvent(androidx.compose.ui.input.key/KeyEvent): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchKeyEvent|dispatchKeyEvent(androidx.compose.ui.input.key.KeyEvent){}[0] final fun dispatchKeyEvent(androidx.compose.ui.input.key/KeyEvent): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchKeyEvent|dispatchKeyEvent(androidx.compose.ui.input.key.KeyEvent){}[0]
final fun dispatchTextInput(kotlin/String): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchTextInput|dispatchTextInput(kotlin.String){}[0]
final fun measureAndLayout() // com.compose.sdl.node/ComposeRootHost.measureAndLayout|measureAndLayout(){}[0] final fun measureAndLayout() // com.compose.sdl.node/ComposeRootHost.measureAndLayout|measureAndLayout(){}[0]
final fun onPointer(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int) // com.compose.sdl.node/ComposeRootHost.onPointer|onPointer(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int){}[0] final fun onDropBegin() // com.compose.sdl.node/ComposeRootHost.onDropBegin|onDropBegin(){}[0]
final fun onDropComplete() // com.compose.sdl.node/ComposeRootHost.onDropComplete|onDropComplete(){}[0]
final fun onDropFile(kotlin/String) // com.compose.sdl.node/ComposeRootHost.onDropFile|onDropFile(kotlin.String){}[0]
final fun onDropPosition(kotlin/Float, kotlin/Float) // com.compose.sdl.node/ComposeRootHost.onDropPosition|onDropPosition(kotlin.Float;kotlin.Float){}[0]
final fun onDropText(kotlin/String) // com.compose.sdl.node/ComposeRootHost.onDropText|onDropText(kotlin.String){}[0]
final fun onPointerRaw(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onPointerRaw|onPointerRaw(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Long){}[0] final fun onPointerRaw(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onPointerRaw|onPointerRaw(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Long){}[0]
final fun onWheel(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onWheel|onWheel(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Long){}[0] final fun onWheel(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onWheel|onWheel(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Long){}[0]
final fun sendAnimationFrame(kotlin/Long) // com.compose.sdl.node/ComposeRootHost.sendAnimationFrame|sendAnimationFrame(kotlin.Long){}[0] final fun sendAnimationFrame(kotlin/Long) // com.compose.sdl.node/ComposeRootHost.sendAnimationFrame|sendAnimationFrame(kotlin.Long){}[0]
@@ -8387,6 +8276,45 @@ sealed class androidx.compose.ui.text.font/SystemFontFamily : androidx.compose.u
sealed class androidx.compose.ui.text/TtsAnnotation : androidx.compose.ui.text/AnnotatedString.Annotation // androidx.compose.ui.text/TtsAnnotation|null[0] sealed class androidx.compose.ui.text/TtsAnnotation : androidx.compose.ui.text/AnnotatedString.Annotation // androidx.compose.ui.text/TtsAnnotation|null[0]
sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0] sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
final enum class DropPhase : kotlin/Enum<com.compose.sdl/AppEvent.DropPhase> { // com.compose.sdl/AppEvent.DropPhase|null[0]
enum entry BEGIN // com.compose.sdl/AppEvent.DropPhase.BEGIN|null[0]
enum entry COMPLETE // com.compose.sdl/AppEvent.DropPhase.COMPLETE|null[0]
enum entry FILE // com.compose.sdl/AppEvent.DropPhase.FILE|null[0]
enum entry POSITION // com.compose.sdl/AppEvent.DropPhase.POSITION|null[0]
enum entry TEXT // com.compose.sdl/AppEvent.DropPhase.TEXT|null[0]
final val entries // com.compose.sdl/AppEvent.DropPhase.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<com.compose.sdl/AppEvent.DropPhase> // com.compose.sdl/AppEvent.DropPhase.entries.<get-entries>|<get-entries>#static(){}[0]
final fun valueOf(kotlin/String): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.DropPhase.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<com.compose.sdl/AppEvent.DropPhase> // com.compose.sdl/AppEvent.DropPhase.values|values#static(){}[0]
}
final class Drop : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Drop|null[0]
constructor <init>(kotlin/UInt, com.compose.sdl/AppEvent.DropPhase, kotlin/Float = ..., kotlin/Float = ..., kotlin/String? = ...) // com.compose.sdl/AppEvent.Drop.<init>|<init>(kotlin.UInt;com.compose.sdl.AppEvent.DropPhase;kotlin.Float;kotlin.Float;kotlin.String?){}[0]
final val data // com.compose.sdl/AppEvent.Drop.data|{}data[0]
final fun <get-data>(): kotlin/String? // com.compose.sdl/AppEvent.Drop.data.<get-data>|<get-data>(){}[0]
final val phase // com.compose.sdl/AppEvent.Drop.phase|{}phase[0]
final fun <get-phase>(): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.Drop.phase.<get-phase>|<get-phase>(){}[0]
final val windowId // com.compose.sdl/AppEvent.Drop.windowId|{}windowId[0]
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.Drop.windowId.<get-windowId>|<get-windowId>(){}[0]
final val x // com.compose.sdl/AppEvent.Drop.x|{}x[0]
final fun <get-x>(): kotlin/Float // com.compose.sdl/AppEvent.Drop.x.<get-x>|<get-x>(){}[0]
final val y // com.compose.sdl/AppEvent.Drop.y|{}y[0]
final fun <get-y>(): kotlin/Float // com.compose.sdl/AppEvent.Drop.y.<get-y>|<get-y>(){}[0]
final fun component1(): kotlin/UInt // com.compose.sdl/AppEvent.Drop.component1|component1(){}[0]
final fun component2(): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.Drop.component2|component2(){}[0]
final fun component3(): kotlin/Float // com.compose.sdl/AppEvent.Drop.component3|component3(){}[0]
final fun component4(): kotlin/Float // com.compose.sdl/AppEvent.Drop.component4|component4(){}[0]
final fun component5(): kotlin/String? // com.compose.sdl/AppEvent.Drop.component5|component5(){}[0]
final fun copy(kotlin/UInt = ..., com.compose.sdl/AppEvent.DropPhase = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/String? = ...): com.compose.sdl/AppEvent.Drop // com.compose.sdl/AppEvent.Drop.copy|copy(kotlin.UInt;com.compose.sdl.AppEvent.DropPhase;kotlin.Float;kotlin.Float;kotlin.String?){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.Drop.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.Drop.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.Drop.toString|toString(){}[0]
}
final class Key : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Key|null[0] final class Key : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Key|null[0]
constructor <init>(androidx.compose.ui.input.key/KeyEvent, kotlin/UInt = ...) // com.compose.sdl/AppEvent.Key.<init>|<init>(androidx.compose.ui.input.key.KeyEvent;kotlin.UInt){}[0] constructor <init>(androidx.compose.ui.input.key/KeyEvent, kotlin/UInt = ...) // com.compose.sdl/AppEvent.Key.<init>|<init>(androidx.compose.ui.input.key.KeyEvent;kotlin.UInt){}[0]
@@ -8473,6 +8401,25 @@ sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.TextInput.toString|toString(){}[0] final fun toString(): kotlin/String // com.compose.sdl/AppEvent.TextInput.toString|toString(){}[0]
} }
final class WindowActivation : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowActivation|null[0]
constructor <init>(kotlin/UInt = ..., kotlin/Boolean? = ..., kotlin/Boolean? = ...) // com.compose.sdl/AppEvent.WindowActivation.<init>|<init>(kotlin.UInt;kotlin.Boolean?;kotlin.Boolean?){}[0]
final val focused // com.compose.sdl/AppEvent.WindowActivation.focused|{}focused[0]
final fun <get-focused>(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.focused.<get-focused>|<get-focused>(){}[0]
final val visible // com.compose.sdl/AppEvent.WindowActivation.visible|{}visible[0]
final fun <get-visible>(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.visible.<get-visible>|<get-visible>(){}[0]
final val windowId // com.compose.sdl/AppEvent.WindowActivation.windowId|{}windowId[0]
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.WindowActivation.windowId.<get-windowId>|<get-windowId>(){}[0]
final fun component1(): kotlin/UInt // com.compose.sdl/AppEvent.WindowActivation.component1|component1(){}[0]
final fun component2(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.component2|component2(){}[0]
final fun component3(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.component3|component3(){}[0]
final fun copy(kotlin/UInt = ..., kotlin/Boolean? = ..., kotlin/Boolean? = ...): com.compose.sdl/AppEvent.WindowActivation // com.compose.sdl/AppEvent.WindowActivation.copy|copy(kotlin.UInt;kotlin.Boolean?;kotlin.Boolean?){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.WindowActivation.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.WindowActivation.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.WindowActivation.toString|toString(){}[0]
}
final class WindowClose : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowClose|null[0] final class WindowClose : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowClose|null[0]
constructor <init>(kotlin/UInt = ...) // com.compose.sdl/AppEvent.WindowClose.<init>|<init>(kotlin.UInt){}[0] constructor <init>(kotlin/UInt = ...) // com.compose.sdl/AppEvent.WindowClose.<init>|<init>(kotlin.UInt){}[0]
@@ -9299,16 +9246,6 @@ final val androidx.compose.ui.platform/androidx_compose_ui_platform_InspectorVal
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop|#static{}androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop[0] final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop|#static{}androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0] final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0] final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0]
final val androidx.compose.ui.platform/currentClipboard // androidx.compose.ui.platform/currentClipboard|{}currentClipboard[0]
final fun <get-currentClipboard>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/currentClipboard.<get-currentClipboard>|<get-currentClipboard>(){}[0]
final val androidx.compose.ui.platform/defaultClipboard // androidx.compose.ui.platform/defaultClipboard|{}defaultClipboard[0]
final fun <get-defaultClipboard>(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/defaultClipboard.<get-defaultClipboard>|<get-defaultClipboard>(){}[0]
final val androidx.compose.ui.platform/defaultClipboardManager // androidx.compose.ui.platform/defaultClipboardManager|{}defaultClipboardManager[0]
final fun <get-defaultClipboardManager>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/defaultClipboardManager.<get-defaultClipboardManager>|<get-defaultClipboardManager>(){}[0]
final val androidx.compose.ui.platform/platformClipboard // androidx.compose.ui.platform/platformClipboard|{}platformClipboard[0]
final fun <get-platformClipboard>(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/platformClipboard.<get-platformClipboard>|<get-platformClipboard>(){}[0]
final val androidx.compose.ui.platform/platformClipboardManager // androidx.compose.ui.platform/platformClipboardManager|{}platformClipboardManager[0]
final fun <get-platformClipboardManager>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/platformClipboardManager.<get-platformClipboardManager>|<get-platformClipboardManager>(){}[0]
final val androidx.compose.ui.platform/safeContent // androidx.compose.ui.platform/safeContent|@androidx.compose.ui.platform.PlatformWindowInsets{}safeContent[0] final val androidx.compose.ui.platform/safeContent // androidx.compose.ui.platform/safeContent|@androidx.compose.ui.platform.PlatformWindowInsets{}safeContent[0]
final fun (androidx.compose.ui.platform/PlatformWindowInsets).<get-safeContent>(): androidx.compose.ui.platform/PlatformInsets // androidx.compose.ui.platform/safeContent.<get-safeContent>|<get-safeContent>@androidx.compose.ui.platform.PlatformWindowInsets(){}[0] final fun (androidx.compose.ui.platform/PlatformWindowInsets).<get-safeContent>(): androidx.compose.ui.platform/PlatformInsets // androidx.compose.ui.platform/safeContent.<get-safeContent>|<get-safeContent>@androidx.compose.ui.platform.PlatformWindowInsets(){}[0]
final val androidx.compose.ui.platform/safeDrawing // androidx.compose.ui.platform/safeDrawing|@androidx.compose.ui.platform.PlatformWindowInsets{}safeDrawing[0] final val androidx.compose.ui.platform/safeDrawing // androidx.compose.ui.platform/safeDrawing|@androidx.compose.ui.platform.PlatformWindowInsets{}safeDrawing[0]
@@ -9424,22 +9361,6 @@ final val com.compose.sdl.element/com_compose_sdl_element_ClipModifier$stablepro
final val com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop|#static{}com_compose_sdl_element_ClipNode$stableprop[0] final val com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop|#static{}com_compose_sdl_element_ClipNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop|#static{}com_compose_sdl_element_GraphicsLayerModifier$stableprop[0] final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop|#static{}com_compose_sdl_element_GraphicsLayerModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop|#static{}com_compose_sdl_element_GraphicsLayerNode$stableprop[0] final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop|#static{}com_compose_sdl_element_GraphicsLayerNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop|#static{}com_compose_sdl_element_MiddleClickModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop|#static{}com_compose_sdl_element_MiddleClickNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop|#static{}com_compose_sdl_element_OnDragModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop|#static{}com_compose_sdl_element_OnDragNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop|#static{}com_compose_sdl_element_OnPressedModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop|#static{}com_compose_sdl_element_OnPressedNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop|#static{}com_compose_sdl_element_OnTextInputModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop|#static{}com_compose_sdl_element_OnTextInputNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop|#static{}com_compose_sdl_element_PressableModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop|#static{}com_compose_sdl_element_PressableNode$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop|#static{}com_compose_sdl_element_SecondaryClickModifier$stableprop[0]
final val com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop|#static{}com_compose_sdl_element_SecondaryClickNode$stableprop[0]
final val com.compose.sdl.graphics/a8 // com.compose.sdl.graphics/a8|@androidx.compose.ui.graphics.Color{}a8[0]
final fun (androidx.compose.ui.graphics/Color).<get-a8>(): kotlin/Int // com.compose.sdl.graphics/a8.<get-a8>|<get-a8>@androidx.compose.ui.graphics.Color(){}[0]
final val com.compose.sdl.graphics/b8 // com.compose.sdl.graphics/b8|@androidx.compose.ui.graphics.Color{}b8[0]
final fun (androidx.compose.ui.graphics/Color).<get-b8>(): kotlin/Int // com.compose.sdl.graphics/b8.<get-b8>|<get-b8>@androidx.compose.ui.graphics.Color(){}[0]
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop|#static{}com_compose_sdl_graphics_PathCommand_Close$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop|#static{}com_compose_sdl_graphics_PathCommand_Close$stableprop[0]
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_CubicTo$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_CubicTo$stableprop[0]
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_LineTo$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_LineTo$stableprop[0]
@@ -9450,8 +9371,6 @@ final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectImageBitmap$s
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop|#static{}com_compose_sdl_graphics_ProjectPaint$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop|#static{}com_compose_sdl_graphics_ProjectPaint$stableprop[0]
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop|#static{}com_compose_sdl_graphics_ProjectPath$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop|#static{}com_compose_sdl_graphics_ProjectPath$stableprop[0]
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop|#static{}com_compose_sdl_graphics_ProjectPathIterator$stableprop[0] final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop|#static{}com_compose_sdl_graphics_ProjectPathIterator$stableprop[0]
final val com.compose.sdl.graphics/g8 // com.compose.sdl.graphics/g8|@androidx.compose.ui.graphics.Color{}g8[0]
final fun (androidx.compose.ui.graphics/Color).<get-g8>(): kotlin/Int // com.compose.sdl.graphics/g8.<get-g8>|<get-g8>@androidx.compose.ui.graphics.Color(){}[0]
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.RadialGradient{}gradientCenter[0] final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.RadialGradient{}gradientCenter[0]
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientCenter>(): androidx.compose.ui.geometry/Offset // com.compose.sdl.graphics/gradientCenter.<get-gradientCenter>|<get-gradientCenter>@androidx.compose.ui.graphics.RadialGradient(){}[0] final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientCenter>(): androidx.compose.ui.geometry/Offset // com.compose.sdl.graphics/gradientCenter.<get-gradientCenter>|<get-gradientCenter>@androidx.compose.ui.graphics.RadialGradient(){}[0]
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.SweepGradient{}gradientCenter[0] final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.SweepGradient{}gradientCenter[0]
@@ -9478,8 +9397,6 @@ final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/
final fun (androidx.compose.ui.graphics/LinearGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.LinearGradient(){}[0] final fun (androidx.compose.ui.graphics/LinearGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.LinearGradient(){}[0]
final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/gradientTileMode|@androidx.compose.ui.graphics.RadialGradient{}gradientTileMode[0] final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/gradientTileMode|@androidx.compose.ui.graphics.RadialGradient{}gradientTileMode[0]
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.RadialGradient(){}[0] final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.RadialGradient(){}[0]
final val com.compose.sdl.graphics/r8 // com.compose.sdl.graphics/r8|@androidx.compose.ui.graphics.Color{}r8[0]
final fun (androidx.compose.ui.graphics/Color).<get-r8>(): kotlin/Int // com.compose.sdl.graphics/r8.<get-r8>|<get-r8>@androidx.compose.ui.graphics.Color(){}[0]
final val com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop|#static{}com_compose_sdl_icons_IconFont$stableprop[0] final val com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop|#static{}com_compose_sdl_icons_IconFont$stableprop[0]
final val com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop|#static{}com_compose_sdl_input_LegacyPointerEvent$stableprop[0] final val com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop|#static{}com_compose_sdl_input_LegacyPointerEvent$stableprop[0]
final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0] final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0]
@@ -9512,12 +9429,14 @@ final val com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stablepro
final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0] final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0]
final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0] final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0]
final val com.compose.sdl/com_compose_sdl_AppEvent$stableprop // com.compose.sdl/com_compose_sdl_AppEvent$stableprop|#static{}com_compose_sdl_AppEvent$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent$stableprop // com.compose.sdl/com_compose_sdl_AppEvent$stableprop|#static{}com_compose_sdl_AppEvent$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop|#static{}com_compose_sdl_AppEvent_Drop$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop|#static{}com_compose_sdl_AppEvent_Key$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop|#static{}com_compose_sdl_AppEvent_Key$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop|#static{}com_compose_sdl_AppEvent_MouseWheel$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop|#static{}com_compose_sdl_AppEvent_MouseWheel$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop|#static{}com_compose_sdl_AppEvent_Pointer$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop|#static{}com_compose_sdl_AppEvent_Pointer$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop|#static{}com_compose_sdl_AppEvent_Quit$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop|#static{}com_compose_sdl_AppEvent_Quit$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop|#static{}com_compose_sdl_AppEvent_RedrawNeeded$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop|#static{}com_compose_sdl_AppEvent_RedrawNeeded$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop|#static{}com_compose_sdl_AppEvent_TextInput$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop|#static{}com_compose_sdl_AppEvent_TextInput$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop|#static{}com_compose_sdl_AppEvent_WindowActivation$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop|#static{}com_compose_sdl_AppEvent_WindowClose$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop|#static{}com_compose_sdl_AppEvent_WindowClose$stableprop[0]
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop|#static{}com_compose_sdl_AppEvent_WindowResized$stableprop[0] final val com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop|#static{}com_compose_sdl_AppEvent_WindowResized$stableprop[0]
final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0] final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0]
@@ -9655,6 +9574,8 @@ final var androidx.compose.ui/isDialogAnimationEnabled // androidx.compose.ui/is
final var androidx.compose.ui/useLegacyRenderNodeLayers // androidx.compose.ui/useLegacyRenderNodeLayers|@androidx.compose.ui.ComposeUiFlags{}useLegacyRenderNodeLayers[0] final var androidx.compose.ui/useLegacyRenderNodeLayers // androidx.compose.ui/useLegacyRenderNodeLayers|@androidx.compose.ui.ComposeUiFlags{}useLegacyRenderNodeLayers[0]
final fun (androidx.compose.ui/ComposeUiFlags).<get-useLegacyRenderNodeLayers>(): kotlin/Boolean // androidx.compose.ui/useLegacyRenderNodeLayers.<get-useLegacyRenderNodeLayers>|<get-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(){}[0] final fun (androidx.compose.ui/ComposeUiFlags).<get-useLegacyRenderNodeLayers>(): kotlin/Boolean // androidx.compose.ui/useLegacyRenderNodeLayers.<get-useLegacyRenderNodeLayers>|<get-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(){}[0]
final fun (androidx.compose.ui/ComposeUiFlags).<set-useLegacyRenderNodeLayers>(kotlin/Boolean) // androidx.compose.ui/useLegacyRenderNodeLayers.<set-useLegacyRenderNodeLayers>|<set-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(kotlin.Boolean){}[0] final fun (androidx.compose.ui/ComposeUiFlags).<set-useLegacyRenderNodeLayers>(kotlin/Boolean) // androidx.compose.ui/useLegacyRenderNodeLayers.<set-useLegacyRenderNodeLayers>|<set-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(kotlin.Boolean){}[0]
final var com.compose.sdl.graphics/encodedImageDecoder // com.compose.sdl.graphics/encodedImageDecoder|{}encodedImageDecoder[0]
final fun <get-encodedImageDecoder>(): com.compose.sdl.graphics/EncodedImageDecoder? // com.compose.sdl.graphics/encodedImageDecoder.<get-encodedImageDecoder>|<get-encodedImageDecoder>(){}[0]
final var com.compose.sdl.res/currentImageLoader // com.compose.sdl.res/currentImageLoader|{}currentImageLoader[0] final var com.compose.sdl.res/currentImageLoader // com.compose.sdl.res/currentImageLoader|{}currentImageLoader[0]
final fun <get-currentImageLoader>(): com.compose.sdl.res/ImageLoader // com.compose.sdl.res/currentImageLoader.<get-currentImageLoader>|<get-currentImageLoader>(){}[0] final fun <get-currentImageLoader>(): com.compose.sdl.res/ImageLoader // com.compose.sdl.res/currentImageLoader.<get-currentImageLoader>|<get-currentImageLoader>(){}[0]
final fun <set-currentImageLoader>(com.compose.sdl.res/ImageLoader) // com.compose.sdl.res/currentImageLoader.<set-currentImageLoader>|<set-currentImageLoader>(com.compose.sdl.res.ImageLoader){}[0] final fun <set-currentImageLoader>(com.compose.sdl.res/ImageLoader) // com.compose.sdl.res/currentImageLoader.<set-currentImageLoader>|<set-currentImageLoader>(com.compose.sdl.res.ImageLoader){}[0]
@@ -9703,9 +9624,6 @@ final fun (androidx.compose.ui.graphics/Canvas).androidx.compose.ui.graphics/sca
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/compositeOver(androidx.compose.ui.graphics/Color): androidx.compose.ui.graphics/Color // androidx.compose.ui.graphics/compositeOver|compositeOver@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color){}[0] final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/compositeOver(androidx.compose.ui.graphics/Color): androidx.compose.ui.graphics/Color // androidx.compose.ui.graphics/compositeOver|compositeOver@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color){}[0]
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/luminance(): kotlin/Float // androidx.compose.ui.graphics/luminance|luminance@androidx.compose.ui.graphics.Color(){}[0] final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/luminance(): kotlin/Float // androidx.compose.ui.graphics/luminance|luminance@androidx.compose.ui.graphics.Color(){}[0]
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/toArgb(): kotlin/Int // androidx.compose.ui.graphics/toArgb|toArgb@androidx.compose.ui.graphics.Color(){}[0] final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/toArgb(): kotlin/Int // androidx.compose.ui.graphics/toArgb|toArgb@androidx.compose.ui.graphics.Color(){}[0]
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/blend(androidx.compose.ui.graphics/Color, kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/blend|blend@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color;kotlin.Float){}[0]
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/darken(kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/darken|darken@androidx.compose.ui.graphics.Color(kotlin.Float){}[0]
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/lighten(kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/lighten|lighten@androidx.compose.ui.graphics.Color(kotlin.Float){}[0]
final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/toPixelMap(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/IntArray = ..., kotlin/Int = ..., kotlin/Int = ...): androidx.compose.ui.graphics/PixelMap // androidx.compose.ui.graphics/toPixelMap|toPixelMap@androidx.compose.ui.graphics.ImageBitmap(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.IntArray;kotlin.Int;kotlin.Int){}[0] final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/toPixelMap(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/IntArray = ..., kotlin/Int = ..., kotlin/Int = ...): androidx.compose.ui.graphics/PixelMap // androidx.compose.ui.graphics/toPixelMap|toPixelMap@androidx.compose.ui.graphics.ImageBitmap(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.IntArray;kotlin.Int;kotlin.Int){}[0]
final fun (androidx.compose.ui.graphics/Matrix).androidx.compose.ui.graphics/isIdentity(): kotlin/Boolean // androidx.compose.ui.graphics/isIdentity|isIdentity@androidx.compose.ui.graphics.Matrix(){}[0] final fun (androidx.compose.ui.graphics/Matrix).androidx.compose.ui.graphics/isIdentity(): kotlin/Boolean // androidx.compose.ui.graphics/isIdentity|isIdentity@androidx.compose.ui.graphics.Matrix(){}[0]
final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addOutline(androidx.compose.ui.graphics/Outline) // androidx.compose.ui.graphics/addOutline|addOutline@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Outline){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addOutline(androidx.compose.ui.graphics/Outline) // androidx.compose.ui.graphics/addOutline|addOutline@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Outline){}[0]
@@ -9908,12 +9826,6 @@ final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/preferredFrameRate|preferredFrameRate@androidx.compose.ui.Modifier(kotlin.Float){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/preferredFrameRate|preferredFrameRate@androidx.compose.ui.Modifier(kotlin.Float){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/sensitiveContent(kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.ui/sensitiveContent|sensitiveContent@androidx.compose.ui.Modifier(kotlin.Boolean){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.ui/sensitiveContent(kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.ui/sensitiveContent|sensitiveContent@androidx.compose.ui.Modifier(kotlin.Boolean){}[0]
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/zIndex(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/zIndex|zIndex@androidx.compose.ui.Modifier(kotlin.Float){}[0] final fun (androidx.compose.ui/Modifier).androidx.compose.ui/zIndex(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/zIndex|zIndex@androidx.compose.ui.Modifier(kotlin.Float){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onDrag(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> = ..., kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> = ..., kotlin/Function0<kotlin/Unit> = ...): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onDrag|onDrag@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onMiddleClick(kotlin/Function0<kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onMiddleClick|onMiddleClick@androidx.compose.ui.Modifier(kotlin.Function0<kotlin.Unit>){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onPressed(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onPressed|onPressed@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onSecondaryClick(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onSecondaryClick|onSecondaryClick@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onTextInput(kotlin/Function1<kotlin/String, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onTextInput|onTextInput@androidx.compose.ui.Modifier(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/pressable(kotlin/Function1<kotlin/Boolean, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/pressable|pressable@androidx.compose.ui.Modifier(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
final fun (kotlin.collections/List<androidx.compose.ui.graphics.vector/PathNode>).androidx.compose.ui.graphics.vector/toPath(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics.vector/toPath|toPath@kotlin.collections.List<androidx.compose.ui.graphics.vector.PathNode>(androidx.compose.ui.graphics.Path){}[0] final fun (kotlin.collections/List<androidx.compose.ui.graphics.vector/PathNode>).androidx.compose.ui.graphics.vector/toPath(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics.vector/toPath|toPath@kotlin.collections.List<androidx.compose.ui.graphics.vector.PathNode>(androidx.compose.ui.graphics.Path){}[0]
final fun (kotlin/ByteArray).androidx.compose.ui.graphics/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0] final fun (kotlin/ByteArray).androidx.compose.ui.graphics/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0]
final fun (kotlin/CharSequence).androidx.compose.ui.text/substring(androidx.compose.ui.text/TextRange): kotlin/String // androidx.compose.ui.text/substring|substring@kotlin.CharSequence(androidx.compose.ui.text.TextRange){}[0] final fun (kotlin/CharSequence).androidx.compose.ui.text/substring(androidx.compose.ui.text/TextRange): kotlin/String // androidx.compose.ui.text/substring|substring@kotlin.CharSequence(androidx.compose.ui.text.TextRange){}[0]
@@ -10168,6 +10080,7 @@ final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0] final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner|findComposeDefaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner|findComposeDefaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.navigationevent/NavigationEventDispatcherOwner? // androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner|findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.navigationevent/NavigationEventDispatcherOwner? // androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner|findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.ui.platform/platformClipboard(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/platformClipboard|platformClipboard(){}[0]
final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter|androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(){}[0] final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter|androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(){}[0]
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter|androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(){}[0] final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter|androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(){}[0]
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter|androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(){}[0] final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter|androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(){}[0]
@@ -10303,18 +10216,6 @@ final fun com.compose.sdl.element/com_compose_sdl_element_ClipModifier$stablepro
final fun com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter|com_compose_sdl_element_ClipNode$stableprop_getter(){}[0] final fun com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter|com_compose_sdl_element_ClipNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter|com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(){}[0] final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter|com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter|com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(){}[0] final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter|com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop_getter|com_compose_sdl_element_MiddleClickModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop_getter|com_compose_sdl_element_MiddleClickNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop_getter|com_compose_sdl_element_OnDragModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop_getter|com_compose_sdl_element_OnDragNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop_getter|com_compose_sdl_element_OnPressedModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop_getter|com_compose_sdl_element_OnPressedNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop_getter|com_compose_sdl_element_OnTextInputModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop_getter|com_compose_sdl_element_OnTextInputNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop_getter|com_compose_sdl_element_PressableModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop_getter|com_compose_sdl_element_PressableNode$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop_getter|com_compose_sdl_element_SecondaryClickModifier$stableprop_getter(){}[0]
final fun com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop_getter|com_compose_sdl_element_SecondaryClickNode$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter|com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter|com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(){}[0]
@@ -10325,6 +10226,7 @@ final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectImageBitmap$s
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter|com_compose_sdl_graphics_ProjectPaint$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter|com_compose_sdl_graphics_ProjectPaint$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter|com_compose_sdl_graphics_ProjectPath$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter|com_compose_sdl_graphics_ProjectPath$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter|com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(){}[0] final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter|com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(){}[0]
final fun com.compose.sdl.graphics/decodeEncodedImageBitmap(kotlin/ByteArray): androidx.compose.ui.graphics/ImageBitmap? // com.compose.sdl.graphics/decodeEncodedImageBitmap|decodeEncodedImageBitmap(kotlin.ByteArray){}[0]
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter|com_compose_sdl_icons_IconFont$stableprop_getter(){}[0] final fun com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter|com_compose_sdl_icons_IconFont$stableprop_getter(){}[0]
final fun com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(): kotlin/Int // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter|com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(){}[0] final fun com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(): kotlin/Int // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter|com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(){}[0]
final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0] final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0]
@@ -10358,12 +10260,14 @@ final fun com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stablepro
final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0] final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0]
final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0] final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter|com_compose_sdl_AppEvent$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter|com_compose_sdl_AppEvent$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop_getter|com_compose_sdl_AppEvent_Drop$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter|com_compose_sdl_AppEvent_Key$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter|com_compose_sdl_AppEvent_Key$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter|com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter|com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter|com_compose_sdl_AppEvent_Pointer$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter|com_compose_sdl_AppEvent_Pointer$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter|com_compose_sdl_AppEvent_Quit$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter|com_compose_sdl_AppEvent_Quit$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter|com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter|com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter|com_compose_sdl_AppEvent_TextInput$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter|com_compose_sdl_AppEvent_TextInput$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop_getter|com_compose_sdl_AppEvent_WindowActivation$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter|com_compose_sdl_AppEvent_WindowClose$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter|com_compose_sdl_AppEvent_WindowClose$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter|com_compose_sdl_AppEvent_WindowResized$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter|com_compose_sdl_AppEvent_WindowResized$stableprop_getter(){}[0]
final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0]
+4 -4
View File
@@ -32,7 +32,7 @@ plugins {
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3" val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
// All targets pull headers from the in-repo static build tree at <repo>/libs, // All targets pull headers from the in-repo static build tree at <repo>/libs,
// populated by scripts/build-sdl/build-all.sh (or the per-library scripts). // populated by scripts/build-sdl/build-all.py (optionally one library at a time).
// Same paths on macOS / Linux / Windows — the .def files are pathless and // Same paths on macOS / Linux / Windows — the .def files are pathless and
// rely on these -I injections. Cross-target cinterop indexing under // rely on these -I injections. Cross-target cinterop indexing under
// kotlin.mpp.enableCInteropCommonization is fine: clang silently ignores -I // kotlin.mpp.enableCInteropCommonization is fine: clang silently ignores -I
@@ -156,9 +156,9 @@ kotlin {
api(project(":ui-geometry")) api(project(":ui-geometry"))
api(project(":ui-unit")) api(project(":ui-unit"))
api(project(":ui-backhandler")) api(project(":ui-backhandler"))
api("org.jetbrains.compose.runtime:runtime:1.11.1") api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api("org.jetbrains.compose.runtime:runtime-saveable:1.11.1") api("org.jetbrains.compose.runtime:runtime-saveable:${libs.versions.composeRuntime.get()}")
api("androidx.compose.runtime:runtime-retain:1.11.1") api("androidx.compose.runtime:runtime-retain:${libs.versions.composeRuntime.get()}")
api("androidx.navigationevent:navigationevent-compose:1.1.2") api("androidx.navigationevent:navigationevent-compose:1.1.2")
api("androidx.savedstate:savedstate:1.5.0") api("androidx.savedstate:savedstate:1.5.0")
api("androidx.savedstate:savedstate-compose:1.5.0") api("androidx.savedstate:savedstate-compose:1.5.0")
+11 -8
View File
@@ -514,14 +514,14 @@ skikoMain/kotlin/ -> src/vendor/skikoRenderer/kotlin/
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt # | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt
# | skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt # | skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt
# | skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt # | skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt # B6.1 (2026-07-16): un-refused — skiko leg adopts upstream graphics actuals.
# SkiaBackedCanvas is then MANUAL-VENDORED (moved to src/skikoRendererMain, re-refused
# below) to also implement the port Native* draw contracts. See RENDERER_TASKS.md B6.1.
# B6.2: un-refused — skiko leg adopts upstream GraphicsLayer/GraphicsContext (real
# skiko RenderNode) now the canvas is SkiaBackedCanvas. See RENDERER_TASKS.md B6.2.
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt !skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt # B6.1: MANUAL-VENDORED (flattened toBitmap/putBytesInto expect-actuals — see the file).
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt !skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt
!skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt
SET_FOLDER=compose/ui/ui-text/src SET_FOLDER=compose/ui/ui-text/src
@@ -757,11 +757,13 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
# | androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt # | androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
# | androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt # | androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
# | androidMain/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt # | androidMain/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt
# desktopMain (173) # desktopMain (175)
# | desktopMain/kotlin/androidx/compose/ui/Actuals.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/Actuals.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/Actuals.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/Actuals.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt # | desktopMain/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt
# | desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/DesktopComposeUiFlags.skiko.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/DesktopComposeUiFlags.skiko.kt
# | desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt # | desktopMain/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt
@@ -1092,7 +1094,7 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
# | wasmJsMain/kotlin/androidx/compose/ui/Actuals.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/Actuals.wasm.kt # | wasmJsMain/kotlin/androidx/compose/ui/Actuals.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/Actuals.wasm.kt
# | wasmJsMain/kotlin/androidx/compose/ui/internal/System.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/internal/System.wasm.kt # | wasmJsMain/kotlin/androidx/compose/ui/internal/System.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/internal/System.wasm.kt
# | wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt # | wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt
# webMain (39) # webMain (40)
# | webMain/kotlin/androidx/compose/ui/dom/Events.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/dom/Events.web.kt # | webMain/kotlin/androidx/compose/ui/dom/Events.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/dom/Events.web.kt
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt # | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt # | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt
@@ -1129,6 +1131,7 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
# | webMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt # | webMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt # | webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt # | webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt
# | webMain/kotlin/androidx/compose/ui/window/RootScrollObserver.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/RootScrollObserver.kt
# | webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt # | webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt
# | webMain/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt # | webMain/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt
# | webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebTextInputSession.kt # | webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebTextInputSession.kt
@@ -10,6 +10,7 @@
* Line-for-line kept in sync; the entry in compose/ui/compose-fork.txt is * Line-for-line kept in sync; the entry in compose/ui/compose-fork.txt is
* commented out so this file survives `scripts/compose-fork/sync.sh`. * commented out so this file survives `scripts/compose-fork/sync.sh`.
*/ */
// VENDOR-BASE: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Synchronization.kt @ v1.12.0-beta03+dev4483
package androidx.compose.ui.platform package androidx.compose.ui.platform
@@ -6,6 +6,7 @@
* compose/ui/compose-fork.txt is commented out so this file survives * compose/ui/compose-fork.txt is commented out so this file survives
* `scripts/compose-fork/sync.sh`. * `scripts/compose-fork/sync.sh`.
*/ */
// VENDOR-BASE: compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/Synchronization.kt @ v1.12.0-beta03+dev4483
package androidx.compose.ui.text.platform package androidx.compose.ui.text.platform
@@ -1,97 +0,0 @@
package com.compose.sdl.element
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.node.ModifierNodeElement
// ==================
// MARK: Project modifier elements
// ==================
// Small set of project-only modifier elements — each pairs a
// `ModifierNodeElement<XxxNode>` with a `XxxNode : Modifier.Node`. Upstream
// LayoutNode reads them through the chain (ModifierNodeElement IS-A
// Modifier.Element). Everything gesture-related has been removed — pointer
// input, text input, right/middle click, drag: all go through the standard
// `Modifier.pointerInput` + `detectTapGestures` / `detectDragGestures` /
// `awaitPointerEventScope` APIs upstream now supplies. What's left is the
// two elements the renderer still owns directly:
// * ClipModifier — GraphicsLayer.kt lowers `clip = true` to it.
// * GraphicsLayerModifier — the transform / alpha / cache pipeline.
class ClipModifier(val shape: Shape) : ModifierNodeElement<ClipNode>() {
override fun create() = ClipNode(shape)
override fun update(node: ClipNode) { node.shape = shape }
override fun hashCode(): Int = shape.hashCode()
override fun equals(other: Any?): Boolean = other is ClipModifier && other.shape == shape
}
class ClipNode(var shape: Shape) : Modifier.Node()
// ==================
// MARK: GraphicsLayerModifier
// ==================
/**
* A "graphics layer" element: alpha + 2D transform (scale / rotation /
* translation), with an optional cacheKey that opts the subtree into
* render-to-texture caching across frames. See `Modifier.graphicsLayer`
* (in `androidx.compose.ui.graphics`) for the caching semantics.
*
* The renderer reads this element directly via the `LayoutNode.graphicsLayer`
* `foldIn` over the chain; the paired [GraphicsLayerNode] lifecycle stays
* dormant until the renderer rewrite drives it.
*/
class GraphicsLayerModifier(
val alpha: Float = 1f,
val scaleX: Float = 1f,
val scaleY: Float = 1f,
val rotationZ: Float = 0f,
val translationX: Float = 0f,
val translationY: Float = 0f,
val transformOrigin: TransformOrigin = TransformOrigin.Center,
val cacheKey: Any? = null,
) : ModifierNodeElement<GraphicsLayerNode>() {
val needsLayer: Boolean
get() = alpha < 1f || cacheKey != null
val needsTransform: Boolean
get() = scaleX != 1f || scaleY != 1f || rotationZ != 0f ||
translationX != 0f || translationY != 0f
val isIdentity: Boolean
get() = !needsLayer && !needsTransform
override fun create() =
GraphicsLayerNode(alpha, scaleX, scaleY, rotationZ, translationX, translationY, transformOrigin, cacheKey)
override fun update(node: GraphicsLayerNode) {
node.alpha = alpha; node.scaleX = scaleX; node.scaleY = scaleY
node.rotationZ = rotationZ; node.translationX = translationX; node.translationY = translationY
node.transformOrigin = transformOrigin; node.cacheKey = cacheKey
}
override fun hashCode(): Int {
var v = alpha.hashCode()
v = 31 * v + scaleX.hashCode(); v = 31 * v + scaleY.hashCode()
v = 31 * v + rotationZ.hashCode()
v = 31 * v + translationX.hashCode(); v = 31 * v + translationY.hashCode()
v = 31 * v + transformOrigin.hashCode(); v = 31 * v + (cacheKey?.hashCode() ?: 0)
return v
}
override fun equals(other: Any?): Boolean =
other is GraphicsLayerModifier &&
other.alpha == alpha && other.scaleX == scaleX && other.scaleY == scaleY &&
other.rotationZ == rotationZ &&
other.translationX == translationX && other.translationY == translationY &&
other.transformOrigin == transformOrigin && other.cacheKey == cacheKey
}
class GraphicsLayerNode(
var alpha: Float,
var scaleX: Float,
var scaleY: Float,
var rotationZ: Float,
var translationX: Float,
var translationY: Float,
var transformOrigin: TransformOrigin,
var cacheKey: Any?,
) : Modifier.Node()
@@ -1,17 +0,0 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.layer.GraphicsLayer
// ==================
// MARK: GraphicsLayer factory (project bridge)
// ==================
/* The vendored `expect class GraphicsLayer` exposes neither a constructor nor
the isReleased setter to common code — upstream owners create layers from
platform source sets. ComposeOwner lives in commonMain, so these two hops
let its GraphicsContext create and release the project record/replay
actual (GraphicsLayer.native.kt). */
internal expect fun createProjectGraphicsLayer(): GraphicsLayer
internal expect fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer)
@@ -0,0 +1,17 @@
package com.compose.sdl.graphics
// ==================
// MARK: NativeFinishableCanvas
// ==================
/*
A renderer canvas that must be flushed once drawing into it is done — SDL batches
geometry and only submits on flush; an offscreen SDL canvas must flush before its
backing texture is read. Implemented by Sdl3Canvas (real flush) and SkiaCanvas
(no-op — Skia save/restore is balanced per call). Lets renderer-agnostic code
(e.g. GraphicsLayer.toImageBitmap) commit an offscreen render without knowing the
concrete canvas type.
*/
interface NativeFinishableCanvas {
fun finish()
}
@@ -44,6 +44,14 @@ interface NativeTextCanvas {
// (underline / line-through). Per-run overrides ride in via inSpans. // (underline / line-through). Per-run overrides ride in via inSpans.
inBaseItalic: Boolean = false, inBaseItalic: Boolean = false,
inTextDecoration: TextDecoration? = null, inTextDecoration: TextDecoration? = null,
// The paragraph's per-line band height in px (TextStyle.lineHeight when the
// style specifies one — SdlParagraph passes it so glyph rows stack exactly
// where layout put the line boxes). <= 0 → derive from font metrics (legacy
// behaviour; icon callers and TextDrawNode don't pass it).
inLineHeightPx: Float = 0f,
// Compat-trim (raw styles): the FIRST line keeps the tight font cell. False
// for M3's LineHeightStyle(Trim.None) — every line is the full band.
inTrimFirstLine: Boolean = true,
) )
} }
@@ -6,7 +6,7 @@ headerFilter = SDL3/**
# archive location comes from :ui build.gradle.kts (-libraryPath) because # archive location comes from :ui build.gradle.kts (-libraryPath) because
# .def files can't reference $rootDir. The linker options below list the # .def files can't reference $rootDir. The linker options below list the
# system frameworks / libs SDL3's static build depends on (see the Libs # system frameworks / libs SDL3's static build depends on (see the Libs
# line in libs/SDL3/lib/pkgconfig/sdl3.pc after build-sdl3.sh runs) — # line in libs/SDL3/lib/pkgconfig/sdl3.pc after build-all.py runs) —
# cinterop propagates them through the klib to the final app link. # cinterop propagates them through the klib to the final app link.
staticLibraries = libSDL3.a staticLibraries = libSDL3.a
@@ -20,8 +20,13 @@ linkerOpts.osx = \
-framework QuartzCore -weak_framework CoreHaptics \ -framework QuartzCore -weak_framework CoreHaptics \
-lpthread -lm -lpthread -lm
linkerOpts.linux_x64 = -lpthread -ldl -lm -lrt # The -L multi-arch dirs + fontconfig/GL/X11 serve the SKIA renderer (font
linkerOpts.linux_arm64 = -lpthread -ldl -lm -lrt # matching, GL backend, windowing) — K/N's LLD sysroot doesn't include the
# host's multi-arch dir, and carrying them here means apps (including bridge
# consumers) need NO linker flags of their own. Unused under -Prenderer=sdl3;
# the extra -l is harmless, and LLD skips missing -L dirs.
linkerOpts.linux_x64 = -L/usr/lib/x86_64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
linkerOpts.linux_arm64 = -L/usr/lib/aarch64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
linkerOpts.mingw_x64 = \ linkerOpts.mingw_x64 = \
-lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 \ -lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 \
@@ -5,6 +5,6 @@ depends = sdl3
# libSDL3_image.a + its vendored codec archives (PNG/JPG built-in via stb, # libSDL3_image.a + its vendored codec archives (PNG/JPG built-in via stb,
# WEBP, SVG built-in) are all baked into the cinterop klib. See sdl3.def — # WEBP, SVG built-in) are all baked into the cinterop klib. See sdl3.def —
# the static archive location is injected via :ui build.gradle.kts # the static archive location is injected via :ui build.gradle.kts
# (-libraryPath). build-sdl3-image.sh normalises libzlibstatic.a → libz.a # (-libraryPath). build-all.py's sdl3-image step normalises libzlibstatic.a
# on Windows so the same filename works everywhere. # → libz.a on Windows so the same filename works everywhere.
staticLibraries = libSDL3_image.a libpng16.a libz.a libwebp.a libwebpdemux.a libwebpmux.a libsharpyuv.a staticLibraries = libSDL3_image.a libpng16.a libz.a libwebp.a libwebpdemux.a libwebpmux.a libsharpyuv.a
@@ -1,161 +0,0 @@
package androidx.compose.ui.graphics.layer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
// ==================
// MARK: GraphicsLayer — native no-op actual
// ==================
//
// Upstream's skiko actual (SkiaGraphicsLayer.skiko.kt, 513L) uses Skia's
// Picture/Canvas API for record + replay. We don't have skiko on the
// SDL3 path and the Skia path doesn't route through GraphicsLayer yet
// — the renderer's per-node draw does its own composition. So this
// actual is a plain field bag with no rendering behavior: setters store
// their values, getters return them, record/draw/toImageBitmap are
// no-ops. Nothing in our pipeline constructs a GraphicsLayer yet.
@Suppress("PropertyName", "unused")
actual class GraphicsLayer {
actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
actual var topLeft: IntOffset = IntOffset.Zero
actual var size: IntSize = IntSize.Zero
actual var pivotOffset: Offset = Offset.Unspecified
actual var alpha: Float = 1f
actual var scaleX: Float = 1f
actual var scaleY: Float = 1f
actual var translationX: Float = 0f
actual var translationY: Float = 0f
actual var shadowElevation: Float = 0f
actual var ambientShadowColor: Color = Color.Black
actual var spotShadowColor: Color = Color.Black
actual var blendMode: BlendMode = BlendMode.SrcOver
actual var colorFilter: ColorFilter? = null
private var outlineField: Outline = Outline.Rectangle(androidx.compose.ui.geometry.Rect.Zero)
actual val outline: Outline get() = outlineField
actual fun setOutsets(left: Int, top: Int, right: Int, bottom: Int) { /* no-op */ }
actual fun setPathOutline(path: Path) { outlineField = Outline.Generic(path) }
actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) {
outlineField = Outline.Rounded(
androidx.compose.ui.geometry.RoundRect(
left = topLeft.x, top = topLeft.y,
right = topLeft.x + size.width, bottom = topLeft.y + size.height,
cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius),
)
)
}
actual fun setRectOutline(topLeft: Offset, size: Size) {
val effTopLeft = if (topLeft == Offset.Unspecified) Offset.Zero else topLeft
val effSize = if (size == Size.Unspecified) this.size.let { Size(it.width.toFloat(), it.height.toFloat()) } else size
outlineField = Outline.Rectangle(
androidx.compose.ui.geometry.Rect(
effTopLeft.x, effTopLeft.y,
effTopLeft.x + effSize.width, effTopLeft.y + effSize.height,
)
)
}
actual var rotationX: Float = 0f
actual var rotationY: Float = 0f
actual var rotationZ: Float = 0f
actual var cameraDistance: Float = DefaultCameraDistance
actual var clip: Boolean = false
actual var renderEffect: RenderEffect? = null
actual var isReleased: Boolean = false
internal set
// ============
// Record / draw — lambda replay
//
// Upstream's skiko actual records into a Skia Picture. This renderer is
// immediate-mode, so record() stores the draw lambda and draw() REPLAYS it
// with the layer's transform / alpha / clip applied. Same visual result as
// a display list as long as callers re-record on invalidation (they do —
// that's the GraphicsLayer contract). This is what makes GraphicsContext
// consumers render: SharedTransitionLayout's overlay, rememberGraphicsLayer.
private var recordedBlock: (DrawScope.() -> Unit)? = null
private var recordedDensity: Density = Density(1f)
private var recordedLayoutDirection: LayoutDirection = LayoutDirection.Ltr
private val drawScope = androidx.compose.ui.graphics.drawscope.CanvasDrawScope()
actual fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit,
) {
recordedDensity = density
recordedLayoutDirection = layoutDirection
this.size = size
recordedBlock = block
}
actual suspend fun toImageBitmap(): ImageBitmap =
throw NotImplementedError("GraphicsLayer.toImageBitmap not implemented on desktop")
internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) {
if (isReleased) return
val vBlock = recordedBlock ?: return
canvas.save()
canvas.translate(topLeft.x.toFloat(), topLeft.y.toFloat())
// Transform about the pivot (defaults to the layer centre).
if (scaleX != 1f || scaleY != 1f || rotationZ != 0f || translationX != 0f || translationY != 0f) {
val vPivot =
if (pivotOffset == Offset.Unspecified) Offset(size.width / 2f, size.height / 2f)
else pivotOffset
canvas.translate(translationX + vPivot.x, translationY + vPivot.y)
if (scaleX != 1f || scaleY != 1f) canvas.scale(scaleX, scaleY)
if (rotationZ != 0f) canvas.rotate(rotationZ)
canvas.translate(-vPivot.x, -vPivot.y)
}
if (clip) {
// Rect/rounded outlines clip to their bounds (SDL clips are rects;
// rounded-corner exactness would need the offscreen mask path);
// generic outlines go through clipPath (bbox fallback on SDL).
when (val vOutline = outline) {
is Outline.Rectangle -> canvas.clipRect(vOutline.rect)
is Outline.Rounded -> canvas.clipRect(
androidx.compose.ui.geometry.Rect(
vOutline.roundRect.left, vOutline.roundRect.top,
vOutline.roundRect.right, vOutline.roundRect.bottom,
),
)
is Outline.Generic -> canvas.clipPath(vOutline.path)
}
}
val vNeedsAlphaLayer = alpha < 1f
if (vNeedsAlphaLayer) {
canvas.saveLayer(
androidx.compose.ui.geometry.Rect(0f, 0f, size.width.toFloat(), size.height.toFloat()),
androidx.compose.ui.graphics.Paint().apply { this.alpha = this@GraphicsLayer.alpha },
)
}
drawScope.draw(
recordedDensity,
recordedLayoutDirection,
canvas,
Size(size.width.toFloat(), size.height.toFloat()),
vBlock,
)
if (vNeedsAlphaLayer) canvas.restore()
canvas.restore()
}
}
@@ -0,0 +1,437 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// MANUALLY VENDORED (NON-IDEMPOTENT) from compose-multiplatform-core:
// compose/ui/ui/src/skikoMain/.../node/GraphicsLayerOwnerLayer.skiko.kt
// VENDOR-BASE: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt @ v1.12.0-beta03+dev4483
// Change vs upstream: dropped the trailing `SkiaGraphicsContext.setLightingInfo`
// extension + LIGHT_* constants (skiko-only shadow lighting; our shadows go via
// NativeShadowCanvas). The class body is unchanged — it is the real OwnedLayer
// (dirty-gated record/replay over a per-renderer GraphicsLayer), replacing the
// immediate-mode ProjectOwnedLayer. `!`-refused in compose-fork.txt; reconcile
// upstream changes by hand (run scripts/compose-fork/check-vendor-drift.py at each
// COMPOSE_CORE_REF bump). See RENDERER.md Appendix A.
package androidx.compose.ui.node
import androidx.compose.ui.FrameRateCategory
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Fields
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.isIdentity
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.layer.setOutline
import com.compose.sdl.graphics.prepareLayerTransformationMatrix
import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.internal.requirePrecondition
import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.platform.isInOutline
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
/**
* A class representing a layer that owns and manages a `GraphicsLayer` for composable rendering.
*
* This layer is responsible for managing graphical properties, transformations, and rendering
* tasks associated with a `GraphicsLayer`. It provides mechanisms for updating layer properties,
* triggering invalidations, resizing, and mapping offsets or bounds using transformation matrices.
*
* @constructor
* @param graphicsLayer The initial `GraphicsLayer` for this layer.
* @param context The graphics context responsible for managing this layer, or `null` if externally
* managed. When context is not null, it means the object is created internally, and we need to
* release it during disposal.
* @param layerManager The manager responsible for handling the ownership and lifecycle of this layer.
* @param drawBlock The lambda function invoked to perform custom drawing on the canvas.
* @param invalidateParentLayer Callback to invalidate the parent layer when necessary.
*/
internal class GraphicsLayerOwnerLayer(
graphicsLayer: GraphicsLayer,
private val context: GraphicsContext?,
private val layerManager: OwnedLayerManager,
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit,
) : OwnedLayer {
internal var graphicsLayer: GraphicsLayer = graphicsLayer
private set
private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
private var size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
private var density = Density(1f)
private var layoutDirection = LayoutDirection.Ltr
private val scope = CanvasDrawScope()
private var mutatedFields: Int = 0
private var transformOrigin: TransformOrigin = TransformOrigin.Center
private var outline: Outline? = null
private val matrixCache = Matrix()
private var inverseMatrixCache: Matrix? = null
private var isDestroyed = false
private var isDirty = true
set(value) {
if (value != field) {
field = value
layerManager.notifyLayerIsDirty(this, value)
}
}
private var isMatrixDirty = false
private var isInverseMatrixDirty = false
private var isIdentity = true
override var frameRate: Float = 0f
override var isFrameRateFromParent = false
override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
val maybeChangedFields = scope.mutatedFields or mutatedFields
this.layoutDirection = scope.layoutDirection
this.density = scope.graphicsDensity
if (maybeChangedFields and Fields.Outsets != 0) {
with(density) {
graphicsLayer.setOutsets(
left = scope.outsets.left.roundToPx(),
top = scope.outsets.top.roundToPx(),
right = scope.outsets.right.roundToPx(),
bottom = scope.outsets.bottom.roundToPx(),
)
invalidate()
}
}
if (maybeChangedFields and Fields.TransformOrigin != 0) {
this.transformOrigin = scope.transformOrigin
}
if (maybeChangedFields and Fields.ScaleX != 0) {
graphicsLayer.scaleX = scope.scaleX
}
if (maybeChangedFields and Fields.ScaleY != 0) {
graphicsLayer.scaleY = scope.scaleY
}
if (maybeChangedFields and Fields.Alpha != 0) {
graphicsLayer.alpha = scope.alpha
}
if (maybeChangedFields and Fields.TranslationX != 0) {
graphicsLayer.translationX = scope.translationX
}
if (maybeChangedFields and Fields.TranslationY != 0) {
graphicsLayer.translationY = scope.translationY
}
if (maybeChangedFields and Fields.ShadowElevation != 0) {
graphicsLayer.shadowElevation = scope.shadowElevation
}
if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
graphicsLayer.ambientShadowColor = scope.ambientShadowColor
}
if (maybeChangedFields and Fields.SpotShadowColor != 0) {
graphicsLayer.spotShadowColor = scope.spotShadowColor
}
if (maybeChangedFields and Fields.RotationZ != 0) {
graphicsLayer.rotationZ = scope.rotationZ
}
if (maybeChangedFields and Fields.RotationX != 0) {
graphicsLayer.rotationX = scope.rotationX
}
if (maybeChangedFields and Fields.RotationY != 0) {
graphicsLayer.rotationY = scope.rotationY
}
if (maybeChangedFields and Fields.CameraDistance != 0) {
graphicsLayer.cameraDistance = scope.cameraDistance
}
if (maybeChangedFields and Fields.TransformOrigin != 0) {
if (transformOrigin == TransformOrigin.Center) {
graphicsLayer.pivotOffset = Offset.Unspecified
} else {
graphicsLayer.pivotOffset =
Offset(
transformOrigin.pivotFractionX * size.width,
transformOrigin.pivotFractionY * size.height
)
}
}
if (maybeChangedFields and Fields.Clip != 0) {
graphicsLayer.clip = scope.clip
}
if (maybeChangedFields and Fields.RenderEffect != 0) {
graphicsLayer.renderEffect = scope.renderEffect
}
if (maybeChangedFields and Fields.ColorFilter != 0) {
graphicsLayer.colorFilter = scope.colorFilter
}
if (maybeChangedFields and Fields.BlendMode != 0) {
graphicsLayer.blendMode = scope.blendMode
}
if (maybeChangedFields and Fields.CompositingStrategy != 0) {
graphicsLayer.compositingStrategy =
when (scope.compositingStrategy) {
CompositingStrategy.Auto -> androidx.compose.ui.graphics.layer.CompositingStrategy.Auto
CompositingStrategy.Offscreen -> androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
CompositingStrategy.ModulateAlpha -> androidx.compose.ui.graphics.layer.CompositingStrategy.ModulateAlpha
else -> throw IllegalStateException("Not supported composition strategy")
}
}
if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
isMatrixDirty = true
isInverseMatrixDirty = true
}
var outlineChanged = false
if (outline != scope.outline) {
outlineChanged = true
outline = scope.outline
updateOutline()
}
mutatedFields = scope.mutatedFields
if (maybeChangedFields != 0 || outlineChanged) {
triggerRepaint()
layerManager.voteFrameRate(frameRate)
}
}
/**
* Triggers redrawing of Compose content during the next frame.
*/
private fun triggerRepaint() {
layerManager.invalidate()
}
private fun updateOutline() {
val outline = outline ?: return
graphicsLayer.setOutline(outline)
}
override fun isInLayer(position: Offset): Boolean {
val x = position.x
val y = position.y
if (graphicsLayer.clip) {
return isInOutline(graphicsLayer.outline, x, y)
}
return true
}
override fun move(position: IntOffset) {
layerManager.voteFrameRate(FrameRateCategory.High.value)
graphicsLayer.topLeft = position
triggerRepaint()
}
override fun resize(size: IntSize) {
if (size != this.size) {
layerManager.voteFrameRate(FrameRateCategory.High.value)
this.size = size
invalidate()
}
}
override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
updateDisplayList()
scope.drawContext.also {
it.canvas = canvas
it.graphicsLayer = parentLayer
}
scope.drawLayer(graphicsLayer)
}
override fun updateDisplayList() {
layerManager.voteFrameRate(frameRate)
if (isDirty) {
if (transformOrigin != TransformOrigin.Center && graphicsLayer.size != size) {
graphicsLayer.pivotOffset =
Offset(
transformOrigin.pivotFractionX * size.width,
transformOrigin.pivotFractionY * size.height
)
}
graphicsLayer.record(density, layoutDirection, size, recordLambda)
isDirty = false
}
}
private val recordLambda: DrawScope.() -> Unit = {
drawIntoCanvas { canvas ->
this@GraphicsLayerOwnerLayer.drawBlock?.let { it(canvas, drawContext.graphicsLayer) }
}
}
/**
* Marks content as dirty and triggers redrawing.
*/
override fun invalidate() {
if (isDestroyed) return
isDirty = true
layerManager.invalidate()
}
override fun destroy() {
frameRate = 0f
isFrameRateFromParent = false
drawBlock = null
invalidateParentLayer = null
isDestroyed = true
isDirty = false
if (context != null) {
context.releaseGraphicsLayer(graphicsLayer)
// Recycle only in case of non-null context (meaning only for not external layers).
layerManager.recycle(this)
}
}
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
val matrix =
if (inverse) {
getInverseMatrix() ?: return Offset.Infinite
} else {
getMatrix()
}
return if (isIdentity) {
point
} else {
matrix.map(point)
}
}
override fun mapBounds(rect: MutableRect, inverse: Boolean) {
val matrix = if (inverse) getInverseMatrix() else getMatrix()
if (!isIdentity) {
if (matrix == null) {
rect.set(0f, 0f, 0f, 0f)
} else {
matrix.map(rect)
}
}
}
override fun reuseLayer(
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit
) {
val context =
checkPreconditionNotNull(context) {
"currently reuse is only supported when we manage the layer lifecycle"
}
requirePrecondition(graphicsLayer.isReleased) {
"layer should have been released before reuse"
}
// recreate a layer
graphicsLayer = context.createGraphicsLayer()
isDestroyed = false
// apply new params
this.drawBlock = drawBlock
this.invalidateParentLayer = invalidateParentLayer
// reset mutable variables to their initial values
isMatrixDirty = false
isInverseMatrixDirty = false
isIdentity = true
matrixCache.reset()
inverseMatrixCache?.reset()
transformOrigin = TransformOrigin.Center
size = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
outline = null
mutatedFields = 0
}
override fun transform(matrix: Matrix) {
matrix.timesAssign(getMatrix())
}
override val underlyingMatrix: Matrix
get() = getMatrix()
override fun inverseTransform(matrix: Matrix) {
val inverse = getInverseMatrix()
if (inverse != null) {
matrix.timesAssign(inverse)
}
}
private fun getMatrix(): Matrix {
updateMatrix()
return matrixCache
}
private fun getInverseMatrix(): Matrix? {
val matrix = getMatrix()
if (isIdentity) {
return matrix
}
val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it }
if (!isInverseMatrixDirty) {
return if (inverseMatrix[0, 0].isNaN()) {
null
} else {
inverseMatrix
}
}
isInverseMatrixDirty = false
return if (matrix.invertTo(inverseMatrix)) {
inverseMatrix
} else {
inverseMatrix[0, 0] = Float.NaN
null
}
}
private fun updateMatrix() {
if (!isMatrixDirty) return
with(graphicsLayer) {
val pivotX: Float
val pivotY: Float
if (pivotOffset.isUnspecified) {
pivotX = this@GraphicsLayerOwnerLayer.size.width / 2f
pivotY = this@GraphicsLayerOwnerLayer.size.height / 2f
} else {
pivotX = pivotOffset.x
pivotY = pivotOffset.y
}
prepareLayerTransformationMatrix(
matrix = matrixCache,
pivotX = pivotX,
pivotY = pivotY,
translationX = translationX,
translationY = translationY,
rotationX = rotationX,
rotationY = rotationY,
rotationZ = rotationZ,
scaleX = scaleX,
scaleY = scaleY,
cameraDistance = cameraDistance
)
}
isMatrixDirty = false
isIdentity = matrixCache.isIdentity()
}
}
@@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.isUnspecified import androidx.compose.ui.unit.isUnspecified
import com.compose.sdl.text.NativeTextCanvas import com.compose.sdl.text.NativeTextCanvas
import com.compose.sdl.text.currentTextMeasurer import com.compose.sdl.text.currentTextMeasurer
@@ -73,7 +74,32 @@ internal class SdlParagraph(
private val wrapped = currentTextMeasurer.wrap(text, fontPx, maxWidthPx, family, variations) private val wrapped = currentTextMeasurer.wrap(text, fontPx, maxWidthPx, family, variations)
private val allLines: List<String> = wrapped.lines private val allLines: List<String> = wrapped.lines
private val lineStarts: IntArray = wrapped.lineStarts private val lineStarts: IntArray = wrapped.lineStarts
private val lh: Float = currentTextMeasurer.lineHeight(fontPx, family, variations).coerceAtLeast(1f)
// TextStyle.lineHeight in px — upstream Skia paragraphs honour it (every M3
// Typography style sets one, e.g. bodyMedium 14sp/20sp), so ignoring it made every
// text block's height differ from upstream by ~1px per WRAPPED line, an error that
// ACCUMULATED down the page (the dominant term in every parity diff). sp scales by
// density like fontPx; em multiplies the font size.
private val styleLineHeightPx: Float? = when {
style.lineHeight.isUnspecified -> null
style.lineHeight.type == TextUnitType.Sp -> style.lineHeight.value * density
style.lineHeight.type == TextUnitType.Em -> style.lineHeight.value * fontPx
else -> null
// Upstream ignores a lineHeight NOT STRICTLY GREATER than the font size (skia
// only applies height multipliers > 1em) — probe: 48sp/24lh -> the 65px cell,
// and 24sp/24lh (h = 1.0 exactly) -> the 33px cell, while 24sp/25lh -> 25.
}?.takeIf { it > fontPx }
// How the band applies depends on TextStyle.lineHeightStyle (probe-verified vs JVM):
// - unspecified (raw BasicText): COMPAT TRIM — the first line keeps the tight font
// cell, lineHeight is the advance between baselines. n lines = cell + (n-1)*lh.
// - Trim.None (every M3 Typography style: Center/None/Fixed): UNIFORM bands — every
// line is exactly lineHeight, single-line included (even below the cell).
private val uniformBands: Boolean =
style.lineHeightStyle?.trim == androidx.compose.ui.text.style.LineHeightStyle.Trim.None
private val fontCellH: Float =
currentTextMeasurer.lineHeight(fontPx, family, variations).coerceAtLeast(1f)
private val lh: Float = styleLineHeightPx ?: fontCellH
private val firstLineH: Float = if (uniformBands && styleLineHeightPx != null) lh else fontCellH
// True when a span changes glyph METRICS (fontSize / fontWeight) — gates // True when a span changes glyph METRICS (fontSize / fontWeight) — gates
// the styled measurement paths so plain/colour-only text keeps the cheap // the styled measurement paths so plain/colour-only text keeps the cheap
@@ -103,10 +129,14 @@ internal class SdlParagraph(
// Per-line box heights: base line height unless a size span makes a line's // Per-line box heights: base line height unless a size span makes a line's
// tallest run cell bigger. lineTops[i] = cumulative top; last entry = height. // tallest run cell bigger. lineTops[i] = cumulative top; last entry = height.
private val lineHeights: FloatArray by lazy { private val lineHeights: FloatArray by lazy {
if (!metricSpans) FloatArray(lineCount) { lh } if (!metricSpans) FloatArray(lineCount) { if (it == 0) firstLineH else lh }
else FloatArray(lineCount) { else FloatArray(lineCount) {
styledLineCellHeight(allLines[it], lineStarts[it], spanStyles, fontPx, density, currentTextMeasurer, family, variations) // A size span can push a line's cell above the style's line height, but
.coerceAtLeast(1f) // never below it (first line keeps its tight cell under compat trim).
max(
styledLineCellHeight(allLines[it], lineStarts[it], spanStyles, fontPx, density, currentTextMeasurer, family, variations),
if (it == 0 && !uniformBands) 0f else (styleLineHeightPx ?: 0f),
).coerceAtLeast(1f)
} }
} }
private val lineTops: FloatArray by lazy { private val lineTops: FloatArray by lazy {
@@ -118,7 +148,9 @@ internal class SdlParagraph(
override val width: Float by lazy { override val width: Float by lazy {
if (maxWidthPx == Int.MAX_VALUE) (lineWidths.maxOrNull() ?: 0f) else widthConstraint if (maxWidthPx == Int.MAX_VALUE) (lineWidths.maxOrNull() ?: 0f) else widthConstraint
} }
override val height: Float by lazy { if (!metricSpans) lineCount * lh else lineTops[lineCount] } override val height: Float by lazy {
if (!metricSpans) firstLineH + (lineCount - 1) * lh else lineTops[lineCount]
}
// Widest single HARD-BREAK line — NOT the concatenated all-on-one-line width. // Widest single HARD-BREAK line — NOT the concatenated all-on-one-line width.
// Compose's Text(softWrap = false) reads this to decide the paragraph width // Compose's Text(softWrap = false) reads this to decide the paragraph width
// (LayoutUtils.finalMaxWidth returns Constraints.Infinity when softWrap=false, // (LayoutUtils.finalMaxWidth returns Constraints.Infinity when softWrap=false,
@@ -193,8 +225,9 @@ internal class SdlParagraph(
} }
override fun getLineForVerticalPosition(vertical: Float): Int { override fun getLineForVerticalPosition(vertical: Float): Int {
if (!metricSpans) return (vertical / lh).toInt().coerceIn(0, lineCount - 1)
// Cumulative tops are monotonic — walk to the line containing `vertical`. // Cumulative tops are monotonic — walk to the line containing `vertical`.
// (No uniform-divide fast path anymore: the first line's box is the font
// cell while later lines use the style lineHeight.)
for (i in 0 until lineCount) if (vertical < lineTops[i + 1]) return i for (i in 0 until lineCount) if (vertical < lineTops[i + 1]) return i
return lineCount - 1 return lineCount - 1
} }
@@ -368,6 +401,8 @@ internal class SdlParagraph(
inFontVariations = variations, inFontVariations = variations,
inBaseItalic = style.fontStyle == androidx.compose.ui.text.font.FontStyle.Italic, inBaseItalic = style.fontStyle == androidx.compose.ui.text.font.FontStyle.Italic,
inTextDecoration = inDecoration ?: style.textDecoration, inTextDecoration = inDecoration ?: style.textDecoration,
inLineHeightPx = lh,
inTrimFirstLine = !uniformBands || styleLineHeightPx == null,
) )
} }
@@ -31,11 +31,13 @@ interface RenderBackend {
so it maps 1:1 onto the pixel back buffer. */ so it maps 1:1 onto the pixel back buffer. */
fun beginFrame(inDpr: Float) fun beginFrame(inDpr: Float)
/* Draw the upstream LayoutNode tree owned by [inHost] through the vendored /* Build this frame's platform Canvas and hand it to [inDraw], which walks the
coordinator / DrawModifierNode pipeline (LayoutNode.draw → CanvasDrawScope composition (host.drawRoot(canvas)) through the vendored coordinator /
→ the platform Canvas backend). Default no-op — the Skia backend will DrawModifierNode pipeline. Taking a (Canvas)->Unit instead of the host keeps
override once SkiaCanvas lands (see NODE_ENGINE_PORT.md Phase 9). */ RenderBackend + its implementations independent of the node/host layer — the
fun drawRoot(inHost: com.compose.sdl.node.ComposeRootHost) {} decoupling that lets the renderers live in :ui-graphics (no ui-graphics→ui
cycle). Default no-op. */
fun drawRoot(inDraw: (canvas: androidx.compose.ui.graphics.Canvas) -> Unit) {}
/* Flush + present whatever was just drawn. */ /* Flush + present whatever was just drawn. */
fun endFrame() fun endFrame()
@@ -0,0 +1,24 @@
package com.compose.sdl
// ==================
// MARK: Renderer entry points (expect)
// ==================
/* The renderer seam :window builds against. Each renderer source set
(skikoRendererMain / sdlRendererMain) supplies the actuals; exactly one of
the two is attached to any given target, so resolution stays unambiguous.
These were originally declared identically in both renderer source sets
with NO expect — fine per-target, but shared nativeMain METADATA (=: what
:window's KotlinMultiplatform publication compiles against) could not see
them on a host where the attached targets span BOTH renderers (Windows:
skikoRenderer for macos/linux + sdlRenderer for mingw). The expect makes
nativeMain metadata self-contained, which lets the WINDOWS publish job —
the only host that declares every target — produce the root modules. */
/* Create the render backend for the selected [GpuMode]; null when the
backend can't initialise (caller falls back / reports). */
expect fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend?
/* The renderer module's per-OS default GpuMode (used for GpuMode.Auto). */
expect fun rendererPreferredGpuMode(): GpuMode
@@ -0,0 +1,34 @@
package com.compose.sdl.graphics
// ==================
// MARK: DrawStats — per-frame renderer work counters (diagnostic)
// ==================
/**
* Cheap per-frame counters the SDL draw path bumps and the frame profiler
* reads, to answer "what inside `draw` costs the time?" — geometry submissions
* (SDL_RenderGeometry calls), total vertices tessellated, rounded-clip mask
* realizations (offscreen render-target passes — the expensive ones), text
* blits and image blits. Reset each frame by the profiler after reading.
*
* Single-threaded (all draw is on the main thread), so plain vars are fine.
* Zero overhead when the profiler isn't reading — just integer increments.
*/
object DrawStats {
var geometrySubmits: Int = 0
var vertices: Int = 0
var maskRealizations: Int = 0
var textDraws: Int = 0
var imageBlits: Int = 0
fun reset() {
geometrySubmits = 0
vertices = 0
maskRealizations = 0
textDraws = 0
imageBlits = 0
}
fun summary(): String =
"geo=$geometrySubmits verts=$vertices masks=$maskRealizations text=$textDraws img=$imageBlits"
}
@@ -0,0 +1,14 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.GraphicsContext
// ==================
// MARK: GraphicsContext factory seam (B2 / P1.3)
// ==================
/* ComposeOwner.graphicsContext is created behind a per-renderer factory seam so each
leg supplies its own GraphicsContext: exactly one actual is attached per target
(the createRenderBackend trick). SDL → the project ProjectGraphicsContext (record/
replay via the SDL NativeRenderNode); Skia → upstream's SkiaGraphicsContext (owns a
skiko RenderNodeContext). See RENDERER.md §4 (B2). */
internal expect fun createGraphicsContext(): GraphicsContext
@@ -1,13 +0,0 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.layer.GraphicsLayer
// ==================
// MARK: GraphicsLayer factory — native actuals
// ==================
internal actual fun createProjectGraphicsLayer(): GraphicsLayer = GraphicsLayer()
internal actual fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer) {
inLayer.isReleased = true
}
@@ -7,18 +7,22 @@ import androidx.compose.ui.graphics.ImageBitmap
// ================== // ==================
/* Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a /* Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
drawable [ImageBitmap]. The active renderer registers its implementation — drawable [ImageBitmap]. The active renderer registers its implementation at
the SDL backend goes through SDL3_image (see Sdl3EncodedImageDecoder); the backend construction — the SDL backend goes through SDL3_image
Skia renderer never registers because its resources pipeline decodes through (Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
Skia directly (vendored skikoRenderer actuals in :components-resources). SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
hook under BOTH renderers (its skikoRendererMain reuses the SDL actuals).
Consumed by :components-resources' SDL actuals (`ByteArray.toImageBitmap`, Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
`SvgElement.toSvgPainter`) so painterResource(Res.drawable.x) works without painterResource(Res.drawable.x) works without that module reaching into
that module reaching into renderer internals. */ renderer internals. */
interface EncodedImageDecoder { interface EncodedImageDecoder {
fun decode(inBytes: ByteArray): ImageBitmap? fun decode(inBytes: ByteArray): ImageBitmap?
} }
/* Volatile: written by the render backend on the main thread, read by the
resources pipeline on Dispatchers.Default workers. */
@kotlin.concurrent.Volatile
var encodedImageDecoder: EncodedImageDecoder? = null var encodedImageDecoder: EncodedImageDecoder? = null
internal set internal set
@@ -0,0 +1,67 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.Matrix
import kotlin.math.abs
// Layer transform matrix builder for GraphicsLayerOwnerLayer (hit-testing /
// coordinate mapping). Body vendored from compose-multiplatform-core
// Matrices.skiko.kt `prepareTransformationMatrix` — pure Compose `Matrix` math,
// no Skia. It lives HERE in nativeMain (shared by both renderer legs) under a
// distinct name so it is visible to GraphicsLayerOwnerLayer (also nativeMain);
// the upstream `prepareTransformationMatrix` stays skikoRenderer-only in the
// vendored Matrices.skiko.kt and would not be visible up here. Distinct name
// avoids clashing with that copy on the Skia leg. (RENDERER.md P1.5 will
// reverse this rename once B2 vendors Matrices.skiko onto the Skia leg.)
// VENDOR-BASE: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt @ v1.12.0-beta03+dev4483
// Builds the layer transform (translation / rotation / scale / camera about a
// pivot) into [matrix]. Matches the skiko copy so hit-testing agrees across legs.
internal fun prepareLayerTransformationMatrix(
matrix: Matrix,
pivotX: Float,
pivotY: Float,
translationX: Float,
translationY: Float,
rotationX: Float,
rotationY: Float,
rotationZ: Float,
scaleX: Float,
scaleY: Float,
cameraDistance: Float,
) {
matrix.reset()
matrix.translate(x = -pivotX, y = -pivotY)
matrix *= Matrix().apply {
rotateZ(rotationZ)
rotateY(rotationY)
rotateX(rotationX)
scale(scaleX, scaleY)
}
// Perspective transform should be applied only in case of rotations to avoid
// multiply application in hierarchies.
// See Android's frameworks/base/libs/hwui/RenderProperties.cpp for reference
if (!rotationX.isZero() || !rotationY.isZero()) {
matrix *= Matrix().apply {
// The camera location is passed in inches, set in pt
val depth = cameraDistance * 72f
this[2, 3] = -1f / depth
}
}
matrix *= Matrix().apply {
translate(x = pivotX + translationX, y = pivotY + translationY)
}
// Third column and row are irrelevant for 2D space.
// Zeroing required to get correct inverse transformation matrix.
matrix[2, 0] = 0f
matrix[2, 1] = 0f
matrix[2, 3] = 0f
matrix[0, 2] = 0f
matrix[1, 2] = 0f
matrix[3, 2] = 0f
}
private const val NON_ZERO_EPSILON = 0.001f
@Suppress("NOTHING_TO_INLINE")
private inline fun Float.isZero(): Boolean = abs(this) <= NON_ZERO_EPSILON
@@ -0,0 +1,55 @@
package com.compose.sdl.graphics
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
// ==================
// MARK: NativeReleaseQueue — main-thread-deferred native resource disposal
// ==================
/**
* A queue of native-resource release actions drained on the MAIN THREAD once
* per frame by the window loop (see ComposeWindow). It exists because the two
* ways a renderer resource dies are both off the main thread, while the calls
* that free them (SDL_DestroyTexture, and any renderer API) are main-thread-only:
*
* - a Kotlin/Native `Cleaner` runs on a GC worker thread, and
* - explicit `close()` can be reached from composition/effect code on other
* dispatchers.
*
* Both simply ENQUEUE here; the main loop runs the actions between frames. This
* turns the GC (issue #2: Cleaner-managed native memory starving because the
* quiet Kotlin heap rarely triggers a collection) from the disposal MECHANISM
* into a mere safeguard — ownership closes resources promptly, the Cleaner is
* the backstop for whatever leaks a `close()`.
*
* Actions must be idempotent-safe at the source (guard against a resource being
* both explicitly closed and later GC-cleaned — see SdlImageBitmap's holder).
*/
object NativeReleaseQueue {
private val fLock = SynchronizedObject()
private var fPending = ArrayList<() -> Unit>()
/** Enqueue a release action. Safe to call from any thread. */
fun enqueue(action: () -> Unit) {
synchronized(fLock) { fPending.add(action) }
}
/**
* Run and clear every queued action. MAIN THREAD ONLY — the actions call
* renderer APIs that aren't thread-safe. Returns the number drained.
*/
fun drain(): Int {
val vBatch = synchronized(fLock) {
if (fPending.isEmpty()) return 0
val vTaken = fPending
fPending = ArrayList()
vTaken
}
for (vAction in vBatch) {
runCatching { vAction() }
}
return vBatch.size
}
}
@@ -65,12 +65,36 @@ class ComposeRootHost(inDensity: Float = 1f) {
fOwner.animationFrameClock.sendFrame(inNanos) fOwner.animationFrameClock.sendFrame(inNanos)
} }
// True while a node-level animation (scroll fling, node Animatable) awaits the next
// animation frame — the node half of the window's quiescence signal (render-to-quiescence
// screenshot capture asks the window whether ANY work is still pending).
fun hasAnimationAwaiters(): Boolean = fOwner.animationFrameClock.hasAwaiters
fun measureAndLayout() { fun measureAndLayout() {
// Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so // Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so
// requestFocus() from composition OR from a pointer event this frame takes effect — // requestFocus() from composition OR from a pointer event this frame takes effect —
// e.g. focus-on-click, which schedules a focus invalidation during event dispatch. // e.g. focus-on-click, which schedules a focus invalidation during event dispatch.
fOwner.onEndApplyChanges() fOwner.onEndApplyChanges()
fOwner.measureAndLayout() fOwner.measureAndLayout()
// Drop snapshot observations whose scope is no longer valid (detached nodes,
// destroyed layers, disposed draw scopes). Upstream RootNodeOwner does this after
// the measure pass; without it, every disposed subtree's measure/layout/DRAW
// observation scopes linger in the OwnerSnapshotObserver forever — each pins its
// observed object graph (a leak the P2.2 soak caught on ripple/indication draws).
fOwner.snapshotObserver.clearInvalidObservations()
}
// Draw the composed tree into [canvas] — re-records dirty layers, then walks
// the root so clean layers replay their cached content. Backends call this
// instead of rootNode.draw so retained-layer bookkeeping runs each frame.
fun drawRoot(canvas: androidx.compose.ui.graphics.Canvas) {
fOwner.renderRoot(canvas)
}
// The window wires this to set needsFrame — a layer whose content changed
// (OwnedLayer.invalidate) schedules a frame even with nothing else pending.
fun setInvalidationCallback(callback: () -> Unit) {
fOwner.onInvalidate = callback
} }
// The owner's FocusOwner IS a FocusManager — the window provides it as LocalFocusManager. // The owner's FocusOwner IS a FocusManager — the window provides it as LocalFocusManager.
@@ -138,29 +162,8 @@ class ComposeRootHost(inDensity: Float = 1f) {
fun onDropComplete() = fOwner.dragAndDropManager.dropComplete() fun onDropComplete() = fOwner.dragAndDropManager.dropComplete()
} }
// ================== // NOTE: feedPointerToProcessor / feedScrollToProcessor are plain nativeMain
// MARK: pointer-event bridge (nativeMain builds the internal PointerInputEvent) // functions in PointerEventBridge.native.kt. This facade moved from commonMain to
// ================== // nativeMain (it now constructs the native retained-layer OwnedLayer via
// ComposeOwner), so the former expect/actual split collapsed to a single native
/* The internal `expect PointerInputEvent` exposes no commonMain constructor, so the // definition — see RENDERER.md §5.
build+dispatch is a nativeMain actual. Constructs a single-pointer event and feeds it
to the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release. */
internal expect fun feedPointerToProcessor(
inOwner: com.compose.sdl.node.impl.ComposeOwner,
inType: Int,
inButton: Int,
inUptime: Long,
inX: Float,
inY: Float,
)
/* Builds a Scroll-type PointerInputEvent (scrollDelta) and drives the processor, so the vendored
Modifier.scrollable's MouseWheelScrollingLogic handles the wheel. nativeMain-only (ctor is native). */
internal expect fun feedScrollToProcessor(
inOwner: com.compose.sdl.node.impl.ComposeOwner,
inX: Float,
inY: Float,
inDeltaX: Float,
inDeltaY: Float,
inUptime: Long,
)
@@ -25,7 +25,7 @@ private var fTertiaryDown = false
/* Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse /* Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse
pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release; pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release;
inButton: 0=primary 1=secondary 2=tertiary. */ inButton: 0=primary 1=secondary 2=tertiary. */
internal actual fun feedPointerToProcessor( internal fun feedPointerToProcessor(
inOwner: ComposeOwner, inOwner: ComposeOwner,
inType: Int, inType: Int,
inButton: Int, inButton: Int,
@@ -82,7 +82,7 @@ internal actual fun feedPointerToProcessor(
vendored Modifier.scrollable (MouseWheelScrollingLogic) consumes it. SDL wheel delta maps vendored Modifier.scrollable (MouseWheelScrollingLogic) consumes it. SDL wheel delta maps
directly to scrollDelta (wheel-down = SDL deltaY<0 → content scrolls down); magnitude is scaled directly to scrollDelta (wheel-down = SDL deltaY<0 → content scrolls down); magnitude is scaled
in Sdl3ScrollConfig (dp per notch). */ in Sdl3ScrollConfig (dp per notch). */
internal actual fun feedScrollToProcessor( internal fun feedScrollToProcessor(
inOwner: ComposeOwner, inOwner: ComposeOwner,
inX: Float, inX: Float,
inY: Float, inY: Float,
@@ -9,6 +9,8 @@ import androidx.compose.ui.node.LayoutNodeDrawScope
import androidx.compose.ui.node.MeasureAndLayoutDelegate import androidx.compose.ui.node.MeasureAndLayoutDelegate
import androidx.compose.ui.node.Owner import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnedLayer import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.OwnedLayerManager
import androidx.compose.ui.node.GraphicsLayerOwnerLayer
import androidx.compose.ui.node.OwnerScope import androidx.compose.ui.node.OwnerScope
import androidx.compose.ui.node.OwnerSnapshotObserver import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest import androidx.compose.ui.node.RootForTest
@@ -79,7 +81,7 @@ internal class ComposeOwner(
override val root: LayoutNode, override val root: LayoutNode,
override val density: Density = Density(1f, 1f), override val density: Density = Density(1f, 1f),
override val layoutDirection: LayoutDirection = LayoutDirection.Ltr, override val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
) : Owner { ) : Owner, OwnedLayerManager {
// The vendored measure/layout state machine, rooted at [root]. // The vendored measure/layout state machine, rooted at [root].
private val fDelegate = MeasureAndLayoutDelegate(root) private val fDelegate = MeasureAndLayoutDelegate(root)
@@ -151,6 +153,11 @@ internal class ComposeOwner(
override fun onDetach(node: LayoutNode) { override fun onDetach(node: LayoutNode) {
fDelegate.onNodeDetached(node) fDelegate.onNodeDetached(node)
// Drop the detached node's read-observation scopes, exactly as upstream
// RootNodeOwner.onDetach does. Without this, every disposed node's measure/
// layout/draw observation scopes accumulate in the snapshot observer forever
// — the baseline composition-machinery leak the P2.2 soak caught.
snapshotObserver.clear(node)
} }
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) { override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
@@ -176,7 +183,86 @@ internal class ComposeOwner(
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit, drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit, invalidateParentLayer: () -> Unit,
explicitLayer: GraphicsLayer?, explicitLayer: GraphicsLayer?,
): OwnedLayer = ProjectOwnedLayer(drawBlock) ): OwnedLayer = GraphicsLayerOwnerLayer(
graphicsLayer = explicitLayer ?: graphicsContext.createGraphicsLayer(),
context = if (explicitLayer != null) null else graphicsContext,
layerManager = this,
drawBlock = drawBlock,
invalidateParentLayer = invalidateParentLayer,
)
// ============
// OwnedLayerManager — retained-layer bookkeeping (mirrors upstream
// OwnedLayerManagerImpl). dirtyLayers are re-recorded once per frame in
// [renderRoot] before the tree is drawn; clean layers replay their cached
// display list. invalidate() asks the window to schedule a frame.
private val dirtyLayers = mutableListOf<OwnedLayer>()
private var postponedDirtyLayers: MutableList<OwnedLayer>? = null
private var isDrawingContent = false
// Set by the window (via ComposeRootHost) to flag needsFrame; a layer whose
// content changed (OwnedLayer.invalidate) schedules a frame even when nothing
// else (recompose / relayout) is pending.
internal var onInvalidate: (() -> Unit)? = null
override fun invalidate() {
onInvalidate?.invoke()
}
override fun notifyLayerIsDirty(layer: OwnedLayer, isDirty: Boolean) {
// Mirrors upstream OwnedLayerManagerImpl exactly. The CRITICAL detail: when a
// layer clears its dirty flag DURING drawing (updateDisplayList sets isDirty
// = false in the renderRoot loop), we must NOT remove it from dirtyLayers here
// — that would shrink the list mid-iteration (IndexOutOfBounds). renderRoot's
// dirtyLayers.clear() after the loop handles it instead.
if (!isDirty) {
if (!isDrawingContent) {
dirtyLayers.remove(layer)
postponedDirtyLayers?.remove(layer)
}
} else if (!isDrawingContent) {
dirtyLayers += layer
} else {
val postponed = postponedDirtyLayers
?: mutableListOf<OwnedLayer>().also { postponedDirtyLayers = it }
postponed += layer
}
}
override fun recycle(layer: OwnedLayer): Boolean {
dirtyLayers.remove(layer)
postponedDirtyLayers?.remove(layer)
return false
}
// Both Owner and OwnedLayerManager declare voteFrameRate with a default, so an
// explicit override is required. We render at a fixed vsync cadence — no voting.
override fun voteFrameRate(frameRate: Float) {}
// Draw the tree: re-record dirty layers' display lists, then walk the root so
// clean layers replay. Called once per frame by ComposeRootHost.drawRoot.
internal fun renderRoot(canvas: Canvas) {
isDrawingContent = true
// Re-record dirty layers BEFORE drawing (unlike Android, our draw forms the
// actual render-command sequence, so display lists must be current first).
// notifyLayerIsDirty(false) is a no-op while drawing, so the list can't shrink
// here; clear() below drops them all at once.
if (dirtyLayers.isNotEmpty()) {
for (i in 0 until dirtyLayers.size) {
dirtyLayers[i].updateDisplayList()
}
}
dirtyLayers.clear()
root.draw(canvas, null)
// Layers that invalidated themselves during draw were parked in postponed;
// roll them into next frame.
postponedDirtyLayers?.let {
dirtyLayers.addAll(it)
it.clear()
}
isDrawingContent = false
}
// ============ // ============
// No-op subsystems (focus / semantics / input / clipboard / text / …). // No-op subsystems (focus / semantics / input / clipboard / text / …).
@@ -216,17 +302,12 @@ internal class ComposeOwner(
containsControls: Boolean, containsControls: Boolean,
): Long = originalTimeoutMillis ): Long = originalTimeoutMillis
} }
// GraphicsLayer here is the project record/replay actual (see // Created behind a per-renderer factory seam (B2 / P1.3): SDL uses the project
// GraphicsLayer.native.kt) — SharedTransitionLayout overlays and // record/replay GraphicsContext; the Skia leg swaps in upstream's SkiaGraphicsContext
// rememberGraphicsLayer() create layers through this context. The // at P1.6. SharedTransitionLayout overlays and rememberGraphicsLayer() create layers
// expect class hides its constructor/release from commonMain, hence // through this context. See com/compose/sdl/graphics/GraphicsContextFactory.kt.
// the project factory hops. override val graphicsContext: GraphicsContext =
override val graphicsContext: GraphicsContext = object : GraphicsContext { com.compose.sdl.graphics.createGraphicsContext()
override fun createGraphicsLayer(): GraphicsLayer =
com.compose.sdl.graphics.createProjectGraphicsLayer()
override fun releaseGraphicsLayer(layer: GraphicsLayer) =
com.compose.sdl.graphics.releaseProjectGraphicsLayer(layer)
}
override val textToolbar: TextToolbar = object : TextToolbar { override val textToolbar: TextToolbar = object : TextToolbar {
override val status: androidx.compose.ui.platform.TextToolbarStatus override val status: androidx.compose.ui.platform.TextToolbarStatus
get() = androidx.compose.ui.platform.TextToolbarStatus.Hidden get() = androidx.compose.ui.platform.TextToolbarStatus.Hidden
@@ -0,0 +1,321 @@
package androidx.compose.ui.graphics.layer
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import com.compose.sdl.graphics.NativeRenderNode
// ==================
// MARK: GraphicsLayer — façade over NativeRenderNode
// ==================
//
// DERIVED (copy + edit) from compose-multiplatform-core
// ui-graphics/src/skikoMain/.../graphics/layer/SkiaGraphicsLayer.skiko.kt.
// VENDOR-BASE: compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt @ v1.12.0-beta03+dev4483
// The ONLY structural change vs upstream: the backing display-list node is our
// renderer-agnostic `NativeRenderNode` instead of `org.jetbrains.skiko.node.RenderNode`,
// so both renderers share this façade (Skia's node wraps the real skiko RenderNode;
// SDL's node is SdlRenderNode). Every visual property is mirrored onto the node and
// applied at replay, exactly like upstream. Trimmed vs upstream: outsets (blur
// expansion) and ChildLayerDependenciesTracker (prompt child release — our children
// release via NativeReleaseQueue/GC instead). See RENDERER.md §4 (B2).
@Suppress("PropertyName")
actual class GraphicsLayer internal constructor(
private val renderNode: NativeRenderNode,
) {
private var outlineDirty = true
private var roundRectOutlineTopLeft: Offset = Offset.Zero
private var roundRectOutlineSize: Size = Size.Unspecified
private var roundRectCornerRadius: Float = 0f
private var internalOutline: Outline? = null
private var outlinePath: Path? = null
actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
set(value) {
if (field != value) {
field = value
renderNode.compositingStrategy = value
}
}
actual var topLeft: IntOffset = IntOffset.Zero
set(value) {
if (field != value) {
field = value
renderNode.topLeft = value
}
}
actual var size: IntSize = IntSize.Zero
private set(value) {
if (field != value) {
field = value
renderNode.size = value
if (roundRectOutlineSize.isUnspecified) {
outlineDirty = true
configureOutlineAndClip()
}
}
}
actual var pivotOffset: Offset = Offset.Unspecified
set(value) {
if (field != value) {
field = value
renderNode.pivot = value
}
}
actual var alpha: Float = 1f
set(value) {
if (field != value) {
field = value
renderNode.alpha = value
}
}
actual var scaleX: Float = 1f
set(value) {
if (field != value) { field = value; renderNode.scaleX = value }
}
actual var scaleY: Float = 1f
set(value) {
if (field != value) { field = value; renderNode.scaleY = value }
}
actual var translationX: Float = 0f
set(value) {
if (field != value) { field = value; renderNode.translationX = value }
}
actual var translationY: Float = 0f
set(value) {
if (field != value) { field = value; renderNode.translationY = value }
}
actual var shadowElevation: Float = 0f
set(value) {
if (field != value) {
field = value
renderNode.shadowElevation = value
outlineDirty = true
configureOutlineAndClip()
}
}
actual var ambientShadowColor: Color = Color.Black
set(value) {
if (field != value) { field = value; renderNode.ambientShadowColor = value }
}
actual var spotShadowColor: Color = Color.Black
set(value) {
if (field != value) { field = value; renderNode.spotShadowColor = value }
}
actual var blendMode: BlendMode = BlendMode.SrcOver
set(value) {
if (field != value) { field = value; renderNode.blendMode = value }
}
actual var colorFilter: ColorFilter? = null
set(value) {
if (field != value) { field = value; renderNode.colorFilter = value }
}
actual val outline: Outline
get() {
val tmpOutline = internalOutline
val tmpPath = outlinePath
return if (tmpOutline != null) {
tmpOutline
} else if (tmpPath != null) {
Outline.Generic(tmpPath).also { internalOutline = it }
} else {
resolveOutlinePosition { outlineTopLeft, outlineSize ->
val left = outlineTopLeft.x
val top = outlineTopLeft.y
val right = left + outlineSize.width
val bottom = top + outlineSize.height
val cornerRadius = this.roundRectCornerRadius
if (cornerRadius > 0f) {
Outline.Rounded(RoundRect(left, top, right, bottom, CornerRadius(cornerRadius)))
} else {
Outline.Rectangle(Rect(left, top, right, bottom))
}
}.also { internalOutline = it }
}
}
private fun resetOutlineParams() {
internalOutline = null
outlinePath = null
roundRectOutlineSize = Size.Unspecified
roundRectOutlineTopLeft = Offset.Zero
roundRectCornerRadius = 0f
outlineDirty = true
}
actual fun setPathOutline(path: Path) {
resetOutlineParams()
this.outlinePath = path
configureOutlineAndClip()
}
actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) {
if (this.roundRectOutlineTopLeft != topLeft ||
this.roundRectOutlineSize != size ||
this.roundRectCornerRadius != cornerRadius ||
this.outlinePath != null
) {
resetOutlineParams()
this.roundRectOutlineTopLeft = topLeft
this.roundRectOutlineSize = size
this.roundRectCornerRadius = cornerRadius
configureOutlineAndClip()
}
}
actual fun setRectOutline(topLeft: Offset, size: Size) {
setRoundRectOutline(topLeft, size, 0f)
}
actual var rotationX: Float = 0f
set(value) {
if (field != value) { field = value; renderNode.rotationX = value }
}
actual var rotationY: Float = 0f
set(value) {
if (field != value) { field = value; renderNode.rotationY = value }
}
actual var rotationZ: Float = 0f
set(value) {
if (field != value) { field = value; renderNode.rotationZ = value }
}
actual var cameraDistance: Float = DefaultCameraDistance
set(value) {
if (field != value) { field = value; renderNode.cameraDistance = value }
}
actual var clip: Boolean = false
set(value) {
if (field != value) {
field = value
renderNode.clip = value
outlineDirty = true
configureOutlineAndClip()
}
}
actual var renderEffect: RenderEffect? = null
set(value) {
if (field != value) { field = value; renderNode.renderEffect = value }
}
actual var isReleased: Boolean = false
internal set
actual fun setOutsets(left: Int, top: Int, right: Int, bottom: Int) {
// Outsets (blur/shadow expansion) not modelled on this port — see header.
}
actual fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit,
) {
this.size = size
renderNode.record(density, layoutDirection, size, block)
}
internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) {
if (isReleased) return
configureOutlineAndClip()
renderNode.drawInto(canvas)
}
@OptIn(androidx.compose.ui.InternalComposeUiApi::class)
private fun configureOutlineAndClip() {
if (!outlineDirty) return
val outlineIsNeeded = clip || shadowElevation > 0f
if (!outlineIsNeeded) {
renderNode.clip = false
renderNode.setClipPath(null)
} else {
renderNode.clip = clip
when (val tmpOutline = outline) {
is Outline.Rectangle -> renderNode.setClipRect(
tmpOutline.rect.left, tmpOutline.rect.top,
tmpOutline.rect.right, tmpOutline.rect.bottom,
)
is Outline.Rounded -> renderNode.setClipRRect(
tmpOutline.roundRect.left, tmpOutline.roundRect.top,
tmpOutline.roundRect.right, tmpOutline.roundRect.bottom,
floatArrayOf(
tmpOutline.roundRect.topLeftCornerRadius.x,
tmpOutline.roundRect.topLeftCornerRadius.y,
tmpOutline.roundRect.topRightCornerRadius.x,
tmpOutline.roundRect.topRightCornerRadius.y,
tmpOutline.roundRect.bottomRightCornerRadius.x,
tmpOutline.roundRect.bottomRightCornerRadius.y,
tmpOutline.roundRect.bottomLeftCornerRadius.x,
tmpOutline.roundRect.bottomLeftCornerRadius.y,
),
)
is Outline.Generic -> renderNode.setClipPath(tmpOutline.path)
}
}
outlineDirty = false
}
private inline fun <T> resolveOutlinePosition(block: (Offset, Size) -> T): T {
val layerSize = this.size.toSize()
val rRectTopLeft = roundRectOutlineTopLeft
val rRectSize = roundRectOutlineSize
val outlineSize = if (rRectSize.isUnspecified) layerSize else rRectSize
return block(rRectTopLeft, outlineSize)
}
internal fun release() {
if (!isReleased) {
isReleased = true
renderNode.close()
}
}
// Mirrors upstream SkiaGraphicsLayer.toImageBitmap: render the layer into a fresh
// bitmap of its own size. The offscreen canvas is flushed via NativeFinishableCanvas
// (SDL batches geometry; the backing render-target texture must be committed before
// it's read) — Skia's finish() is a no-op.
actual suspend fun toImageBitmap(): ImageBitmap {
val bitmap = ImageBitmap(size.width, size.height)
val canvas = Canvas(bitmap)
draw(canvas, null)
(canvas as? com.compose.sdl.graphics.NativeFinishableCanvas)?.finish()
return bitmap
}
}
@@ -6,7 +6,7 @@ import kotlin.experimental.ExperimentalNativeApi
// MARK: SDL3 renderer per-OS default // MARK: SDL3 renderer per-OS default
// ================== // ==================
fun rendererPreferredGpuMode(): GpuMode { actual fun rendererPreferredGpuMode(): GpuMode {
@OptIn(ExperimentalNativeApi::class) @OptIn(ExperimentalNativeApi::class)
return when (Platform.osFamily) { return when (Platform.osFamily) {
OsFamily.MACOSX -> GpuMode.Sdl3.Metal OsFamily.MACOSX -> GpuMode.Sdl3.Metal
@@ -11,7 +11,7 @@ import com.compose.sdl.renderer.sdl.Sdl3RenderBackend
renderer modules expose createRenderBackend / rendererPreferredGpuMode renderer modules expose createRenderBackend / rendererPreferredGpuMode
with identical signatures in this package, and the build includes exactly with identical signatures in this package, and the build includes exactly
one of them per target. Rejects Skia.* since this module has no Skiko. */ one of them per target. Rejects Skia.* since this module has no Skiko. */
fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? { actual fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
if (vResolved is GpuMode.Skia) { if (vResolved is GpuMode.Skia) {
error("$vResolved isn't available in this build — Skiko isn't linked. " + error("$vResolved isn't available in this build — Skiko isn't linked. " +
@@ -0,0 +1,201 @@
package com.compose.sdl.graphics
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.DefaultCameraDistance
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
// ==================
// MARK: DeferredRenderNode — replay-the-block NativeRenderNode
// ==================
/*
The renderer-agnostic [NativeRenderNode] both renderers use today. It does NOT
cache rasterised output: [record] stores the draw block; [drawInto] REPLAYS it
against the target canvas under the node's transform / clip / shadow / alpha. This
is behaviour-identical to the pre-refactor paths it replaces — the old
GraphicsLayer.native lambda-replay AND ProjectOwnedLayer's transform/shadow/clip —
now unified in one place behind the RenderNode seam.
It is the correct-but-unoptimised node. The perf win (stop re-tessellating on
replay) comes from swapping the per-renderer createNativeRenderNode actual to a
caching node — skiko RenderNode (a real display list) on Skia, an offscreen /
cached-geometry display list on SDL — WITHOUT touching GraphicsLayer. See
RENDERER.md §4 (Phase 2 = that swap).
Transform is applied via [prepareLayerTransformationMatrix] + canvas.concat, the
SAME matrix GraphicsLayerOwnerLayer uses for hit-testing (mapOffset), so draw and
hit-test provably agree. Shadow + rounded/shape clip go through the project canvas
interfaces (NativeShadowCanvas / NativeShapeClipCanvas), exactly as the old
ProjectOwnedLayer did.
*/
internal class DeferredRenderNode : NativeRenderNode {
override var topLeft: IntOffset = IntOffset.Zero
override var size: IntSize = IntSize.Zero
override var pivot: Offset = Offset.Unspecified
override var alpha: Float = 1f
override var scaleX: Float = 1f
override var scaleY: Float = 1f
override var translationX: Float = 0f
override var translationY: Float = 0f
override var rotationX: Float = 0f
override var rotationY: Float = 0f
override var rotationZ: Float = 0f
override var cameraDistance: Float = DefaultCameraDistance
override var shadowElevation: Float = 0f
override var ambientShadowColor: Color = Color.Black
override var spotShadowColor: Color = Color.Black
override var blendMode: BlendMode = BlendMode.SrcOver
override var colorFilter: ColorFilter? = null
override var renderEffect: RenderEffect? = null
override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
override var clip: Boolean = false
// Outline pushed by GraphicsLayer.configureOutlineAndClip whenever clip OR a
// shadow is present — so it drives BOTH the content clip (when clip=true) and
// the drop-shadow silhouette (when shadowElevation>0, even with clip=false).
private var clipOutline: Outline? = null
override fun setClipRect(left: Float, top: Float, right: Float, bottom: Float) {
clipOutline = Outline.Rectangle(Rect(left, top, right, bottom))
}
override fun setClipRRect(left: Float, top: Float, right: Float, bottom: Float, radii: FloatArray) {
clipOutline = Outline.Rounded(
RoundRect(
left = left, top = top, right = right, bottom = bottom,
topLeftCornerRadius = CornerRadius(radii[0], radii[1]),
topRightCornerRadius = CornerRadius(radii[2], radii[3]),
bottomRightCornerRadius = CornerRadius(radii[4], radii[5]),
bottomLeftCornerRadius = CornerRadius(radii[6], radii[7]),
),
)
}
override fun setClipPath(path: Path?) {
clipOutline = if (path == null) null else Outline.Generic(path)
}
// ============
// Record — store the block (deferred; replayed in drawInto).
private var recordedBlock: (DrawScope.() -> Unit)? = null
private var recordedDensity: Density = Density(1f)
private var recordedLayoutDirection: LayoutDirection = LayoutDirection.Ltr
private val drawScope = CanvasDrawScope()
override fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit,
) {
recordedDensity = density
recordedLayoutDirection = layoutDirection
this.size = size
recordedBlock = block
}
override fun drawInto(canvas: Canvas) {
val block = recordedBlock ?: return
val w = size.width.toFloat()
val h = size.height.toFloat()
canvas.save()
canvas.translate(topLeft.x.toFloat(), topLeft.y.toFloat())
// Transform about the pivot (default centre) — via the SAME matrix used for
// hit-testing, so draw and hit-test agree.
val hasTransform = scaleX != 1f || scaleY != 1f || rotationZ != 0f ||
rotationX != 0f || rotationY != 0f || translationX != 0f || translationY != 0f
if (hasTransform && w > 0f && h > 0f) {
val pivotX = if (pivot.isUnspecified) w / 2f else pivot.x
val pivotY = if (pivot.isUnspecified) h / 2f else pivot.y
val matrix = Matrix()
prepareLayerTransformationMatrix(
matrix = matrix,
pivotX = pivotX, pivotY = pivotY,
translationX = translationX, translationY = translationY,
rotationX = rotationX, rotationY = rotationY, rotationZ = rotationZ,
scaleX = scaleX, scaleY = scaleY, cameraDistance = cameraDistance,
)
canvas.concat(matrix)
}
// Drop shadow — layer-local, BEFORE the clip (shadow lives outside bounds).
if (shadowElevation > 0f && w > 0f && h > 0f) {
val outline = clipOutline
if (outline != null) {
(canvas as? NativeShadowCanvas)
?.drawDropShadow(outline, shadowElevation, ambientShadowColor, spotShadowColor)
}
}
// Content clip.
if (clip && w > 0f && h > 0f) applyClip(canvas)
// Offscreen for alpha (behaviour-preserving: the old paths saveLayer'd on
// alpha<1). ModulateAlpha suppresses the offscreen (alpha folds per-op) — but
// our DrawScope has no per-op multiplier yet, so it still composites here.
val needsAlphaLayer = alpha < 1f && w > 0f && h > 0f
if (needsAlphaLayer) {
canvas.saveLayer(
Rect(0f, 0f, w, h),
Paint().apply { alpha = this@DeferredRenderNode.alpha },
)
}
drawScope.draw(recordedDensity, recordedLayoutDirection, canvas, Size(w, h), block)
if (needsAlphaLayer) canvas.restore()
canvas.restore()
}
private fun applyClip(canvas: Canvas) {
when (val outline = clipOutline) {
is Outline.Rectangle -> canvas.clipRect(
outline.rect.left, outline.rect.top, outline.rect.right, outline.rect.bottom,
)
is Outline.Rounded -> {
// Skia clips rounded natively via clipPath; the SDL canvas has no
// path-clip primitive, so it implements NativeShapeClipCanvas and clips
// the rounded outline via an offscreen mask (preferred over clipPath,
// which falls back to the bounding rect there and left rounded state
// layers looking square).
val shapeClip = canvas as? NativeShapeClipCanvas
if (shapeClip != null) {
shapeClip.clipRoundRect(outline.roundRect)
} else {
val path = Path().apply { addRoundRect(outline.roundRect) }
canvas.clipPath(path)
}
}
is Outline.Generic -> canvas.clipPath(outline.path)
null -> {}
}
}
override fun close() {
recordedBlock = null
}
}
@@ -0,0 +1,11 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.GraphicsContext
// ==================
// MARK: createGraphicsContext — SDL3 renderer actual
// ==================
/* The SDL leg uses the project record/replay GraphicsContext (backed by the SDL
NativeRenderNode via createProjectGraphicsLayer). This is the permanent SDL home. */
internal actual fun createGraphicsContext(): GraphicsContext = ProjectGraphicsContext()
@@ -0,0 +1,14 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.layer.GraphicsLayer
// ==================
// MARK: GraphicsLayer factory — native actuals
// ==================
internal fun createProjectGraphicsLayer(): GraphicsLayer =
GraphicsLayer(createNativeRenderNode(NativeRenderNodeContext()))
internal fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer) {
inLayer.release()
}
@@ -0,0 +1,105 @@
package com.compose.sdl.graphics
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
// ==================
// MARK: NativeRenderNode — the renderer-agnostic retained display-list node
// ==================
/**
* The one genuinely renderer-specific piece of the retained-layer engine. This is
* the [GraphicsLayer] façade's backing node — the analogue of skiko's
* `org.jetbrains.skiko.node.RenderNode`. [GraphicsLayer] (our copy-edit of upstream
* `SkiaGraphicsLayer.skiko.kt`) mirrors every visual property onto this node, calls
* [record] to capture the layer's drawing once, and [drawInto] to replay it every
* frame with the transform / clip / shadow / compositing applied — WITHOUT
* re-recording.
*
* Property contract mirrors skiko `RenderNode` (translation/scale/rotation/pivot/
* alpha/shadow/clip/…), so the Skia actual wraps skiko `RenderNode` almost verbatim
* and the SDL actual (`SdlRenderNode`) implements the same contract.
*
* [record]/[drawInto] are higher-level than skiko's `beginRecording()`/`drawInto()`
* so BOTH a display-list impl (Skia: record into a skiko RenderNode; SDL later: a
* cached-geometry / offscreen display list) AND the current deferred-replay impl
* (SDL now: store the block, replay it under the transform) satisfy them.
*/
internal interface NativeRenderNode {
// ============
// Geometry — where the recorded content sits and its transform origin.
var topLeft: IntOffset
var size: IntSize
var pivot: Offset
// ============
// Transform — applied at replay, no re-record (matches skiko RenderNode).
var alpha: Float
var scaleX: Float
var scaleY: Float
var translationX: Float
var translationY: Float
var rotationX: Float
var rotationY: Float
var rotationZ: Float
var cameraDistance: Float
// ============
// Elevation shadow.
var shadowElevation: Float
var ambientShadowColor: Color
var spotShadowColor: Color
// ============
// Compositing — decides an offscreen (requiresLayer) and how content composites.
var blendMode: BlendMode
var colorFilter: ColorFilter?
var renderEffect: RenderEffect?
var compositingStrategy: CompositingStrategy
// ============
// Clip — outline pushed onto the node; rect / rounded-rect / generic path.
var clip: Boolean
fun setClipRect(left: Float, top: Float, right: Float, bottom: Float)
fun setClipRRect(left: Float, top: Float, right: Float, bottom: Float, radii: FloatArray)
fun setClipPath(path: Path?)
// ============
// Record / replay.
/** Capture the layer content once; re-called only when the layer is dirty. */
fun record(density: Density, layoutDirection: LayoutDirection, size: IntSize, block: DrawScope.() -> Unit)
/** Replay the recorded content onto [canvas], applying this node's transform. */
fun drawInto(canvas: Canvas)
/** Release native resources (textures / picture / display list). */
fun close()
}
/**
* Per-window context shared by the [NativeRenderNode]s of one composition — the
* analogue of skiko's `RenderNodeContext`. Minimal today; the SDL caching impl will
* grow it to carry the renderer handle it needs to allocate offscreen targets.
*/
internal class NativeRenderNodeContext(
val measureDrawBounds: Boolean = false,
)
@@ -0,0 +1,37 @@
package com.compose.sdl.graphics
import com.compose.sdl.renderer.sdl.SdlDisplayListRenderNode
import com.compose.sdl.renderer.sdl.SdlRenderNode
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
// ==================
// MARK: createNativeRenderNode — SDL3 renderer actual
// ==================
/* CDN_LAYERCACHE selects the retained node. The DEFAULT is now the cached-geometry
node (Phase 4): it records each leaf's tessellated geometry + plain text runs ONCE
and replays them under the layer transform — crisp/bit-exact at any transform, no
render-target state, deterministic. A full 57-screen geo-vs-block-replay sweep is
clean (55 at 0.000%; GraphicsLayer/ModShortcuts <0.13% cosmetic rotated-edge AA)
and the former Carousel/Pickers diffs are gone, so it graduates from opt-in to
default. See RENDERER.md §4b/§13.
Escape hatches (env override):
off / defer / 0 → DeferredRenderNode (replay the block every frame; no caching —
the previous default, kept as a fallback)
1 / texture → SdlRenderNode (offscreen-texture cache — legacy; fast for
static leaves but timing-nondeterministic
on complex screens, so not the default)
* → SdlDisplayListRenderNode (geo — the default) */
@OptIn(ExperimentalForeignApi::class)
private val layerCacheMode: String by lazy {
platform.posix.getenv("CDN_LAYERCACHE")?.toKString() ?: ""
}
internal fun createNativeRenderNode(context: NativeRenderNodeContext): NativeRenderNode =
when (layerCacheMode) {
"off", "defer", "0" -> DeferredRenderNode()
"1", "texture" -> SdlRenderNode()
else -> SdlDisplayListRenderNode()
}
@@ -0,0 +1,18 @@
package com.compose.sdl.graphics
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.graphics.layer.GraphicsLayer
// ==================
// MARK: ProjectGraphicsContext — SDL leg (record/replay GraphicsLayer)
// ==================
/* The SDL leg's GraphicsContext: creates/releases the project record/replay
GraphicsLayer (backed by the SDL NativeRenderNode). GraphicsLayer's `expect class`
hides its constructor/release from common code, so layer creation/release hop
through the [createProjectGraphicsLayer] / [releaseProjectGraphicsLayer] factories
(co-located here). `shadowContext` keeps the GraphicsContext interface default. */
internal class ProjectGraphicsContext : GraphicsContext {
override fun createGraphicsLayer(): GraphicsLayer = createProjectGraphicsLayer()
override fun releaseGraphicsLayer(layer: GraphicsLayer) = releaseProjectGraphicsLayer(layer)
}
@@ -1,5 +1,6 @@
package com.compose.sdl.renderer.sdl package com.compose.sdl.renderer.sdl
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.RoundRect
@@ -19,6 +20,7 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.drawscope.DrawStyle import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import com.compose.sdl.graphics.a8
import com.compose.sdl.graphics.b8 import com.compose.sdl.graphics.b8
import com.compose.sdl.graphics.g8 import com.compose.sdl.graphics.g8
import com.compose.sdl.graphics.r8 import com.compose.sdl.graphics.r8
@@ -26,6 +28,7 @@ import com.compose.sdl.icons.IconFont
import kotlinx.cinterop.* import kotlinx.cinterop.*
import sdl3.* import sdl3.*
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.sqrt
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -69,17 +72,87 @@ internal class Sdl3Canvas(
// Alpha8 mask) so the tint applied on blit colours it correctly. // Alpha8 mask) so the tint applied on blit colours it correctly.
private val fOffscreenTexture: COpaquePointer? = null, private val fOffscreenTexture: COpaquePointer? = null,
private val fForceWhite: Boolean = false, private val fForceWhite: Boolean = false,
// Phase 4 capture mode: when set, this canvas RECORDS shape geometry into the list
// (via the scope's recordingSink) and touches NO GPU. Ops not yet capturable
// (text / image / clip / saveLayer / shadow) mark the list `unsupported` so the
// SdlDisplayListRenderNode falls back to a crisp block-replay for that leaf — so
// no un-captured op ever leaks to the screen. See SdlDisplayList.
private val fCaptureList: SdlDisplayList? = null,
) : Canvas, ) : Canvas,
com.compose.sdl.text.NativeTextCanvas, com.compose.sdl.text.NativeTextCanvas,
com.compose.sdl.graphics.NativePainterCanvas, com.compose.sdl.graphics.NativePainterCanvas,
com.compose.sdl.graphics.NativeShapeClipCanvas, com.compose.sdl.graphics.NativeShapeClipCanvas,
com.compose.sdl.graphics.NativeShadowCanvas { com.compose.sdl.graphics.NativeShadowCanvas,
com.compose.sdl.graphics.NativeFinishableCanvas {
// The tessellating scope. It emits geometry in the canvas's user space; the // The tessellating scope. It emits geometry in the canvas's user space; the
// canvas hands it the current affine each draw (setMatrix) so scale / rotate / // canvas hands it the current affine each draw (setMatrix) so scale / rotate /
// translate reach the vertices. // translate reach the vertices.
private val fScope = Sdl3DrawScope(fRenderer, 0f, 0f, fSize) private val fScope = Sdl3DrawScope(fRenderer, 0f, 0f, fSize)
init {
// Capture mode routes the scope's geometry to the display list instead of the GPU.
if (fCaptureList != null) fScope.recordingSink = fCaptureList
}
// Capture-mode gate for ops we can't record yet: flag the list unsupported (the
// node will replay the block instead) and skip the GPU work. Returns true if the
// caller should return early. No-op (false) outside capture mode.
private fun captureUnsupported(): Boolean {
val list = fCaptureList ?: return false
list.unsupported = true
return true
}
// Replay a captured display list into this (real) canvas under its CURRENT
// transform, in order (z-order preserved). Geometry batches re-emit their
// layer-local vertices through the affine; text runs flush pending geometry then
// re-look-up + blit at the transformed origin (glyph size stays logical). Used by
// SdlDisplayListRenderNode.drawInto.
internal fun replayDisplayList(list: SdlDisplayList) {
fScope.flush()
fScope.setMatrix(fMa, fMb, fMc, fMd, fMe, fMf)
for (i in list.commands.indices) {
when (val cmd = list.commands[i]) {
is GeometryBatch -> fScope.replayBatch(cmd.vertexData, cmd.vertexCount)
is TextRun -> {
fScope.flush() // submit geometry before this run (z-order)
// Transform the run ORIGIN by the affine; keep glyph size logical
// (matches the immediate path: layer scale reaches position, not size).
fTextRenderer?.blitRun(
inFontFamily = cmd.fontFamily,
inText = cmd.text,
inFontSize = cmd.fontSizePx,
inFontVariations = cmd.variations,
inStyle = cmd.style,
inDeviceX = mapX(cmd.x, cmd.y),
inDeviceY = mapY(cmd.x, cmd.y),
inLogW = cmd.w,
inLogH = cmd.h,
inColor = androidx.compose.ui.graphics.Color(cmd.colorArgb),
)
}
is IconRun -> {
fScope.flush() // submit geometry before this glyph (z-order)
// Map the box origin through the affine; box size stays logical (the
// glyph centres in it — layer scale reaches position, not glyph size).
fTextRenderer?.blitIconRun(
inFontFamily = cmd.fontFamily,
inText = cmd.text,
inFontSize = cmd.fontSizePx,
inFontVariations = cmd.variations,
inDeviceBoxX = mapX(cmd.boxX, cmd.boxY),
inDeviceBoxY = mapY(cmd.boxX, cmd.boxY),
inBoxW = cmd.boxW,
inBoxH = cmd.boxH,
inColor = androidx.compose.ui.graphics.Color(cmd.colorArgb),
)
}
}
}
fScope.flush()
}
// Current user→screen affine (a,b,c,d,e,f): screen = (a*x+c*y+e, b*x+d*y+f). // Current user→screen affine (a,b,c,d,e,f): screen = (a*x+c*y+e, b*x+d*y+f).
// Accumulates Canvas.translate/scale/rotate/concat; identity + a translate is // Accumulates Canvas.translate/scale/rotate/concat; identity + a translate is
// the common layout-positioning case. // the common layout-positioning case.
@@ -126,7 +199,7 @@ internal class Sdl3Canvas(
// offscreen clip opened inside this save frame (see fClipLayers below). // offscreen clip opened inside this save frame (see fClipLayers below).
private data class State( private data class State(
val a: Float, val b: Float, val c: Float, val d: Float, val e: Float, val f: Float, val a: Float, val b: Float, val c: Float, val d: Float, val e: Float, val f: Float,
val clip: IntArray?, val alpha: Float, val clipLayers: Int, val clip: IntArray?, val alpha: Float, val clipLayers: Int, val pendingClips: Int,
) )
private val fStack = ArrayDeque<State>() private val fStack = ArrayDeque<State>()
@@ -146,9 +219,139 @@ internal class Sdl3Canvas(
) )
private val fClipLayers = ArrayDeque<OffscreenClip>() private val fClipLayers = ArrayDeque<OffscreenClip>()
// ============
// Lazy rounded clips. Opening an offscreen mask per clip is the SINGLE
// most expensive thing this canvas does (two render-target switches +
// clear + corner cut + feather + composite) - and the dominant Compose
// idiom `Modifier.clip(CircleShape).background(...)` never needs one:
// the background is a full-cover rect drawable AS the rounded shape, and
// content wholly inside the rounded region needs no mask at all. So
// clipRoundRect only RECORDS the clip; each draw either proves itself
// safe (containment / cover) or realizes the offscreen on demand.
private class PendingRoundClip(
val deviceRound: RoundRect, // device-space outline (clamped radii)
val localRound: RoundRect, // as passed in (for cover -> shape-fill emission)
val region: IntArray, // fClip intersect bbox at push time
val bbox: IntArray, // device bbox of the outline
val prevClip: IntArray?, // fClip before this clip was pushed
// Affine snapshot at push - cover -> fill re-emits in LOCAL space and
// must bail if the transform moved since.
val ma: Float, val mb: Float, val mc: Float, val md: Float, val me: Float, val mf: Float,
// fStack.size when the clip was pushed: a lazily-realized mask must be
// composited by the save-frame that OWNS the clip, not by whatever
// inner frame happened to trigger realization (a press draws the state
// layer and the ripple in separate inner clipRect{} frames - the first
// realize must not let the inner restore close the mask, or the second
// draw runs unmasked: the "square on click" artifact).
val saveDepth: Int,
)
private val fPendingClips = ArrayDeque<PendingRoundClip>()
/* Point-in-round-rect (device space) - inside the rect and, within a
corner cut square, inside its ellipse. */
private fun roundRectContains(inRR: RoundRect, inX: Float, inY: Float): Boolean {
if (inX < inRR.left || inX > inRR.right || inY < inRR.top || inY > inRR.bottom) return false
fun corner(inRx: Float, inRy: Float, inDx: Float, inDy: Float): Boolean {
if (inRx <= 0f || inRy <= 0f) return true
val vNx = inDx / inRx; val vNy = inDy / inRy
return vNx * vNx + vNy * vNy <= 1f
}
val vTl = inRR.topLeftCornerRadius; val vTr = inRR.topRightCornerRadius
val vBr = inRR.bottomRightCornerRadius; val vBl = inRR.bottomLeftCornerRadius
if (inX < inRR.left + vTl.x && inY < inRR.top + vTl.y)
return corner(vTl.x, vTl.y, inX - (inRR.left + vTl.x), inY - (inRR.top + vTl.y))
if (inX > inRR.right - vTr.x && inY < inRR.top + vTr.y)
return corner(vTr.x, vTr.y, inX - (inRR.right - vTr.x), inY - (inRR.top + vTr.y))
if (inX > inRR.right - vBr.x && inY > inRR.bottom - vBr.y)
return corner(vBr.x, vBr.y, inX - (inRR.right - vBr.x), inY - (inRR.bottom - vBr.y))
if (inX < inRR.left + vBl.x && inY > inRR.bottom - vBl.y)
return corner(vBl.x, vBl.y, inX - (inRR.left + vBl.x), inY - (inRR.bottom - vBl.y))
return true
}
/* A device-space aabb is inside the round rect iff its four corners are. */
private fun roundRectContainsAabb(inRR: RoundRect, inL: Float, inT: Float, inR: Float, inB: Float): Boolean =
roundRectContains(inRR, inL, inT) && roundRectContains(inRR, inR, inT) &&
roundRectContains(inRR, inR, inB) && roundRectContains(inRR, inL, inB)
/* Device aabb of a LOCAL rect under the current affine. */
private fun deviceAabb(inL: Float, inT: Float, inR: Float, inB: Float): FloatArray {
val vX0 = mapX(inL, inT); val vX1 = mapX(inR, inT); val vX2 = mapX(inR, inB); val vX3 = mapX(inL, inB)
val vY0 = mapY(inL, inT); val vY1 = mapY(inR, inT); val vY2 = mapY(inR, inB); val vY3 = mapY(inL, inB)
return floatArrayOf(
minOf(vX0, vX1, vX2, vX3), minOf(vY0, vY1, vY2, vY3),
maxOf(vX0, vX1, vX2, vX3), maxOf(vY0, vY1, vY2, vY3),
)
}
/* If the LOCAL-space aabb stays inside every pending rounded clip, drawing
it under the already-active rect clip is correct - no mask needed.
Otherwise realize the pending masks before the draw proceeds. */
private fun admitDraw(inL: Float, inT: Float, inR: Float, inB: Float) {
if (fPendingClips.isEmpty()) return
val vBox = deviceAabb(inL, inT, inR, inB)
for (vPending in fPendingClips) {
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
realizePendingClips()
return
}
}
}
/* Convert pending clips into real offscreen mask layers (outermost first).
Each clip realizes with its OWN push-time region (pending.region =
enclosing-clip-at-push ∩ bbox), NOT the current fClip: a lazily-realized
clip may fire arbitrarily deep inside descendants whose own clips have
since narrowed fClip. Re-intersecting with that narrow clip shrank an
outer clip's offscreen to the innermost descendant's rect, so its
composite blitted back only that sliver and every sibling drawn into the
same offscreen was discarded (the TLS-chain regression: only the first
card + nothing below it, until a scroll shifted which card triggered
realization). The real (already-narrowed) drawing clip is preserved
across the loop and restored at the end, so subsequent draws into the
innermost target stay correctly clipped. */
private fun realizePendingClips() {
if (fPendingClips.isEmpty()) return
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
val vDrawClip = fClip
while (fPendingClips.isNotEmpty()) {
val vPending = fPendingClips.removeFirst()
fScope.flush()
val vRegion = vPending.region
val vTarget = fClipTargets?.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt())
if (vTarget == null || vRegion[2] <= vRegion[0] || vRegion[3] <= vRegion[1]) {
// Degrade to the rect clip that is already active.
continue
}
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
fClip = vRegion
applyClip()
clearRegion(vRegion)
com.compose.sdl.graphics.DrawStats.maskRealizations++
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPending.prevClip, vRegion, vPending.bbox, vPending.deviceRound))
// Attribute the realized mask to the save-frame that pushed the
// clip: inner frames entered since must NOT composite it on their
// restore - bump their thresholds past the new layer.
for (vI in vPending.saveDepth until fStack.size) {
val vState = fStack[vI]
if (vState.clipLayers < fClipLayers.size) {
fStack[vI] = vState.copy(clipLayers = fClipLayers.size)
}
}
}
// Restore the real drawing clip (already narrowed by inner clipRects /
// scroll bounds) for draws into the now-active innermost target.
fClip = vDrawClip
applyClip()
}
// Flushes any pending batched geometry to SDL, then frees the scope's // Flushes any pending batched geometry to SDL, then frees the scope's
// native buffer + clears the SDL clip. Call once per frame after the draw. // native buffer + clears the SDL clip. Call once per frame after the draw.
fun finish() { override fun finish() {
fPendingClips.clear()
fScope.release() fScope.release()
// Safety: an unbalanced save/restore must never leave the renderer pointed // Safety: an unbalanced save/restore must never leave the renderer pointed
// at a scratch target when the frame is presented. // at a scratch target when the frame is presented.
@@ -166,11 +369,14 @@ internal class Sdl3Canvas(
// Offscreen canvas: the outermost save (CanvasDrawScope brackets its draw with // Offscreen canvas: the outermost save (CanvasDrawScope brackets its draw with
// save/restore) switches the SDL render target to the ImageBitmap's texture. // save/restore) switches the SDL render target to the ImageBitmap's texture.
if (fOffscreenTexture != null && fStack.isEmpty()) beginOffscreen() if (fOffscreenTexture != null && fStack.isEmpty()) beginOffscreen()
fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size)) fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size, fPendingClips.size))
} }
override fun restore() { override fun restore() {
val vPrev = fStack.removeLastOrNull() ?: return val vPrev = fStack.removeLastOrNull() ?: return
// Pending (never-realized) rounded clips opened inside this save frame:
// nothing was drawn through an offscreen for them - just drop them.
while (fPendingClips.size > vPrev.pendingClips) fPendingClips.removeLast()
// Composite + pop any offscreen rounded clips opened inside this save frame // Composite + pop any offscreen rounded clips opened inside this save frame
// before restoring the plain state (each composite restores render target // before restoring the plain state (each composite restores render target
// and SDL clip to what it was when the clip was opened). // and SDL clip to what it was when the clip was opened).
@@ -208,6 +414,14 @@ internal class Sdl3Canvas(
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>() val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
fSavedOffscreenTarget = SDL_GetRenderTarget(vRenderer) fSavedOffscreenTarget = SDL_GetRenderTarget(vRenderer)
SDL_SetRenderTarget(vRenderer, vTex.reinterpret()) SDL_SetRenderTarget(vRenderer, vTex.reinterpret())
// Clear to transparent. Render-target textures aren't auto-cleared, and a
// retained-layer texture (SdlRenderNode) is REUSED across re-records — without
// this, a re-record ghosts the previous content, which showed up as multi-frame
// instability on churny screens (Pickers). Fresh textures happen to be zeroed
// so static leaves were fine, but reuse needs an explicit clear. RenderClear
// fills with the draw colour ignoring blend, so (0,0,0,0) = transparent.
SDL_SetRenderDrawColor(vRenderer, 0u, 0u, 0u, 0u)
SDL_RenderClear(vRenderer)
fClip = null fClip = null
SDL_SetRenderClipRect(vRenderer, null) SDL_SetRenderClipRect(vRenderer, null)
} }
@@ -220,6 +434,7 @@ internal class Sdl3Canvas(
} }
override fun saveLayer(bounds: Rect, paint: Paint) { override fun saveLayer(bounds: Rect, paint: Paint) {
if (captureUnsupported()) return
// No real offscreen buffer — SDL3 lacks a portable render-target-in-a-batched-scope // No real offscreen buffer — SDL3 lacks a portable render-target-in-a-batched-scope
// primitive here. Instead, multiplicatively propagate the layer's alpha through // primitive here. Instead, multiplicatively propagate the layer's alpha through
// every enclosed primitive: each draw() reads `fAlpha` and multiplies against // every enclosed primitive: each draw() reads `fAlpha` and multiplies against
@@ -251,6 +466,7 @@ internal class Sdl3Canvas(
} }
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) { override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
if (captureUnsupported()) return
if (clipOp == ClipOp.Difference) { if (clipOp == ClipOp.Difference) {
clipRectDifference(left, top, right, bottom) clipRectDifference(left, top, right, bottom)
return return
@@ -267,6 +483,7 @@ internal class Sdl3Canvas(
// treating Difference as intersect clipped the border TO the notch instead // treating Difference as intersect clipped the border TO the notch instead
// (a floating line under the label, no outline anywhere else). // (a floating line under the label, no outline anywhere else).
private fun clipRectDifference(inL: Float, inT: Float, inR: Float, inB: Float) { private fun clipRectDifference(inL: Float, inT: Float, inR: Float, inB: Float) {
realizePendingClips()
val vDiff = mapRectAABB(inL, inT, inR, inB) val vDiff = mapRectAABB(inL, inT, inR, inB)
val vRegion = fClip ?: intArrayOf(0, 0, fSize.width.toInt(), fSize.height.toInt()) val vRegion = fClip ?: intArrayOf(0, 0, fSize.width.toInt(), fSize.height.toInt())
if (fClipTargets == null || fSize.width < 1f || fSize.height < 1f || if (fClipTargets == null || fSize.width < 1f || fSize.height < 1f ||
@@ -290,6 +507,7 @@ internal class Sdl3Canvas(
} }
override fun clipPath(path: ComposePath, clipOp: ClipOp) { override fun clipPath(path: ComposePath, clipOp: ClipOp) {
if (captureUnsupported()) return
// SDL has only rectangular render clipping; approximate a path clip by // SDL has only rectangular render clipping; approximate a path clip by
// its bounding box (TODO: stencil/mask for non-rect clips). // its bounding box (TODO: stencil/mask for non-rect clips).
val vB = pathBounds(path) ?: return val vB = pathBounds(path) ?: return
@@ -330,11 +548,20 @@ internal class Sdl3Canvas(
// arbitrary paths keep the clipPath bbox fallback. // arbitrary paths keep the clipPath bbox fallback.
override fun clipRoundRect(inRoundRect: RoundRect) { override fun clipRoundRect(inRoundRect: RoundRect) {
if (captureUnsupported()) return
// The mask is cut in DEVICE space (the bbox below goes through the
// affine), but the corner radii arrive in LOCAL user space — scale
// them by the affine too, or a graphicsLayer-scaled circle clip cuts
// with oversized corners and degenerates toward a lozenge (seen as
// diamond-shaped "popped" bubbles under scale 0.55). Column norms
// approximate per-axis scale; rotation already degrades to AABBs here.
val vScaleX = sqrt(fMa * fMa + fMb * fMb)
val vScaleY = sqrt(fMc * fMc + fMd * fMd)
val vMaxRadius = maxOf( val vMaxRadius = maxOf(
inRoundRect.topLeftCornerRadius.x, inRoundRect.topLeftCornerRadius.y, inRoundRect.topLeftCornerRadius.x * vScaleX, inRoundRect.topLeftCornerRadius.y * vScaleY,
inRoundRect.topRightCornerRadius.x, inRoundRect.topRightCornerRadius.y, inRoundRect.topRightCornerRadius.x * vScaleX, inRoundRect.topRightCornerRadius.y * vScaleY,
inRoundRect.bottomRightCornerRadius.x, inRoundRect.bottomRightCornerRadius.y, inRoundRect.bottomRightCornerRadius.x * vScaleX, inRoundRect.bottomRightCornerRadius.y * vScaleY,
inRoundRect.bottomLeftCornerRadius.x, inRoundRect.bottomLeftCornerRadius.y, inRoundRect.bottomLeftCornerRadius.x * vScaleX, inRoundRect.bottomLeftCornerRadius.y * vScaleY,
) )
// Effectively-square corners or no offscreen pool → plain rectangular clip. // Effectively-square corners or no offscreen pool → plain rectangular clip.
if (vMaxRadius < 0.5f || fClipTargets == null || fSize.width < 1f || fSize.height < 1f) { if (vMaxRadius < 0.5f || fClipTargets == null || fSize.width < 1f || fSize.height < 1f) {
@@ -343,6 +570,20 @@ internal class Sdl3Canvas(
} }
fScope.flush() fScope.flush()
val vBbox = mapRectAABB(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom) val vBbox = mapRectAABB(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
// Clamp to the DEVICE half-box: the bbox is int-truncated, so a scaled
// radius can exceed it by a fraction — opposite corner cuts then overlap
// and leave flat spots at N/E/S/W (a circle read as an octagon).
val vHalfW = (vBbox[2] - vBbox[0]) / 2f
val vHalfH = (vBbox[3] - vBbox[1]) / 2f
fun deviceRadius(inR: CornerRadius) =
CornerRadius(minOf(inR.x * vScaleX, vHalfW), minOf(inR.y * vScaleY, vHalfH))
val vDeviceRound = RoundRect(
vBbox[0].toFloat(), vBbox[1].toFloat(), vBbox[2].toFloat(), vBbox[3].toFloat(),
topLeftCornerRadius = deviceRadius(inRoundRect.topLeftCornerRadius),
topRightCornerRadius = deviceRadius(inRoundRect.topRightCornerRadius),
bottomRightCornerRadius = deviceRadius(inRoundRect.bottomRightCornerRadius),
bottomLeftCornerRadius = deviceRadius(inRoundRect.bottomLeftCornerRadius),
)
val vRegion = intersect(fClip, vBbox) val vRegion = intersect(fClip, vBbox)
// Nothing visible: keep behaviour of a normal clip (cull) without an // Nothing visible: keep behaviour of a normal clip (cull) without an
// offscreen pass. The enclosing save/restore restores the clip afterwards. // offscreen pass. The enclosing save/restore restores the clip afterwards.
@@ -351,20 +592,18 @@ internal class Sdl3Canvas(
applyClip() applyClip()
return return
} }
val vTarget = fClipTargets.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt()) // LAZY: record only — the rect part of the clip applies immediately,
if (vTarget == null) { // the offscreen mask is opened by the first draw that actually needs
// Couldn't allocate a scratch target — degrade to a rectangular clip. // it (admitDraw / realizePendingClips). The common clip+background
clipRect(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom) // idiom never does.
return
}
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
val vPrevClip = fClip val vPrevClip = fClip
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
fClip = vRegion fClip = vRegion
applyClip() applyClip()
clearRegion(vRegion) fPendingClips.addLast(PendingRoundClip(
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPrevClip, vRegion, vBbox, inRoundRect)) vDeviceRound, inRoundRect, vRegion, vBbox, vPrevClip,
fMa, fMb, fMc, fMd, fMe, fMf,
fStack.size,
))
} }
// Pops the top offscreen clip: flush its subtree, cut the rounded corners out // Pops the top offscreen clip: flush its subtree, cut the rounded corners out
@@ -427,22 +666,24 @@ internal class Sdl3Canvas(
private fun zeroRoundRectCorners(inBbox: IntArray, inRoundRect: RoundRect) { private fun zeroRoundRectCorners(inBbox: IntArray, inRoundRect: RoundRect) {
val vLeft = inBbox[0].toFloat(); val vTop = inBbox[1].toFloat() val vLeft = inBbox[0].toFloat(); val vTop = inBbox[1].toFloat()
val vRight = inBbox[2].toFloat(); val vBottom = inBbox[3].toFloat() val vRight = inBbox[2].toFloat(); val vBottom = inBbox[3].toFloat()
val vSeg = 12
val vTl = inRoundRect.topLeftCornerRadius val vTl = inRoundRect.topLeftCornerRadius
val vTr = inRoundRect.topRightCornerRadius val vTr = inRoundRect.topRightCornerRadius
val vBr = inRoundRect.bottomRightCornerRadius val vBr = inRoundRect.bottomRightCornerRadius
val vBl = inRoundRect.bottomLeftCornerRadius val vBl = inRoundRect.bottomLeftCornerRadius
// Upper bound: 4 corners × vSeg triangles × 3 vertices. Written straight // Segments per quarter arc ~ every 4 device px along the arc, matching
// into the native SDL_Vertex buffer — the old ArrayList<Float> staging // Sdl3DrawScope's density (~64/full circle at typical radii).
// boxed ~300 floats per clip layer per frame. val vMaxR = maxOf(vTl.x, vTl.y, vTr.x, vTr.y, vBr.x, vBr.y, vBl.x, vBl.y)
fun cornerCount(inR: androidx.compose.ui.geometry.CornerRadius): Int = val vSeg = ((vMaxR * 1.5708f) / 4f).toInt().coerceIn(8, 32)
if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3 fun cornerCount(inR: CornerRadius): Int = if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3
val vMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl) val vFanMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl)
if (vMax == 0) return if (vFanMax == 0) return
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>() val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
memScoped { memScoped {
val vBuf = allocArray<SDL_Vertex>(vMax) // ============
// Pass 1 — hard cut: zero everything outside each corner arc
// (triangle fan from the corner apex, blend NONE writes RGBA 0).
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
val vBuf = allocArray<SDL_Vertex>(vFanMax)
var vIdx = 0 var vIdx = 0
fun put(inX: Float, inY: Float) { fun put(inX: Float, inY: Float) {
vBuf[vIdx].position.x = inX; vBuf[vIdx].position.y = inY vBuf[vIdx].position.x = inX; vBuf[vIdx].position.y = inY
@@ -470,6 +711,51 @@ internal class Sdl3Canvas(
cutout(vRight, vBottom, vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f) cutout(vRight, vBottom, vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
cutout(vLeft, vBottom, vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f) cutout(vLeft, vBottom, vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
if (vIdx > 0) SDL_RenderGeometry(vRenderer, null, vBuf, vIdx, null, 0) if (vIdx > 0) SDL_RenderGeometry(vRenderer, null, vBuf, vIdx, null, 0)
// ============
// Pass 2 — feathered edge: a ~1.2px ring band straddling each arc
// whose vertex alpha ramps 1 → 0 outward, drawn with a custom
// "dst *= srcAlpha" blend. SDL_RenderGeometry has no antialiasing;
// without this the mask boundary is a hard polygon edge while the
// drawn shapes get Sdl3DrawScope's fringe AA (visibly rougher —
// scaled-down circle clips read as low-poly).
val vMul = SDL_ComposeCustomBlendMode(
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
)
SDL_SetRenderDrawBlendMode(vRenderer, vMul)
val vFeather = 1.2f
val vBandMax = 4 * vSeg * 6
val vBand = allocArray<SDL_Vertex>(vBandMax)
var vBIdx = 0
fun putBand(inX: Float, inY: Float, inKeep: Float) {
vBand[vBIdx].position.x = inX; vBand[vBIdx].position.y = inY
vBand[vBIdx].color.r = 1f; vBand[vBIdx].color.g = 1f; vBand[vBIdx].color.b = 1f; vBand[vBIdx].color.a = inKeep
vBand[vBIdx].tex_coord.x = 0f; vBand[vBIdx].tex_coord.y = 0f
vBIdx++
}
fun feather(inCx: Float, inCy: Float, inRx: Float, inRy: Float, inStartDeg: Float) {
if (inRx <= 1f || inRy <= 1f) return
var vPrevInX = 0f; var vPrevInY = 0f; var vPrevOutX = 0f; var vPrevOutY = 0f
for (vI in 0..vSeg) {
val vAngle = ((inStartDeg + 90f * vI / vSeg) * (PI / 180.0)).toFloat()
val vCos = cos(vAngle); val vSin = sin(vAngle)
val vInX = inCx + (inRx - vFeather) * vCos
val vInY = inCy + (inRy - vFeather) * vSin
val vOutX = inCx + inRx * vCos
val vOutY = inCy + inRy * vSin
if (vI > 0) {
putBand(vPrevInX, vPrevInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vInX, vInY, 1f)
putBand(vInX, vInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vOutX, vOutY, 0f)
}
vPrevInX = vInX; vPrevInY = vInY; vPrevOutX = vOutX; vPrevOutY = vOutY
}
}
feather(vLeft + vTl.x, vTop + vTl.y, vTl.x, vTl.y, 180f)
feather(vRight - vTr.x, vTop + vTr.y, vTr.x, vTr.y, 270f)
feather(vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
feather(vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
if (vBIdx > 0) SDL_RenderGeometry(vRenderer, null, vBand, vBIdx, null, 0)
} }
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_BLEND) SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_BLEND)
} }
@@ -502,6 +788,8 @@ internal class Sdl3Canvas(
inAmbientColor: androidx.compose.ui.graphics.Color, inAmbientColor: androidx.compose.ui.graphics.Color,
inSpotColor: androidx.compose.ui.graphics.Color, inSpotColor: androidx.compose.ui.graphics.Color,
) { ) {
if (captureUnsupported()) return
realizePendingClips()
if (inElevationPx <= 0f) return if (inElevationPx <= 0f) return
// Generic paths (CutCornerShape, GenericShape) don't 9-slice — blur the // Generic paths (CutCornerShape, GenericShape) don't 9-slice — blur the
@@ -636,6 +924,7 @@ internal class Sdl3Canvas(
// before re-rasterising a vector). Write transparent directly instead of the // before re-rasterising a vector). Write transparent directly instead of the
// paint colour. // paint colour.
if (paint.blendMode == BlendMode.Clear) { if (paint.blendMode == BlendMode.Clear) {
realizePendingClips()
fScope.flush() fScope.flush()
clearRegion(intArrayOf( clearRegion(intArrayOf(
mapX(left, top).toInt(), mapY(left, top).toInt(), mapX(left, top).toInt(), mapY(left, top).toInt(),
@@ -643,21 +932,75 @@ internal class Sdl3Canvas(
)) ))
return return
} }
if (fPendingClips.isNotEmpty() && tryDrawRectUnderPendingClips(left, top, right, bottom, paint)) return
prep().rectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint)) prep().rectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
} }
/* The `clip(shape).background(color)` idiom: a FILL rect that fully covers
the innermost pending clip becomes a direct rounded-shape fill (with the
scope's fringe AA — no offscreen mask at all). A rect fully INSIDE every
pending clip draws as-is. Returns true when the draw has been emitted. */
private fun tryDrawRectUnderPendingClips(inL: Float, inT: Float, inR: Float, inB: Float, inPaint: Paint): Boolean {
val vBox = deviceAabb(inL, inT, inR, inB)
val vInner = fPendingClips.last()
// Cover case: fill paint, no stroke, rect covers the whole clip outline,
// affine unchanged since the clip was pushed (local-space re-emission),
// uniform corner radii (the scope's roundRectCore takes one radius), and
// the clip outline itself contained in every OUTER pending clip.
val vLr = vInner.localRound
val vUniform = vLr.topLeftCornerRadius.x == vLr.topLeftCornerRadius.y &&
vLr.topLeftCornerRadius == vLr.topRightCornerRadius &&
vLr.topLeftCornerRadius == vLr.bottomRightCornerRadius &&
vLr.topLeftCornerRadius == vLr.bottomLeftCornerRadius
val vAffineUnchanged = vInner.ma == fMa && vInner.mb == fMb && vInner.mc == fMc &&
vInner.md == fMd && vInner.me == fMe && vInner.mf == fMf
if (inPaint.style == PaintingStyle.Fill && inPaint.blendMode == BlendMode.SrcOver &&
vUniform && vAffineUnchanged &&
vBox[0] <= vInner.bbox[0] && vBox[1] <= vInner.bbox[1] &&
vBox[2] >= vInner.bbox[2] && vBox[3] >= vInner.bbox[3]
) {
var vOutersContain = true
for (vPending in fPendingClips) {
if (vPending === vInner) continue
if (!roundRectContainsAabb(vPending.deviceRound, vInner.deviceRound.left, vInner.deviceRound.top, vInner.deviceRound.right, vInner.deviceRound.bottom)) {
vOutersContain = false; break
}
}
if (vOutersContain) {
prep().roundRectCore(
brushFor(inPaint),
Offset(vLr.left, vLr.top), Size(vLr.width, vLr.height),
vLr.topLeftCornerRadius.x,
(inPaint.alpha * fAlpha), Fill,
)
return true
}
}
// Containment: inside every pending outline → the active rect clip is enough.
for (vPending in fPendingClips) {
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
realizePendingClips()
return false
}
}
return false
}
override fun drawRoundRect( override fun drawRoundRect(
left: Float, top: Float, right: Float, bottom: Float, left: Float, top: Float, right: Float, bottom: Float,
radiusX: Float, radiusY: Float, paint: Paint, radiusX: Float, radiusY: Float, paint: Paint,
) { ) {
admitDraw(left, top, right, bottom)
prep().roundRectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), radiusX, (paint.alpha * fAlpha), styleFor(paint)) prep().roundRectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), radiusX, (paint.alpha * fAlpha), styleFor(paint))
} }
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) { override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
admitDraw(left, top, right, bottom)
prep().ovalCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint)) prep().ovalCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
} }
override fun drawCircle(center: Offset, radius: Float, paint: Paint) { override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
admitDraw(center.x - radius, center.y - radius, center.x + radius, center.y + radius)
prep().circleCore(brushFor(paint), radius, center, (paint.alpha * fAlpha), styleFor(paint)) prep().circleCore(brushFor(paint), radius, center, (paint.alpha * fAlpha), styleFor(paint))
} }
@@ -665,14 +1008,17 @@ internal class Sdl3Canvas(
left: Float, top: Float, right: Float, bottom: Float, left: Float, top: Float, right: Float, bottom: Float,
startAngle: Float, sweepAngle: Float, useCenter: Boolean, paint: Paint, startAngle: Float, sweepAngle: Float, useCenter: Boolean, paint: Paint,
) { ) {
realizePendingClips()
prep().arcCore(brushFor(paint), startAngle, sweepAngle, useCenter, Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint)) prep().arcCore(brushFor(paint), startAngle, sweepAngle, useCenter, Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
} }
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) { override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
realizePendingClips()
prep().lineCore(brushFor(paint), p1, p2, paint.strokeWidth, paint.strokeCap, (paint.alpha * fAlpha)) prep().lineCore(brushFor(paint), p1, p2, paint.strokeWidth, paint.strokeCap, (paint.alpha * fAlpha))
} }
override fun drawPath(path: ComposePath, paint: Paint) { override fun drawPath(path: ComposePath, paint: Paint) {
realizePendingClips()
prep().pathCore(path, brushFor(paint), (paint.alpha * fAlpha), styleFor(paint)) prep().pathCore(path, brushFor(paint), (paint.alpha * fAlpha), styleFor(paint))
} }
@@ -696,7 +1042,32 @@ internal class Sdl3Canvas(
inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?, inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?,
inBaseItalic: Boolean, inBaseItalic: Boolean,
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?, inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
inLineHeightPx: Float,
inTrimFirstLine: Boolean,
) { ) {
// Capture mode: icon-font glyphs (Material Symbols via FreeType) record as an
// IconRun — flush pending geometry first for z-order, then capture the box in
// LAYER-LOCAL device coords (replay maps it through the layer affine + re-draws
// via FreeTypeIcons' cache). Plain/decorated AND spanned text ARE captured too
// (runSink set below; each styled run becomes a TextRun) — only a run with a
// SpanStyle.background still bails inside Sdl3TextRenderer.drawText.
val vCaptureList = fCaptureList
if (vCaptureList != null && inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
fScope.flush()
val vIconColor = if (fAlpha >= 1f) inColor else inColor.copy(alpha = inColor.alpha * fAlpha)
val vArgb = (vIconColor.a8 shl 24) or (vIconColor.r8 shl 16) or (vIconColor.g8 shl 8) or vIconColor.b8
vCaptureList.captureIconRun(
inFontFamily, inText, inFontSizePx, inFontVariations,
mapX(inX, inY), mapY(inX, inY), inBoxWidth, inBoxHeight, vArgb,
)
return
}
// Small centred labels (a popped bubble's "pop") usually sit fully
// inside a rounded clip — gate on containment like other draws, with
// a 2px margin for glyph overhang / AA bleed, instead of paying a
// mask pass per text run.
admitDraw(inX - 2f, inY - 2f, inX + inBoxWidth + 2f, inY + inBoxHeight + 2f)
com.compose.sdl.graphics.DrawStats.textDraws++
fScope.flush() fScope.flush()
// Paragraph-level decoration bits forwarded to every wrapped line. // Paragraph-level decoration bits forwarded to every wrapped line.
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
@@ -712,6 +1083,16 @@ internal class Sdl3Canvas(
// node's real box. The per-line path below centres within a lineHeight band // node's real box. The per-line path below centres within a lineHeight band
// (1.2 em for Material Symbols), taller than the size-clamped icon node, // (1.2 em for Material Symbols), taller than the size-clamped icon node,
// which pushed every icon ~0.1 em below centre. // which pushed every icon ~0.1 em below centre.
// Text is rasterised at its LOGICAL font size (stable glyph-texture cache
// + stable hinting) and blit 1:1 at the affine-mapped pen position; the
// layer's scale reaches position but NOT glyph size. Re-rasterising at a
// per-frame device size (an earlier attempt) made text under an animated
// layer scale — every Material popup/dropdown enter transition
// (Menu.kt scaleX/scaleY) — step through integer sizes with visible
// size/weight jumps, since SDL3_ttf re-hints at each size. Smoothly
// scaling glyphs would need blit-scaling a logical-size rasterisation
// (JVM/Skia GPU-scales the layer) — a future text-renderer enhancement;
// until then logical-size + no glyph scale is the stable choice.
if (inFontFamily != null && IconFont.isIconFamily(inFontFamily)) { if (inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
vTr.drawText( vTr.drawText(
inText = inText, inText = inText,
@@ -738,29 +1119,46 @@ internal class Sdl3Canvas(
// right edge instead of wrapping when the sidebar was resized narrower. // right edge instead of wrapping when the sidebar was resized narrower.
val vWrapWidth = if (inSoftWrap && inBoxWidth > 0f) inBoxWidth.toInt() else Int.MAX_VALUE val vWrapWidth = if (inSoftWrap && inBoxWidth > 0f) inBoxWidth.toInt() else Int.MAX_VALUE
val vWrapped = vRenderer.wrap(inText, inFontSizePx, vWrapWidth, inFontFamily, inFontVariations) val vWrapped = vRenderer.wrap(inText, inFontSizePx, vWrapWidth, inFontFamily, inFontVariations)
val vBaseLineH = vRenderer.lineHeight(inFontSizePx, inFontFamily, inFontVariations) // Line stacking mirrors SdlParagraph's per-mode model: compat trim (raw styles)
// keeps the FIRST line at the tight font cell; M3's Trim.None (inTrimFirstLine =
// false) makes every line the full band. Following lines always advance by the
// band (inLineHeightPx) when supplied, else the font cell.
val vFontCellH = vRenderer.lineHeight(inFontSizePx, inFontFamily, inFontVariations)
val vAdvance = if (inLineHeightPx > 0f) inLineHeightPx else vFontCellH
val vFirstLineH = if (inTrimFirstLine || inLineHeightPx <= 0f) vFontCellH else vAdvance
// Per-run fontSize spans make a line's box the TALLEST run cell on it — // Per-run fontSize spans make a line's box the TALLEST run cell on it —
// same styledLineCellHeight the paragraph measured with, so paint stacks // same styledLineCellHeight the paragraph measured with, so paint stacks
// lines exactly where layout put them. // lines exactly where layout put them.
val vMetricSpans = com.compose.sdl.text.spansAffectMetrics(inSpans) val vMetricSpans = com.compose.sdl.text.spansAffectMetrics(inSpans)
// Capture mode: route each plain line's run to the display list (spanned lines
// bail inside drawText via markUnsupported). Reset after the loop. No-op when
// not capturing (vCaptureList == null).
fTextRenderer?.runSink = vCaptureList
var vLineY = inY var vLineY = inY
for ((vIdx, vLine) in vWrapped.lines.withIndex()) { for ((vIdx, vLine) in vWrapped.lines.withIndex()) {
val vLineH = val vLineH =
if (vMetricSpans && inSpans != null) { if (vMetricSpans && inSpans != null) {
// Size spans can exceed the band, never shrink below it — the same
// max() the paragraph's lineHeights used, so paint tracks layout
// (first line keeps its tight cell under compat trim).
kotlin.math.max(
com.compose.sdl.text.styledLineCellHeight( com.compose.sdl.text.styledLineCellHeight(
vLine, vWrapped.lineStarts.getOrElse(vIdx) { 0 }, inSpans, vLine, vWrapped.lineStarts.getOrElse(vIdx) { 0 }, inSpans,
inFontSizePx, vTr.dpr, vRenderer, inFontFamily, inFontVariations, inFontSizePx, vTr.dpr, vRenderer, inFontFamily, inFontVariations,
),
if ((vIdx == 0 && inTrimFirstLine) || inLineHeightPx <= 0f) 0f else inLineHeightPx,
) )
} else vBaseLineH } else if (vIdx == 0) vFirstLineH else vAdvance
// Cull lines that would fall entirely below the box (softWrap keeps the // Cull lines that would fall entirely below the box (softWrap keeps the
// natural line count; the box just clips at draw time). // natural line count; the box just clips at draw time).
if (vLineY >= inY + inBoxHeight) break if (vLineY >= inY + inBoxHeight) break
vTr.drawText( vTr.drawText(
inText = vLine, inText = vLine,
// Position maps through the affine (translate/scale reach the origin); // Position maps through the affine; size/boxes switch to DEVICE
// glyph scaling/rotation isn't wired, so text in a rotated/scaled layer // units here (scaled by the affine's column norms) so glyphs
// repositions but doesn't itself scale or rotate. // scale with the layer. Rotation still only repositions.
inX = mapX(inX, vLineY).toInt(), inX = mapX(inX, vLineY).toInt(),
inY = mapY(inX, vLineY).toInt(), inY = mapY(inX, vLineY).toInt(),
inBoxWidth = inBoxWidth.toInt(), inBoxWidth = inBoxWidth.toInt(),
@@ -778,6 +1176,7 @@ internal class Sdl3Canvas(
) )
vLineY += vLineH vLineY += vLineH
} }
fTextRenderer?.runSink = null
} }
// ============ // ============
@@ -794,6 +1193,8 @@ internal class Sdl3Canvas(
inContentScale: androidx.compose.ui.layout.ContentScale, inContentScale: androidx.compose.ui.layout.ContentScale,
inAlpha: Float, inAlpha: Float,
) { ) {
if (captureUnsupported()) return
admitDraw(inX, inY, inX + inWidth, inY + inHeight)
fScope.flush() fScope.flush()
// Map the origin through the affine and scale the size by the matrix's axis // Map the origin through the affine and scale the size by the matrix's axis
// magnitudes (rotation contributes 1, so it only repositions the blit — the // magnitudes (rotation contributes 1, so it only repositions the blit — the
@@ -811,7 +1212,9 @@ internal class Sdl3Canvas(
// Not-yet-wired ops (B5 image leaf / B6 layers). Accept-and-ignore. // Not-yet-wired ops (B5 image leaf / B6 layers). Accept-and-ignore.
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) { override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
if (captureUnsupported()) return
val vBmp = image as? SdlImageBitmap ?: return val vBmp = image as? SdlImageBitmap ?: return
admitDraw(topLeftOffset.x, topLeftOffset.y, topLeftOffset.x + vBmp.width, topLeftOffset.y + vBmp.height)
drawImageRect( drawImageRect(
image, image,
androidx.compose.ui.unit.IntOffset.Zero, androidx.compose.ui.unit.IntOffset.Zero,
@@ -834,7 +1237,10 @@ internal class Sdl3Canvas(
dstSize: androidx.compose.ui.unit.IntSize, dstSize: androidx.compose.ui.unit.IntSize,
paint: Paint, paint: Paint,
) { ) {
if (captureUnsupported()) return
realizePendingClips()
val vTex = (image as? SdlImageBitmap)?.texture ?: return val vTex = (image as? SdlImageBitmap)?.texture ?: return
com.compose.sdl.graphics.DrawStats.imageBlits++
// Commit pending frame geometry and re-assert this canvas's target + clip // Commit pending frame geometry and re-assert this canvas's target + clip
// (an offscreen render just borrowed the render target). // (an offscreen render just borrowed the render target).
fScope.flush() fScope.flush()
@@ -112,6 +112,14 @@ internal class Sdl3DrawScope(
internal fun setMatrix(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) { internal fun setMatrix(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) {
fMa = a; fMb = b; fMc = c; fMd = d; fMe = e; fMf = f fMa = a; fMb = b; fMc = c; fMd = d; fMe = e; fMf = f
} }
// Current device affine (a,b,c,d,e,f) — screen = (a*x+c*y+e, b*x+d*y+f). Phase 4
// replay reads this to re-transform captured layer-local vertices.
internal fun currentDeviceMatrix(): FloatArray = floatArrayOf(fMa, fMb, fMc, fMd, fMe, fMf)
// Phase 4: when set, flush() CAPTURES the tessellated batch (layer-local, if this
// scope's base CTM is identity) instead of submitting to the GPU. null = normal
// immediate submit. See SdlDisplayList.
internal var recordingSink: GeometrySink? = null
// ============ // ============
// Vertex batch — heap-allocated buffer of SDL_Vertex shared across // Vertex batch — heap-allocated buffer of SDL_Vertex shared across
@@ -133,6 +141,13 @@ internal class Sdl3DrawScope(
call mid-scope when the buffer fills up (no cross-triangle state). */ call mid-scope when the buffer fills up (no cross-triangle state). */
fun flush() { fun flush() {
if (fBatchCount == 0) return if (fBatchCount == 0) return
val vSink = recordingSink
if (vSink != null) {
// Phase 4 recording: capture the batch (layer-local) instead of submitting.
vSink.captureGeometry(fVertexData, fBatchCount)
fBatchCount = 0
return
}
fVertexData.usePinned { vPinned -> fVertexData.usePinned { vPinned ->
platform.posix.memcpy( platform.posix.memcpy(
fBatch, fBatch,
@@ -141,6 +156,8 @@ internal class Sdl3DrawScope(
) )
} }
SDL_RenderGeometry(fRenderer.reinterpret(), null, fBatch, fBatchCount, null, 0) SDL_RenderGeometry(fRenderer.reinterpret(), null, fBatch, fBatchCount, null, 0)
com.compose.sdl.graphics.DrawStats.geometrySubmits++
com.compose.sdl.graphics.DrawStats.vertices += fBatchCount
fBatchCount = 0 fBatchCount = 0
} }
@@ -243,7 +260,10 @@ internal class Sdl3DrawScope(
val vB = vT + size.height val vB = vT + size.height
when (style) { when (style) {
Fill -> { Fill -> {
emitQuad(vL, vT, vR, vT, vR, vB, vL, vB, vSampler) // Gradient fills need interior samples (see emitRectMesh); solid
// fills are a single flat quad.
if (brush is SolidColor) emitQuad(vL, vT, vR, vT, vR, vB, vL, vB, vSampler)
else emitRectMesh(vL, vT, vR, vB, vSampler)
// Axis-aligned rects are pixel-crisp and need no AA. When the affine // Axis-aligned rects are pixel-crisp and need no AA. When the affine
// rotates/shears them, the edges become diagonal — feather each edge // rotates/shears them, the edges become diagonal — feather each edge
// outward (normals in local space; the matrix orients them on screen). // outward (normals in local space; the matrix orients them on screen).
@@ -524,20 +544,30 @@ internal class Sdl3DrawScope(
val vY = fOriginY + topLeft.y val vY = fOriginY + topLeft.y
val vW = size.width val vW = size.width
val vH = size.height val vH = size.height
// Gradient fills sample per-vertex, so the interior must be meshed
// (corner-only sampling renders radial/sweep flat — see emitRectMesh);
// solid fills stay single quads.
val vGrad = brush !is SolidColor
if (vR <= 0f) { if (vR <= 0f) {
// Trivial: just two triangles. if (vGrad) emitRectMesh(vX, vY, vX + vW, vY + vH, vSampler)
emitQuad(vX, vY, vX + vW, vY, vX + vW, vY + vH, vX, vY + vH, vSampler) else emitQuad(vX, vY, vX + vW, vY, vX + vW, vY + vH, vX, vY + vH, vSampler)
return return
} }
// Body in 3 strips: middle (full width × inner height), top edge, // Body in 3 strips: middle (full width × inner height), top edge,
// bottom edge — plus the 4 corner arcs. // bottom edge — plus the 4 corner arcs.
if (style == Fill) { if (style == Fill) {
if (vGrad) {
emitRectMesh(vX, vY + vR, vX + vW, vY + vH - vR, vSampler) // middle
emitRectMesh(vX + vR, vY, vX + vW - vR, vY + vR, vSampler) // top edge
emitRectMesh(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH, vSampler) // bottom edge
} else {
// Middle strip // Middle strip
emitQuad(vX, vY + vR, vX + vW, vY + vR, vX + vW, vY + vH - vR, vX, vY + vH - vR, vSampler) emitQuad(vX, vY + vR, vX + vW, vY + vR, vX + vW, vY + vH - vR, vX, vY + vH - vR, vSampler)
// Top edge (between left+right corners) // Top edge (between left+right corners)
emitQuad(vX + vR, vY, vX + vW - vR, vY, vX + vW - vR, vY + vR, vX + vR, vY + vR, vSampler) emitQuad(vX + vR, vY, vX + vW - vR, vY, vX + vW - vR, vY + vR, vX + vR, vY + vR, vSampler)
// Bottom edge // Bottom edge
emitQuad(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH - vR, vX + vW - vR, vY + vH, vX + vR, vY + vH, vSampler) emitQuad(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH - vR, vX + vW - vR, vY + vH, vX + vR, vY + vH, vSampler)
}
// 4 corner fills — segment count adapts to the corner radius. // 4 corner fills — segment count adapts to the corner radius.
val vSeg = arcSegments(90f, vR) val vSeg = arcSegments(90f, vR)
emitFilledArc(vX + vR, vY + vR, vR, vR, 180f, 90f, false, vSeg, vSampler) emitFilledArc(vX + vR, vY + vR, vR, vR, 180f, 90f, false, vSeg, vSampler)
@@ -854,6 +884,34 @@ internal class Sdl3DrawScope(
emitTri(ax, ay, cx, cy, dx, dy, inSampler) emitTri(ax, ay, cx, cy, dx, dy, inSampler)
} }
// Axis-aligned rect subdivided into a grid, so a per-vertex sampler is
// evaluated across the INTERIOR, not just the 4 corners. Corner-only
// sampling renders radial/sweep gradients flat (a square's corners are all
// equidistant from its centre → identical t → solid fill) and clips
// non-uniform multi-stop linear ramps. SDL interpolates linearly within
// each cell, so ~16 cells/axis tracks the gradient curve closely. Solid
// fills never come here (they use emitQuad — one quad).
private fun emitRectMesh(
inL: Float, inT: Float, inR: Float, inB: Float, inSampler: Sampler,
) {
// ~one cell per 8 device px per axis (1..24), so thin edge strips stay
// cheap while large fills get enough interior samples for the gradient.
val vCols = (((inR - inL) / 8f).toInt()).coerceIn(1, 24)
val vRows = (((inB - inT) / 8f).toInt()).coerceIn(1, 24)
val vW = (inR - inL) / vCols
val vH = (inB - inT) / vRows
for (vRow in 0 until vRows) {
val vY0 = inT + vRow * vH
val vY1 = if (vRow == vRows - 1) inB else vY0 + vH
for (vCol in 0 until vCols) {
val vX0 = inL + vCol * vW
val vX1 = if (vCol == vCols - 1) inR else vX0 + vW
emitTri(vX0, vY0, vX1, vY0, vX1, vY1, inSampler)
emitTri(vX0, vY0, vX1, vY1, vX0, vY1, inSampler)
}
}
}
private fun emitTri( private fun emitTri(
ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float, ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float,
inSampler: Sampler, inSampler: Sampler,
@@ -889,11 +947,39 @@ internal class Sdl3DrawScope(
emitFringeQuad(x0, y0, x1, y1, x1 + inNx, y1 + inNy, x0 + inNx, y0 + inNy, inSampler) emitFringeQuad(x0, y0, x1, y1, x1 + inNx, y1 + inNy, x0 + inNx, y0 + inNy, inSampler)
} }
// Phase 4 replay: re-emit a captured (layer-local) batch through the CURRENT CTM.
// The batch was recorded with an identity base CTM, so applying fMa..fMf here
// places it at the layer's transform. Colours/UVs pass through unchanged. Runs
// in NORMAL (non-recording) mode → the next flush() submits via SDL_RenderGeometry.
internal fun replayBatch(vd: FloatArray, count: Int) {
for (i in 0 until count) {
if (fBatchCount >= kBatchCapacity && fBatchCount % 3 == 0) flush()
val s = i * kFloatsPerVertex
val x = vd[s]; val y = vd[s + 1]
val base = fBatchCount * kFloatsPerVertex
fVertexData[base + 0] = fMa * x + fMc * y + fMe
fVertexData[base + 1] = fMb * x + fMd * y + fMf
fVertexData[base + 2] = vd[s + 2]
fVertexData[base + 3] = vd[s + 3]
fVertexData[base + 4] = vd[s + 4]
fVertexData[base + 5] = vd[s + 5]
fVertexData[base + 6] = vd[s + 6]
fVertexData[base + 7] = vd[s + 7]
fBatchCount++
}
}
// Stage one vertex into fVertexData (auto-flush when full). Position goes // Stage one vertex into fVertexData (auto-flush when full). Position goes
// through the current affine so scale/rotate reach the GPU; the colour is // through the current affine so scale/rotate reach the GPU; the colour is
// sampled at the pre-transform point so a gradient rides its shape. // sampled at the pre-transform point so a gradient rides its shape.
private fun writeVertex(inX: Float, inY: Float, inColor: ComposeColor, inAlphaScale: Float = 1f) { private fun writeVertex(inX: Float, inY: Float, inColor: ComposeColor, inAlphaScale: Float = 1f) {
if (fBatchCount >= kBatchCapacity) flush() // Auto-flush ONLY at a triangle boundary: SDL_RenderGeometry renders
// floor(count/3) triangles and silently drops the tail, so flushing
// mid-triangle tears it AND misaligns every following triangle in the
// new batch (seen as ~350-degree pac-man circles and glitch shapes
// once a frame's geometry outgrew one batch - e.g. 84 unclipped
// drawBehind bubbles). kBatchCapacity is a multiple of 3.
if (fBatchCount >= kBatchCapacity && fBatchCount % 3 == 0) flush()
val vBase = fBatchCount * kFloatsPerVertex val vBase = fBatchCount * kFloatsPerVertex
fVertexData[vBase + 0] = fMa * inX + fMc * inY + fMe fVertexData[vBase + 0] = fMa * inX + fMc * inY + fMe
fVertexData[vBase + 1] = fMb * inX + fMd * inY + fMf fVertexData[vBase + 1] = fMb * inX + fMd * inY + fMf
@@ -914,15 +1000,17 @@ private const val kAaFeather: Float = 1.0f
private const val kAaHalf: Float = kAaFeather * 0.5f private const val kAaHalf: Float = kAaFeather * 0.5f
// ============ // ============
// Batch capacity — 8192 vertices = ~2730 triangles per submission. At // Batch capacity — 8190 vertices = 2730 triangles per submission (kept a
// 64 segments per full circle that's room for ~21 full-circle filled // MULTIPLE OF 3: the auto-flush in writeVertex only fires on triangle
// shapes per Canvas{} before any flush. Bigger gives fewer GPU // boundaries, so the cap must land on one). At 64 segments per full circle
// submissions; smaller saves RAM. ~128 KB at 16 bytes per SDL_Vertex. // that's room for ~21 full-circle filled shapes per Canvas{} before any
private const val kBatchCapacity: Int = 8192 // flush. Bigger gives fewer GPU submissions; smaller saves RAM. ~128 KB at
// 16 bytes per SDL_Vertex.
private const val kBatchCapacity: Int = 8190
// Floats per SDL_Vertex: position(x,y) + color(r,g,b,a) + tex_coord(x,y), tightly // Floats per SDL_Vertex: position(x,y) + color(r,g,b,a) + tex_coord(x,y), tightly
// packed. Used to stage vertices in a Kotlin FloatArray and memcpy them across. // packed. Used to stage vertices in a Kotlin FloatArray and memcpy them across.
private const val kFloatsPerVertex: Int = 8 internal const val kFloatsPerVertex: Int = 8
// ================== // ==================
// MARK: Brush → per-vertex colour sampler // MARK: Brush → per-vertex colour sampler

Some files were not shown because too many files have changed in this diff Show More