mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 18:57:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f7595a5f | ||
|
|
cdce0575e3 | ||
|
|
623266d467 | ||
|
|
52817af282 | ||
|
|
749a405a03 | ||
|
|
675bc99b23 | ||
|
|
eae87b98ba | ||
|
|
982dc50d2a | ||
|
|
5ec5f4cb9a | ||
|
|
3c759476ac | ||
|
|
7ee2e4a09f | ||
|
|
2b49ececa7 | ||
|
|
f16ceea0da | ||
|
|
7f332ead0a | ||
|
|
d022462cad | ||
|
|
9637215931 | ||
|
|
66653f903e | ||
|
|
88efdba9f7 | ||
|
|
487e151ac8 | ||
|
|
e68de8794f | ||
|
|
c8aefebbf1 | ||
|
|
cc04c82042 | ||
|
|
416ae3a03e | ||
|
|
9e97b4357f | ||
|
|
8c5a795055 | ||
|
|
9a8540499d | ||
|
|
68ea57204c | ||
|
|
e3f3d64d0b | ||
|
|
229a39b38d | ||
|
|
57cf99608b | ||
|
|
05ec6d5b40 | ||
|
|
e2d125dfea | ||
|
|
50b31641ed | ||
|
|
71738dafb7 | ||
|
|
739d6e4852 | ||
|
|
c828888590 | ||
|
|
9b165a07d2 | ||
|
|
971047b799 | ||
|
|
1e8ccdbeee | ||
|
|
8f53880854 | ||
|
|
82d2e9b16f | ||
|
|
27d1dc1637 | ||
|
|
b16f456aaf | ||
|
|
bdb5c64df3 | ||
|
|
5dc78f47b5 | ||
|
|
862da85de0 | ||
|
|
25689c1f17 | ||
|
|
275ee97d56 | ||
|
|
e5fb04f882 | ||
|
|
23849330d1 | ||
|
|
4272cec614 | ||
|
|
3f4b3a23be | ||
|
|
d058f1b6a2 | ||
|
|
924ebe9776 | ||
|
|
5dc4b21867 | ||
|
|
accd74c9ee | ||
|
|
30cc39606a | ||
|
|
099211b015 | ||
|
|
37a44ffb46 | ||
|
|
045c734eef | ||
|
|
83c1902b08 | ||
|
|
076d60460c | ||
|
|
93b7ff28e1 | ||
|
|
08968fe820 | ||
|
|
3a57e96c80 | ||
|
|
332ba757fe | ||
|
|
09fe92fc95 | ||
|
|
ec2451aad1 | ||
|
|
ff32d68a72 | ||
|
|
1a56b7dcaf | ||
|
|
7c5abc2705 |
@@ -7,6 +7,9 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
# Manual fallback: run with --ref <tag> so github.ref_name is the tag itself
|
||||
# (PUBLISH_VERSION is derived from it).
|
||||
workflow_dispatch:
|
||||
|
||||
# Never overlap two publish runs for the same tag.
|
||||
concurrency:
|
||||
@@ -103,16 +106,13 @@ jobs:
|
||||
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
|
||||
# link time via --defsym in sdl3.def.
|
||||
|
||||
# Build the static native deps from scripts/build-sdl/build-sdl.properties.
|
||||
# Only Windows ships the SDL3 renderer (no Skiko on mingw), so only it needs
|
||||
# SDL3_ttf / SDL3_image / FreeType (SDL3_ttf text, SDL3_image decode, FreeType
|
||||
# icon-axis rasterisation). macOS / Linux publish the Skia renderer — Skiko
|
||||
# handles text / images / icons — so they only need base SDL3 for the main
|
||||
# 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.
|
||||
# Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
|
||||
# target renders through Skia now, so all platforms need only base SDL3 for
|
||||
# the main loop (windowing / input / audio / filesystem) — the from-scratch
|
||||
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
|
||||
# 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/
|
||||
id: cache-libs
|
||||
uses: actions/cache@v4
|
||||
@@ -120,16 +120,10 @@ jobs:
|
||||
path: libs
|
||||
key: sdl-libs-v3-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
|
||||
|
||||
# Windows: full SDL3 renderer stack. macOS / Linux (Skia): base SDL3 only.
|
||||
- name: Build static native deps
|
||||
if: steps.cache-libs.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
python3 scripts/build-sdl/build-all.py
|
||||
else
|
||||
python3 scripts/build-sdl/build-all.py sdl3
|
||||
fi
|
||||
run: python3 scripts/build-sdl/build-all.py
|
||||
|
||||
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
|
||||
# androidx.compose.* files. Sync copies them in from the upstream refs
|
||||
@@ -160,16 +154,35 @@ jobs:
|
||||
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"
|
||||
MODULES="sdl-core ui ui-graphics ui-text ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple desktop-native-window material-symbols navigation3-ui components-resources"
|
||||
case "${{ matrix.host }}" in
|
||||
macos) SUFFIXES="-macosarm64" ;;
|
||||
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
||||
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
||||
esac
|
||||
# Published group per module — mirrors the per-area com.bitsycore groups
|
||||
# in root build.gradle.kts. Project-only modules (sdl-core, material-symbols)
|
||||
# fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven
|
||||
# package name is "<group>.<artifact>".
|
||||
group_of() {
|
||||
case "$1" in
|
||||
ui|ui-graphics|ui-text|ui-util|ui-geometry|ui-unit|ui-backhandler|ui-tooling-preview)
|
||||
echo "com.bitsycore.compose.ui" ;;
|
||||
foundation|foundation-layout) echo "com.bitsycore.compose.foundation" ;;
|
||||
animation|animation-core|animation-graphics) echo "com.bitsycore.compose.animation" ;;
|
||||
material3) echo "com.bitsycore.compose.material3" ;;
|
||||
material-ripple) echo "com.bitsycore.compose.material" ;;
|
||||
components-resources) echo "com.bitsycore.compose.components" ;;
|
||||
navigation3-ui) echo "com.bitsycore.navigation3" ;;
|
||||
desktop-native-window) echo "com.bitsycore.compose" ;;
|
||||
*) echo "com.bitsycore.compose.sdl" ;;
|
||||
esac
|
||||
}
|
||||
host_packages() {
|
||||
for m in $MODULES; do
|
||||
g=$(group_of "$m")
|
||||
for s in $SUFFIXES; do
|
||||
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
|
||||
if [ "$s" = "_ROOT_" ]; then echo "$g.$m"; else echo "$g.$m$s"; fi
|
||||
done
|
||||
done
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
@@ -183,7 +196,7 @@ jobs:
|
||||
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')
|
||||
| jq -r --arg v "$VER" 'if type=="array" then ([.[] | select(.name==$v) | .id] | first // empty) else empty end')
|
||||
[ -z "$vid" ] && continue
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
@@ -214,6 +227,33 @@ jobs:
|
||||
echo "::error::Publish failed after 3 attempts"
|
||||
exit 1
|
||||
|
||||
# Second target: the self-hosted Reposilite instance. Same publications,
|
||||
# task names derived from the matrix by swapping the repository name.
|
||||
# The releases repo has redeployment enabled, so a plain retry after a
|
||||
# mid-upload failure is safe (no GitHub-Packages-style 409 poisoning).
|
||||
- name: Publish to maven.bitsycore.com
|
||||
shell: bash
|
||||
env:
|
||||
BITSYCORE_MAVEN_USER: ${{ secrets.BITSYCORE_MAVEN_USER }}
|
||||
BITSYCORE_MAVEN_TOKEN: ${{ secrets.BITSYCORE_MAVEN_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
BITSY_TASKS=$(echo "${{ matrix.tasks }}" | sed 's/GitHubPackagesRepository/BitsycoreRepository/g')
|
||||
for attempt in 1 2 3; do
|
||||
echo "::group::Bitsycore publish attempt $attempt"
|
||||
if ./gradlew $BITSY_TASKS --continue --no-daemon --stacktrace; then
|
||||
echo "::endgroup::"
|
||||
exit 0
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "::warning::Bitsycore publish attempt $attempt failed — retrying"
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
echo "::error::Bitsycore publish failed after 3 attempts"
|
||||
exit 1
|
||||
|
||||
# Release binaries for the host target — the binary plus the data.kres
|
||||
# (bundled composeResources + fonts + subset icon-font). Each app's
|
||||
# linkReleaseExecutable<Target> auto-runs its data.kres zip task —
|
||||
@@ -242,8 +282,21 @@ jobs:
|
||||
for app in demo apidemo; do
|
||||
src="$app/build/bin/$target_dir/releaseExecutable"
|
||||
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
|
||||
files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres")
|
||||
# Windows Skia leg ships two runtime sidecars next to the exe — the
|
||||
# skiko fork DLL and Skia's ICU data (icudtl.dat) — provisioned by the
|
||||
# bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run
|
||||
# without them, so include them (and fail loudly if a provision regressed).
|
||||
if [ "${{ matrix.exe_ext }}" = "exe" ]; then
|
||||
for sidecar in skiko-windows-x64.dll icudtl.dat; do
|
||||
if [ ! -f "$src/$sidecar" ]; then
|
||||
echo "::error::Missing Windows runtime sidecar: $src/$sidecar"; exit 1
|
||||
fi
|
||||
files+=("$src/$sidecar")
|
||||
done
|
||||
fi
|
||||
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()' \
|
||||
"$zip_path" "$src/$app.${{ matrix.exe_ext }}" "$src/data.kres"
|
||||
"$zip_path" "${files[@]}"
|
||||
done
|
||||
ls -la dist/
|
||||
|
||||
|
||||
@@ -7,14 +7,22 @@ context — read it first, then look at the files it points to.
|
||||
|
||||
- [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.
|
||||
- Renderer — one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows /
|
||||
CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine.
|
||||
Retained per-node display lists: transform / alpha / clip changes REPLAY without
|
||||
re-recording (only a content change or resize re-records; dirty-region rendering
|
||||
is a non-goal). Text is a reduced-local port over Skia `skparagraph`. Read these
|
||||
before touching renderer / graphics-actual / layer-engine code:
|
||||
`SkiaRenderBackend.kt`, `RenderBackend.kt`, `GpuMode.kt`, `ComposeRootHost.kt`
|
||||
(root host + snapshot-observation sweep — omitting `clearInvalidObservations()`
|
||||
once leaked the whole graph), `ComposeOwner.kt`, and `SkiaParagraph.native.kt` /
|
||||
`SkiaParagraphEngine.kt`. Open renderer/fidelity work lives in [PLAN.md](PLAN.md).
|
||||
- [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.
|
||||
- [PLAN.md](PLAN.md) §2 — audited list of no-ops, stubs, and hardcodes left in the
|
||||
port, with P0/P1/P2 severity, plus the road-to-1.0.0 fidelity / SDL / release work
|
||||
(this subsumes the former TODO.md, which was never committed).
|
||||
- This file — architecture, module layout, vendoring rules, source-set
|
||||
hierarchy, density flow, conventions, and common pitfalls.
|
||||
|
||||
@@ -24,11 +32,16 @@ context — read it first, then look at the files it points to.
|
||||
on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux
|
||||
(x64/arm64), Windows (mingwX64).
|
||||
|
||||
Rendering is pluggable behind one `RenderBackend`:
|
||||
Rendering is **Skia everywhere** behind one `RenderBackend` — Metal / OpenGL
|
||||
/ CPU raster:
|
||||
|
||||
- **Skia** (via Skiko klibs) on macOS + Linux — Metal / OpenGL / CPU raster.
|
||||
- **SDL3** (`SDL3_ttf` + `SDL_RenderGeometry`) on Windows, and on macOS/Linux
|
||||
when `-Prenderer=sdl3` is passed.
|
||||
- **macOS + Linux** link the OFFICIAL Skiko klibs from Maven.
|
||||
- **Windows (mingwX64)** links the bitsycore skiko FORK — skiko+Skia compiled
|
||||
into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via
|
||||
an embedded GNU import lib, published to GitHub Packages as
|
||||
`com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override with
|
||||
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
|
||||
the bridge plugin (`installWindowsSkiaDll`).
|
||||
|
||||
Windowing, input, audio, filesystem access, and the OS-integration surface
|
||||
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
|
||||
@@ -41,8 +54,11 @@ re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
|
||||
## Module layout
|
||||
|
||||
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
|
||||
Only `:window` is the SDL integration layer ("Compose SDL" — SDL3 main loop)
|
||||
and lives under `compose/sdl/`.
|
||||
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
|
||||
primitives — zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and
|
||||
`:desktop-native-window` (the SDL3 main-loop shell + app entry point) at
|
||||
`compose/desktop/native/window/`. `:ui` depends on `:sdl-core` and its renderer
|
||||
+ SDL↔Compose bridges pick the cinterop from it.
|
||||
|
||||
One Gradle module per upstream artifact; the directory mirrors the upstream
|
||||
`compose/` path, the gradle path is kept short (redirected via `projectDir`).
|
||||
@@ -50,10 +66,18 @@ One Gradle module per upstream artifact; the directory mirrors the upstream
|
||||
```
|
||||
compose/
|
||||
├── ui/
|
||||
│ ├── ui/ → :ui — androidx.compose.ui.* (ui + ui-graphics + ui-text) +
|
||||
│ │ com.compose.sdl.* — cinterops + BOTH renderer pipelines live
|
||||
│ │ here. ui-graphics/ui-text can't split off: their Canvas /
|
||||
│ │ Paragraph `expect`s are the renderers' `actual`s (same-module).
|
||||
│ ├── ui/ → :ui — androidx.compose.ui.* CORE (Modifier, LayoutNode,
|
||||
│ │ composition, semantics, input, focus) + com.compose.sdl.* —
|
||||
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
|
||||
│ │ bridges (events / clipboard / cursors / window). Depends on
|
||||
│ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text +
|
||||
│ │ the sdl3 cinterop were split OUT — upstream layout.)
|
||||
│ ├── ui-graphics/ → :ui-graphics — androidx.compose.ui.graphics.* + the Skia actuals
|
||||
│ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache,
|
||||
│ │ painter/image + resource seams). → skiko; SDL-free.
|
||||
│ ├── ui-text/ → :ui-text — androidx.compose.ui.text.* + the skiko text engine
|
||||
│ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont,
|
||||
│ │ NamedFont). → :ui-graphics, skiko; SDL-free.
|
||||
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
||||
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
||||
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
||||
@@ -74,16 +98,21 @@ compose/
|
||||
│ └── material3/ → :material3 — androidx.compose.material3.*
|
||||
├── material/
|
||||
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.*
|
||||
└── sdl/ ("Compose SDL" — project modules, not upstream CMP artifacts)
|
||||
└── window/ → :window — nativeComposeApp { Window(...) {} } multi-window
|
||||
shell + SDL3 main loop; nativeComposeWindow() wrapper
|
||||
└── desktop/native/window/ → :desktop-native-window — nativeComposeApp { Window(...) {} }
|
||||
multi-window shell + SDL3 main loop; nativeComposeWindow()
|
||||
wrapper (project app-shell, not an upstream CMP artifact).
|
||||
Published as com.bitsycore.compose:desktop-native-window.
|
||||
|
||||
sdl/
|
||||
└── sdl-core/ → :sdl-core — the NAKED sdl3 cinterop + platform primitives
|
||||
(zero Compose dep, like skiko); the single `sdl3` cinterop
|
||||
lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core.
|
||||
|
||||
utils/
|
||||
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
||||
(Outlined / Rounded / Sharp). COMMON API (usable from
|
||||
shared app code) + per-stack actuals: native renders
|
||||
via :foundation IconFontIcon (IconFont handles the
|
||||
SDL3/Skia split), jvm() via Skiko directly
|
||||
via :foundation IconFontIcon (Skia), 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
|
||||
@@ -106,8 +135,8 @@ components/
|
||||
no mingwX64/linux klibs. Platform layer is project
|
||||
code: data.kres ResourceReader, pure-Kotlin
|
||||
DomXmlParser (upstream's is Darwin NSXMLParser),
|
||||
SDL3_image decode via the :ui EncodedImageDecoder
|
||||
hook, NamedFont registration, SDL locale/theme env.
|
||||
image decode via the :ui EncodedImageDecoder hook
|
||||
(Skia), NamedFont registration, SDL locale/theme env.
|
||||
Apps' JVM targets keep the Maven artifact — the
|
||||
generated Res accessors work against BOTH.
|
||||
|
||||
@@ -173,11 +202,11 @@ scripts/ → vendor-sync + python helper scripts (compose-coverage =
|
||||
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
|
||||
output of scripts/build-sdl/build-all.py on Windows
|
||||
libs/ → gitignored per-host static SDL3 output of
|
||||
scripts/build-sdl/build-all.py on Windows
|
||||
```
|
||||
|
||||
Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) —
|
||||
Module PATHS stay short (`:ui`, `:foundation`, `:desktop-native-window`, …) —
|
||||
`settings.gradle.kts` redirects `projectDir` for each so build files across
|
||||
the repo stay terse. `androidx.collection` is a plain Maven dependency
|
||||
(`androidx.collection:collection`), not a module — same as other simple
|
||||
@@ -189,23 +218,27 @@ androidx KMP libs.
|
||||
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
|
||||
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
|
||||
:material-ripple ←────────────┘──────────────┘
|
||||
:foundation, :animation-core ← :window ; :foundation, :material3 ← :material-symbols
|
||||
:foundation, :animation-core ← :desktop-native-window ; :foundation, :material3 ← :material-symbols
|
||||
```
|
||||
|
||||
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
|
||||
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
|
||||
`:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`;
|
||||
`:ui → :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
|
||||
`:sdl-core → sdl3 cinterop (NAKED — no Compose)`;
|
||||
`:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`;
|
||||
`:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`;
|
||||
`:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
|
||||
`:foundation-layout → :ui`; `:animation → :animation-core, :foundation-layout`;
|
||||
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
|
||||
`:material-ripple → :foundation, :animation-core`;
|
||||
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
|
||||
|
||||
`:ui` is the renderer/cinterop hub (the ui-graphics + ui-text `expect`s resolve to
|
||||
its SDL/Skia renderer `actual`s). The pure lower artifacts (`:ui-util`,
|
||||
`:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are split out below it. Everything
|
||||
above `:ui` can only touch renderer / cinterop internals via its public surface.
|
||||
`:window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
|
||||
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
|
||||
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
|
||||
`actual`s, SDL-free) and the naked `:sdl-core` (sdl3 cinterop). The pure lower
|
||||
artifacts (`:ui-util`, `:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are below.
|
||||
Everything above `:ui` touches renderer internals only via its public surface.
|
||||
`:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
|
||||
install the popup / scaffold layer at the composition root).
|
||||
|
||||
## Vendoring philosophy — read this before writing any `androidx.compose.*` code
|
||||
@@ -222,7 +255,7 @@ compose-multiplatform umbrella repo that `:components-resources` vendors from).
|
||||
There is no implicit default — `SET_REPO` is required. `scripts/compose-fork/sync.sh`
|
||||
walks all manifests and copies each selected file byte-for-byte from the pinned
|
||||
checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into
|
||||
`<module>/src/vendor/{common,native,skikoRenderer,sdlRenderer}/kotlin/`. The
|
||||
`<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The
|
||||
`src/vendor/` tree is **gitignored** — you don't check it in, you re-sync
|
||||
on demand.
|
||||
|
||||
@@ -247,8 +280,8 @@ Two categories of code live in each module:
|
||||
Never hand-edit these. If upstream diverges, adjust the manifest or the
|
||||
pinned ref and re-sync. This is the bulk of the codebase (~1500 files
|
||||
across the modules).
|
||||
2. **Project code (in `src/commonMain/`, `src/nativeMain/`, `src/skikoRendererMain/`,
|
||||
`src/sdlRendererMain/`)** — code we author (project actuals, glue between
|
||||
2. **Project code (in `src/commonMain/`, `src/nativeMain/`,
|
||||
`src/skikoRendererMain/`)** — code we author (project actuals, glue between
|
||||
Compose and SDL3, project-specific extensions).
|
||||
|
||||
### The 5 rules for adding upstream Compose surface
|
||||
@@ -288,14 +321,13 @@ Two categories of code live in each module:
|
||||
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
|
||||
an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream
|
||||
ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
|
||||
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain`.
|
||||
Then hand-roll (or minimally vendor) the SDL3 counterpart in
|
||||
`sdlRendererMain` — `SkiaCanvas.kt` ↔ `Sdl3Canvas.kt`,
|
||||
`SkiaTextRenderer.kt` ↔ `Sdl3TextRenderer.kt` (using SDL3_ttf +
|
||||
FreeType), `SkiaImageCache.kt` ↔ `Sdl3ImageCache.kt` (using SDL3_image).
|
||||
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
|
||||
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
|
||||
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` — graphics
|
||||
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
|
||||
text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into
|
||||
`:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set;
|
||||
mingwX64 layers its fork actuals on top under `skikoRendererMingwMain`.
|
||||
|
||||
5. **Multi-OS project code goes through SDL3, not hand-rolled per-target
|
||||
ifdefs.** SDL3 already handles the platform differences for filesystem
|
||||
@@ -313,52 +345,41 @@ call sites don't care.
|
||||
|
||||
## Source-set hierarchy (:ui only)
|
||||
|
||||
`:ui` owns cinterops + both renderer pipelines. Its source-set tree:
|
||||
`:ui` owns the Skia RenderBackend + GPU bridges. The SAME source-set tree is used
|
||||
by `:ui-graphics` / `:ui-text` for their skiko `actual`s (only the deps differ);
|
||||
the `sdl3` cinterop lives in `:sdl-core` (see below). `:ui`'s tree:
|
||||
|
||||
```
|
||||
commonMain
|
||||
└── nativeMain (vendored .native.kt + project native code)
|
||||
├── skikoRendererMain (Skia drawing pipeline; Skiko on classpath)
|
||||
├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath)
|
||||
│ ├── skikoRendererMacosMain (macOS-only Skia actuals — Metal bridge)
|
||||
│ └── skikoRendererLinuxMain (Linux-only Skia actuals — OpenGL)
|
||||
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main
|
||||
│ ONLY when the Skia renderer is active for the target.
|
||||
└── sdlRendererMain (SDL3 drawing pipeline + TTF/image/FreeType)
|
||||
├── sdlRendererMacosMain (macOS-only SDL3 driver hint)
|
||||
├── sdlRendererLinuxMain (Linux-only SDL3 driver hint)
|
||||
└── sdlRendererMingwMain (mingwX64-only SDL3 driver hint)
|
||||
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
|
||||
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main.
|
||||
└── skikoRendererMingwSharedMain
|
||||
└── skikoRendererMingwMain (mingwX64-only Skia actuals — the bitsycore
|
||||
skiko FORK: skiko-windows-x64.dll bound via
|
||||
an embedded GNU import lib; OpenGL context)
|
||||
attached to: mingwX64Main.
|
||||
```
|
||||
|
||||
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are `expect`s in
|
||||
`:ui`'s nativeMain with `actual`s in BOTH `skikoRendererMain` and
|
||||
`sdlRendererMain` — unambiguous because **only one of the two renderer source
|
||||
sets is attached to a given target**. (They used to be plain duplicate
|
||||
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
|
||||
pulled in.
|
||||
`:ui`'s nativeMain with `actual`s in `skikoRendererMain` (shared by every native
|
||||
target). `rendererPreferredGpuMode()` picks Metal on macOS, OpenGL on
|
||||
Linux + Windows, with a Software (CPU raster) auto-fallback if the GPU context
|
||||
fails to come up.
|
||||
|
||||
### Cinterop sibling-dependency gotcha
|
||||
### Cinterop
|
||||
|
||||
`:ui` owns four cinterops in `src/nativeInterop/cinterop/`:
|
||||
`sdl3`, `sdl3_ttf`, `sdl3_image`, `freetype`. The `.def` files for `sdl3_ttf` /
|
||||
`sdl3_image` carry `depends = sdl3` so their `SDL_Surface` / `SDL_Color`
|
||||
references resolve to the *same* types `sdl3` produces (not duplicates inside
|
||||
`sdl3_image.SDL_Surface`). **Gradle does not automatically add a sibling
|
||||
cinterop's klib to a cinterop task's `-library` list**, so the manifest
|
||||
directive silently fails and you get cryptic
|
||||
`expected CPointer<sdl3.SDL_Surface>?, actual CPointer<sdl3_image.SDL_Surface>?`
|
||||
errors.
|
||||
|
||||
`:ui/build.gradle.kts` works around this by passing the sdl3 cinterop output
|
||||
klib path explicitly via `extraOpts("-library", vSdl3Klib)` on `sdl3_ttf` /
|
||||
`sdl3_image`, plus a task dependency
|
||||
(`cinteropSdl3_ttf*Target.dependsOn(cinteropSdl3*Target)`). If you ever add
|
||||
another `depends = sdl3` cinterop, add it to that list too.
|
||||
The `sdl3` cinterop lives in the NAKED `:sdl-core` module
|
||||
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
|
||||
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
|
||||
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
|
||||
bridges — and `:desktop-native-window` downstream — see `sdl3.*` and inherit SDL3's static-lib +
|
||||
linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the
|
||||
per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps
|
||||
link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose
|
||||
dependency, so `:ui → :sdl-core` is cycle-free — the compose way (like `ui → skiko`).
|
||||
|
||||
## Density flow (Option B — layout in physical pixels)
|
||||
|
||||
@@ -392,13 +413,13 @@ Passing raw `px.dp` will double-scale on Retina.
|
||||
./gradlew :demo:runDebugExecutableLinuxX64
|
||||
./gradlew :apidemo:runDebugExecutableLinuxX64
|
||||
|
||||
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop)
|
||||
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop).
|
||||
# Links the bitsycore skiko FORK from GitHub Packages (com.bitsycore.skiko:skiko:
|
||||
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops
|
||||
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
|
||||
gradlew.bat :demo:runDebugExecutableMingwX64
|
||||
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||
|
||||
# Skiko-free build on macOS/Linux — SDL3 renderer everywhere
|
||||
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
|
||||
|
||||
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
|
||||
# screens on upstream Compose; differences vs the native build = port bugs.
|
||||
./gradlew :demo:run
|
||||
@@ -407,12 +428,16 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||
|
||||
### System dependencies
|
||||
|
||||
SDL3 + SDL3_ttf + SDL3_image + FreeType (+ image codecs) are **built from
|
||||
source as static libraries on every OS** and linked straight into the
|
||||
executable — no brew/apt SDL packages, no runtime .dll/.so/.dylib; a
|
||||
distributable is `<app>` + `data.kres`. Build them once per host with
|
||||
`python scripts/build-sdl/build-all.py`. Per-host toolchain requirements and
|
||||
the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
|
||||
SDL3 is **built from source as a static library on every OS** and linked
|
||||
straight into the executable — no brew/apt SDL packages. Build it once per host
|
||||
with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements
|
||||
and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
|
||||
|
||||
Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux
|
||||
(no runtime .so/.dylib — Skia is statically inside the klib), and the bitsycore
|
||||
fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next
|
||||
to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows
|
||||
one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`.
|
||||
|
||||
## Runtime bundling — data.kres
|
||||
|
||||
@@ -456,15 +481,15 @@ drift / vendor-clean guardrails are in
|
||||
## Key files by area — start here when you need to find something
|
||||
|
||||
### Renderer + main loop
|
||||
- `compose/sdl/window/src/nativeMain/…/ComposeWindow.kt` — main loop,
|
||||
- `compose/desktop/native/window/src/nativeMain/…/ComposeWindow.kt` — main loop,
|
||||
recomposer lifecycle, SDL event dispatch, composition-local seeding.
|
||||
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface.
|
||||
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed 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 rasterisation (SDL3_ttf has no axis-set API; we go
|
||||
to FreeType directly for icon families).
|
||||
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker
|
||||
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
|
||||
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` —
|
||||
the Skia render backend (shared by every native target).
|
||||
- `compose/ui/ui/src/skikoRendererMingwMain/…` — mingwX64-only Skia actuals
|
||||
bound against the skiko fork's flat extern-C surface.
|
||||
|
||||
### Layout / composition wiring
|
||||
- `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` — root LayoutNode
|
||||
@@ -474,15 +499,15 @@ drift / vendor-clean guardrails are in
|
||||
alpha bridge).
|
||||
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
|
||||
|
||||
### Text
|
||||
- `compose/ui/ui/src/nativeMain/…/ui/text/SdlParagraph.native.kt` — the
|
||||
bridged `Paragraph` implementation (measurement, hit-test, line metrics,
|
||||
span painting).
|
||||
- `compose/ui/ui/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
|
||||
### Text (all in `:ui-text` now)
|
||||
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` — the
|
||||
`Paragraph` actual (skiko-free; the 30 interface methods over plain data),
|
||||
driving the skiko engine through the `NativeParagraphOps` seam.
|
||||
- `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt`
|
||||
— `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts`
|
||||
(data.kres bytes → skiko Typeface; the port's family/variable-axis model).
|
||||
- `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
|
||||
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
|
||||
- `compose/ui/ui/src/commonMain/…/text/TextMeasurer.kt` — `NativeTextMeasurer`
|
||||
interface (per-renderer implementations: `SkiaTextRenderer` /
|
||||
`Sdl3TextRenderer`).
|
||||
|
||||
### Icons
|
||||
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` —
|
||||
@@ -516,9 +541,9 @@ profiler. Quick rules of thumb:
|
||||
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.
|
||||
- Any renderer change → the **`verify-mac`** runbook gates it before commit.
|
||||
|
||||
See [RENDERER.md](RENDERER.md) for the renderer work these support.
|
||||
Open renderer / fidelity / release work is tracked in [PLAN.md](PLAN.md).
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -565,12 +590,6 @@ that should surface in tooling.
|
||||
`Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`.
|
||||
For a drag ghost that also translates, put `alpha` and `translationX` on
|
||||
the SAME `graphicsLayer(...)` so clip stays false.
|
||||
- **Cinterop `depends = sdl3`** — silently doesn't propagate; see the
|
||||
gotcha section above.
|
||||
- **Configuration cache + `-Prenderer=`** — Gradle caches configuration;
|
||||
toggling the renderer property may not invalidate it. Delete
|
||||
`.gradle/configuration-cache/` between switches if you see weird
|
||||
"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
|
||||
@@ -583,17 +602,11 @@ that should surface in tooling.
|
||||
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** —
|
||||
the Skia renderer produces a `SkiaBackedPath` (wraps
|
||||
`org.jetbrains.skia.Path`), the SDL renderer produces a project
|
||||
`ProjectPath` (command-list based). `SkiaCanvas.toSkiaPath` handles
|
||||
both — never assume the type without checking.
|
||||
|
||||
## Useful Gradle tricks
|
||||
|
||||
- `--args="--gpu=sdl3.opengl --screen=Buttons"` — pass CLI to the demo.
|
||||
- `--args="--gpu=skia.opengl --screen=Buttons"` — pass CLI to the demo.
|
||||
- `--info` — see cinterop classpath + include paths actually used.
|
||||
- `--rerun-tasks` — force rebuild after toggling `-Prenderer=`.
|
||||
- After a module rename or IC-cache mismatch: nuke
|
||||
`demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native
|
||||
pins module IDs into its klib metadata; a stale cache surfaces as
|
||||
|
||||
@@ -0,0 +1,736 @@
|
||||
# ROAD TO 1.0.0
|
||||
|
||||
**Objective:** get ComposeNativeSDL3 **as close to upstream Compose Multiplatform as
|
||||
possible** — vendor every file we can instead of rewriting, stay performant, and look
|
||||
faithful (ideally pixel-identical across macOS / Linux / Windows). The port is already at
|
||||
99% upstream API coverage with maximal vendoring and no steady-state perf gap on
|
||||
macOS/Metal; the remaining 1.0.0 work is **cross-platform rendering fidelity** (Windows
|
||||
skiko-fork divergences), **completing native-actual stubs**, and the release mechanics
|
||||
(1.12.0-stable re-pin + WIN-SMOKE).
|
||||
|
||||
**Status / handoff:** the macOS/Linux-verifiable work is done and committed on `main` (see
|
||||
**"Landed toward 1.0.0"** below — don't redo it). The remaining blockers need a **Windows host**
|
||||
→ jump to **"▶ Continuing on Windows (mingwX64) — START HERE"**. This file is self-contained: it
|
||||
has the build/run commands, the exact confirm steps, the macOS baseline data to diff against, and
|
||||
per-task fixes. So on Windows you can say **"continue PLAN.md"** and have everything.
|
||||
|
||||
## Definition of done for 1.0.0
|
||||
|
||||
- [x] No text tofu on any platform — Windows tab fix **CONFIRMED on-device** (§1a, 2026-08-04:
|
||||
literal `\t`→space, no `.notdef`); CJK / color-emoji / control chars render clean too (§1c).
|
||||
- [x] Fork-vs-official divergence surface (FontMgr, gamma, ICU) **audited on native Windows**
|
||||
(2026-08-04): metrics == JVM (§1b), FontMgr fallback works (CJK + color emoji, §1c), ICU
|
||||
classification works (§1c). Net — the fork's Skia matches upstream; the only deltas are its
|
||||
thin extern-C bindings, handled in shared Kotlin (LineMetrics reconstruction). No fork-side
|
||||
fix needed for 1.0.
|
||||
- [~] Native-actual fidelity blockers closed: float pointer coords **DONE**, screen-reader
|
||||
no-op **DONE**, text context menu **RECONCILED — already works** via the legacy path
|
||||
(§2). Date/time localization is P1 polish (formatter already works).
|
||||
- [x] Vendor hygiene stays clean: zero drift (all 10 manual vendors match pin), zero
|
||||
commonMain rule-1 violations, zero vendored files touched this session — verified via
|
||||
`check-vendor-drift.py`.
|
||||
- [~] SDL static lib slimmed to the used subsystem surface (§3) — **DONE + verified on
|
||||
macOS/Metal**; other hosts pending WIN-SMOKE.
|
||||
- [ ] Refs re-pinned to Compose **1.12.0 stable**, JVM parity versions bumped, WIN-SMOKE
|
||||
fidelity pass green on a real Windows host.
|
||||
- [x] `CLAUDE.md` documentation map consistent with tree — line-1 typo fixed; the stale
|
||||
historical docs `RENDERER.md` + `SKIKO-MINGW-FEASIBILITY.md` removed (current renderer
|
||||
essentials inlined into CLAUDE.md, the rest is in git history); all dead doc links
|
||||
(TODO.md / RENDERER.md / SKIKO) removed or repointed to PLAN.md.
|
||||
|
||||
## Landed toward 1.0.0 (this pass — all build + run verified on macOS/Metal)
|
||||
|
||||
Everything below is committed; details + file refs are in the sections that follow.
|
||||
|
||||
1. **Tab tofu fix** (§1a P0) — normalize `\t`→space before shaping so the Windows fork
|
||||
can't render `.notdef`. Platform-independent, length-preserving.
|
||||
2. **HiDPI caret quantization fix** (§2 P0) — pointer/wheel coords carried as `Float`
|
||||
end-to-end instead of truncated to `Int`.
|
||||
3. **Screen-reader no-op** (§2 P0) — `LocalPlatformScreenReader` defaults to inactive
|
||||
instead of throwing.
|
||||
4. **SDL slimming** (§3) — 10 unused subsystems disabled in `build-all.py`; 6 now-dead
|
||||
macOS frameworks dropped from `sdl3.def`. Rebuilt; demo + apidemo link, demo runs.
|
||||
5. **Context menu reconciled** (§2 P0) — confirmed already working via the vendored legacy
|
||||
path; the 3 "NOP" seams are vestigial (disabled new API) — misleading TODOs corrected.
|
||||
6. **`CDN_TEXT_METRICS` diagnostic** (§1b) — env-gated line-metrics dump; macOS baseline
|
||||
captured, hypothesis sharpened (fork likely drops lineHeight leading).
|
||||
7. **Doc hygiene** (§5) — removed the stale historical `RENDERER.md` +
|
||||
`SKIKO-MINGW-FEASIBILITY.md` (renderer essentials folded into `CLAUDE.md`; rest in git
|
||||
history), fixed the `CLAUDE.md` typo + all dead doc links; extracted the fork
|
||||
FreeType-scaler lead into §1c before deleting. Vendor drift verified clean.
|
||||
|
||||
**Windows pass (2026-08-04, this host — DPR 1).** On a real Windows host at last: (8) tab fix
|
||||
**CONFIRMED on-device** (§1a); (9) vertical metrics **RESOLVED** — native `--metricsprobe` heights
|
||||
== JVM `--metrics` exactly, feared "dropped leading" DISPROVEN (§1b); (10) fork `LineMetrics.
|
||||
ascent/descent` mis-decode **FIXED in shared Kotlin** (`SkiaParagraphOps.lineMetrics()`, guarded
|
||||
no-op on official skiko) — repairs Windows caret/selection; (11) fork FontMgr/ICU **AUDITED** — CJK
|
||||
+ color-emoji + control-char fallback all work → the "empty FreeType stub" fear DISPROVEN (§1c);
|
||||
(12) `FontRasterizationSettings.native.kt` verified to mirror upstream VERBATIM + given a
|
||||
`VENDOR-BASE` provenance line; rasterization stays upstream-faithful per-OS (user decision). Net:
|
||||
the fork's Skia matches upstream — the only deltas are its thin extern-C bindings, fixed in shared
|
||||
Kotlin, **zero fork rebuild**.
|
||||
|
||||
**Still not done — WIN-SMOKE interactive + release** (§5): interactive caret-height check (the one
|
||||
behavioral consequence of the LineMetrics fix), the `PrintWindow` probe, `compileCommonMainKotlin
|
||||
Metadata`, apiDump + publish FROM Windows, and the Compose 1.12.0-stable re-pin. **Deliberately
|
||||
deferred** (large/structural, rationale in-section): RTL, `PlatformFontLoader`, grapheme source-set
|
||||
move, date CLDR, brush/gradient text — plus the big **vendor-the-upstream-skiko-text-engine
|
||||
refactor (§6)**, sequenced after these fixes land.
|
||||
|
||||
---
|
||||
|
||||
## ▶ Continuing on Windows (mingwX64) — START HERE
|
||||
|
||||
**Read this before doing anything.** This file is the single source of truth for the road to
|
||||
1.0.0. Everything in **"Landed toward 1.0.0"** above is **already implemented and committed on
|
||||
`main`** — do NOT redo it, just verify it on this host. Every open `[ ]` / `[~]` below is
|
||||
remaining work. The macOS/Linux side is done and verified; the remaining blockers need a **real
|
||||
Windows host** — the mingwX64 build cross-compiled from mac/Linux fails at cinterop, and the
|
||||
bitsycore skiko **fork** can only be rebuilt on Windows.
|
||||
|
||||
**The Windows fidelity reference is skiko-on-JVM Compose Desktop on THIS Windows host**
|
||||
(`gradlew :demo:run` — also DirectWrite/system fonts), NOT macOS. mac↔Windows metric deltas are
|
||||
expected in upstream Compose Desktop too, so "matches Windows-JVM" is the acceptance bar, not
|
||||
"matches macOS".
|
||||
|
||||
### Build / run (Windows shell, repo root)
|
||||
|
||||
```bat
|
||||
gradlew.bat :demo:runDebugExecutableMingwX64 :: native demo (the port under test)
|
||||
gradlew.bat :apidemo:runDebugExecutableMingwX64 :: native apidemo (has the Session/Override buttons)
|
||||
gradlew :demo:run :: JVM Compose Desktop = the FIDELITY REFERENCE
|
||||
gradlew :apidemo:run :: JVM apidemo reference
|
||||
```
|
||||
|
||||
- The bridge plugin auto-drops `skiko-windows-x64.dll` next to the exe (`installWindowsSkiaDll`).
|
||||
Fork coords `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override `-PskikoMingwVersion`).
|
||||
- `:demo` CLI flags (native AND jvm): `--screen=<Name>`, `--screenshot=<path.bmp>` (capture at
|
||||
quiescence then quit), `--gpu=auto|software|skia.opengl`. `CDN_TEXT_METRICS=1` env var dumps
|
||||
text metrics (§1b). `CDN_PROFILE=1` frame profiler. `:apidemo` has NO screenshot CLI — drive it
|
||||
by hand for the button-metrics visual.
|
||||
- If an IC-cache error appears after any module churn: delete `demo\build\kotlin-native-ic-cache`.
|
||||
|
||||
### ✅ Windows session results (2026-08-04, this host — DPR 1.0)
|
||||
|
||||
Items 1–3 below are **DONE + verified on the shipped mingwX64 demo binary**; findings folded into
|
||||
§1a/§1b/§1c. Only 4–5 (WIN-SMOKE interactive gate + apiDump/publish) remain. TL;DR:
|
||||
|
||||
- **§1a tab fix — CONFIRMED.** `--screen=BasicText` with `"start>a\tb<end\tcols\tand\tthere"`
|
||||
rendered `start>a b<end cols and there` — every `\t` a SPACE, zero `.notdef`. (Structural proof
|
||||
too: the whole module has exactly 2 `.addText(` sites, both `shapedText`.)
|
||||
- **§1b vertical metrics — RESOLVED, the feared bug is DISPROVEN.** Native `--metricsprobe` vs JVM
|
||||
`--metrics` (same host, NotoSans, density 1): **every paragraph height + non-M3 baseline matches
|
||||
JVM EXACTLY** (size 11→24). Leading is NOT dropped. Two residual deltas found (details in §1b):
|
||||
(a) the fork's `LineMetrics.ascent/descent` are mis-decoded — **fixed in Kotlin** this session;
|
||||
(b) M3 baseline drifts ≤0.77px (halfLeading, a shared-engine omission on ALL platforms, sub-pixel).
|
||||
- **§1c FontMgr/ICU — the "empty FreeType stub" fear is DISPROVEN.** Native render of CJK
|
||||
(你好世界 日本語 한국어) + **full-COLOR emoji** (😀🎉🚀❤) + control chars (nbsp/zwsp/en-dash) = zero
|
||||
tofu. Color emoji + CJK fallback prove `FontMgr.default` is a real DirectWrite-backed system
|
||||
manager, not an empty custom FreeType one. Remaining §1c work is doc-only (capture `args.gn`).
|
||||
|
||||
Reference commands actually used (both probes print `metrics:` lines; native runs a real window):
|
||||
```bat
|
||||
demo\build\bin\mingwX64\debugExecutable\demo.exe --metricsprobe :: native/fork metrics
|
||||
gradlew :demo:run --args="--metrics" :: JVM/upstream metrics
|
||||
set CDN_TEXT_METRICS=1 && ...demo.exe --screen=Buttons :: raw per-paragraph dump
|
||||
```
|
||||
(The JVM leg's screenshot flag is `--screenshot-all=<dir>`, not the native `--screenshot=<file>`.)
|
||||
|
||||
### Ordered Windows work (do top-to-bottom; full detail in the numbered sections below)
|
||||
|
||||
1. **[§1a] Confirm the tab fix — ✅ DONE** (see session results above). Rendered a literal-tab
|
||||
string on the fork; all `\t`→space, no tofu. The `SkiaParagraphEngine.shapedText` normalization
|
||||
holds; no bypass path exists (2 `.addText(` sites, both `shapedText`).
|
||||
|
||||
2. **[§1b] Confirm/resolve vertical metrics — ✅ RESOLVED (feared bug DISPROVEN).** Used the
|
||||
`--metricsprobe` (native) vs `--metrics` (JVM) pair, NOT the screenshot diff (the JVM leg has no
|
||||
`--screenshot=<file>`; it dumps the same `metrics:` table). Every paragraph HEIGHT matches JVM
|
||||
exactly (leading applied identically) → correct-by-reference. Only a ≤0.77px M3-baseline drift
|
||||
remains (halfLeading, all platforms). Separately fixed the fork's broken `LineMetrics.ascent/
|
||||
descent` in Kotlin. Full data in §1b.
|
||||
|
||||
3. **[§1c] Audit the fork FontMgr / gamma / ICU — ✅ AUDITED (fallback WORKS).** CJK + color emoji +
|
||||
control chars all render with zero tofu on the fork → `FontMgr.default` is a real system
|
||||
(DirectWrite) manager with working glyph fallback, NOT an empty FreeType stub. Remaining: capture
|
||||
the fork's REAL `args.gn` (`SK_GAMMA_*`, ICU packaging, freetype-vs-dwrite) into the fork repo so
|
||||
drift is auditable (doc-only, not a code blocker).
|
||||
|
||||
4. **[§5] WIN-SMOKE gate** (pre-ship): (1) the §1b metrics dump, (2) tab/control-char render clean,
|
||||
(3) the Windows-only `PrintWindow` probe (`scripts/probe/`), (4) `gradlew
|
||||
:<module>:compileCommonMainKotlinMetadata` (only the Windows job compiles common metadata).
|
||||
|
||||
5. **[§5] apiDump + publish FROM Windows** (host-specific; only the Windows job carries the full
|
||||
mingwX64 variant table). Then, when Compose **1.12.0 stable** ships, do the re-pin + version
|
||||
bump. Release runbook: [TOOLING.md](TOOLING.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Cross-platform FIDELITY (headline)
|
||||
|
||||
All Windows divergences live **below Kotlin** — the mingw source set reuses
|
||||
`src/skikoRendererMain/kotlin` byte-for-byte (`compose/ui/ui/build.gradle.kts:102-113`),
|
||||
the only mingw-unique file is `PlatformGpu.mingw.kt` (13 lines, GPU only). So every
|
||||
divergence is in the **fork's Skia binary**, in **`FontMgr.default`'s per-OS impl**, or in
|
||||
the **GPU backend**. The fork is external (`bitsycore/skiko`,
|
||||
`com.bitsycore.skiko:skiko:0.150.1-mingw.1`) and its GN args are **not verifiable from this
|
||||
tree** — recovering them is a prerequisite for several fixes below.
|
||||
|
||||
**Fidelity reference for Windows is skiko-on-JVM Compose Desktop on Windows** (also
|
||||
DirectWrite), NOT macOS/Linux. macOS↔Windows metric deltas partly exist in upstream
|
||||
Compose Desktop too (per-platform font backends). Reframe V-items as "match JVM-Windows"
|
||||
acceptance tests, not "bugs vs macOS".
|
||||
|
||||
### 1a. Windows tab-character tofu — HIGH CONFIDENCE (root cause traced)
|
||||
|
||||
Kotlin sets `ParagraphStyle.replaceTabCharacters = true`
|
||||
(`compose/ui/ui-text/src/skikoRendererMain/kotlin/.../renderer/skia/SkiaParagraphEngine.kt:234-236`,
|
||||
exact mirror of upstream `ParagraphBuilder.skiko.kt:625`, CMP-6589). Works on
|
||||
macOS/Linux (official skiko honors the flag → `U+0009` replaced by space before shaping).
|
||||
On Windows the same call runs against the fork's flat extern-C DLL, which evidently does
|
||||
**not** wire the setter → raw `\t` reaches HarfBuzz → NotoSans has no tab glyph → `.notdef`
|
||||
box. Font fallback is a **red herring**: no font has a tab glyph, only tab→space
|
||||
replacement cures it.
|
||||
|
||||
- [x] **P0** Confirm — ✅ DONE (Windows 2026-08-04): rendered a literal-tab string on the fork
|
||||
(`--screen=BasicText`); every `\t` shows a SPACE, no `.notdef`. The fork renders everything
|
||||
else clean too, so tab→space was the only gap. Structural backstop: the module has exactly 2
|
||||
`.addText(` sites, both `shapedText` — no raw tab can reach skiko.
|
||||
- [x] **P0** Fix (ship now, author project code): in
|
||||
`.../SkiaParagraphEngine.kt`, feed skiko a normalized copy — added
|
||||
`private val shapedText = if (text.indexOf('\t') >= 0) text.replace('\t', ' ') else text`,
|
||||
used at `addText` (no-span path) and via `shapedText.substring(segStart, segEnd)`
|
||||
(span path); `text` kept for ALL length/index queries (`getRectsForRange`,
|
||||
`getCursorRect`, `wordBoundary`, `lineMetrics`, span cut-points). Length-preserving so
|
||||
every offset consumer stays correct; this is exactly what Skia's flag does internally →
|
||||
upstream-faithful; makes behavior platform-independent (no fork rebuild needed).
|
||||
Kept `replaceTabCharacters = true` (harmless/redundant elsewhere).
|
||||
**DONE** — compiles clean on macosArm64. Windows tofu render still needs on-device
|
||||
confirmation (WIN-SMOKE §5).
|
||||
- [ ] **P2** Long-term true fix (fork, out of tree): wire
|
||||
`ParagraphStyle::setReplaceTabCharacters` through the fork's extern-C surface,
|
||||
rebuild/republish `skiko-windows-x64.dll`, bump `-PskikoMingwVersion`. Do this *in
|
||||
addition to* the Kotlin fix, not instead — it keeps the port thin. Can't be verified
|
||||
in-tree.
|
||||
|
||||
### 1b. macOS-vs-Windows vertical text metrics — ✅ RESOLVED (Windows 2026-08-04): heights match JVM, feared bug DISPROVEN
|
||||
|
||||
**WINDOWS RESULT (2026-08-04).** All targets render through the SAME Skia, so metrics are expected
|
||||
to be similar — and they are. Native `--metricsprobe` (fork) vs JVM `--metrics` (upstream skiko) on
|
||||
this host (NotoSans, density 1) print IDENTICAL paragraph heights and non-M3 baselines for every
|
||||
size 11→24 (`cell`/`one`/`three`/`oneM3`/`threeM3`, `base1`/`base3` all match). The fork does NOT
|
||||
drop lineHeight leading — the hypothesis below is disproven. Two small deltas remained, both
|
||||
handled in SHARED Kotlin (no fork rebuild, keeping fork-reliance minimal):
|
||||
|
||||
1. **Fork `LineMetrics.ascent/descent` mis-decode → FIXED in Kotlin (this session).**
|
||||
`CDN_TEXT_METRICS` showed the fork's per-line `ascent`/`descent` constant (18.837/5.163)
|
||||
regardless of `fontPx`, while `baseline`/`height`/`width` are correct (mac scales them:
|
||||
37.674@30px). This is the fork's flat extern-C LineMetrics binding, NOT a Skia difference.
|
||||
Lines stack contiguously so true ascent = `baseline - lineTop`; `SkiaParagraphOps.lineMetrics()`
|
||||
now rebuilds ascent/descent from the reliable baseline+height when they're internally
|
||||
inconsistent (guard `|ascent-(baseline-lineTop)|>0.5` = no-op on official skiko). Repairs caret
|
||||
height / getLineTop/Bottom / vertical hit-test on Windows. **Behavioral caret check → WIN-SMOKE.**
|
||||
2. **M3 baseline drift ≤0.77px (halfLeading) — SHARED-engine omission, all platforms.** `base1M3`
|
||||
differs native-vs-JVM by ≤0.77px in the 11→24 sweep (size 14 → 0.28px, sub-pixel), 2.19px in
|
||||
the contrived `boundary 24/25` case. Root cause is NOT the fork: `SkiaParagraphEngine` maps
|
||||
`LineHeightStyle.trim`→HeightMode but never wires `halfLeading`/`LineHeightStyle.Alignment`, so
|
||||
macOS-native drifts from JVM by the same amount (parity tolerated it as sub-pixel). Fixable in
|
||||
shared Kotlin (`textStyle.halfLeading` from the alignment); deferred pending a cross-platform
|
||||
parity re-check (can't run mac parity from a Windows host).
|
||||
|
||||
Native-fork metricsprobe vs JVM `--metrics` (this host) — identical except `base?M3`:
|
||||
```
|
||||
size=14 lh=20 cell=19 one=19 three=59 base1=14.898 base3=54.898 oneM3=20 threeM3=60 base1M3: 15.682 fork / 15.398 jvm
|
||||
size=24 lh=30 cell=33 one=33 three=93 base1=25.968 base3=85.968 oneM3=30 threeM3=90 base1M3: 23.546 fork / 24.312 jvm
|
||||
boundary 24/25 m3=25 base: 19.622 fork / 21.812 jvm
|
||||
```
|
||||
|
||||
The vertical-layout code path is **identical** on all targets. The lineHeight/HeightMode
|
||||
path is a byte-faithful port of upstream and is NOT the cause: m3 `labelLarge` has
|
||||
lineHeight 20sp > fontSize 14sp with `LineHeightStyle.Default.trim == Both` →
|
||||
`HeightMode.DISABLE_ALL`, which collapses the single-line box to `fontAscent + fontDescent`
|
||||
of the font itself (`SkiaParagraphEngine.kt:244-249`, matches upstream
|
||||
`ParagraphBuilder.skiko.kt:631-646`). So box height is driven entirely by the font's
|
||||
reported ascent/descent.
|
||||
|
||||
Root cause: **`FontMgr.default` resolves to a different Skia font scaler per build** for
|
||||
the *same* `font/NotoSans.ttf` bytes (`SkiaFonts.kt:38,44,50`) — CoreText on macOS,
|
||||
fontconfig/FreeType on Linux, DirectWrite (or FreeType, unverified) in the fork on Windows.
|
||||
Different backends select different metric tables (hhea `ascender/descender` vs OS/2
|
||||
`sTypoAscender/Descender` vs `usWinAscent/Descent`) and honor the OS/2 `USE_TYPO_METRICS`
|
||||
fs_selection bit differently. NotoSans is the textbook case (large top-heavy ascent).
|
||||
`SkiaParagraphEngine.kt:147-148` read `-defaultFont.metrics.ascent`/`.descent`;
|
||||
`:150-169 lineMetrics()` forwards skiko's numbers verbatim; `SkiaParagraph.native.kt:116,148-153`
|
||||
consume them. Contributors V2 (hinting Normal on Windows grid-fits vertically, CoreText
|
||||
ignores it) and V3 (subpixel/edging) are secondary and per-scaler.
|
||||
|
||||
**Hypothesis, not confirmed:** whether Windows-native actually *diverges from
|
||||
Windows-JVM*. If it matches JVM-Windows it's correct-by-reference (not a bug), and the
|
||||
macOS↔Windows delta is expected upstream behavior.
|
||||
|
||||
- [x] **P0** Confirm FIRST — ✅ RESOLVED (Windows 2026-08-04; heights == JVM, see WINDOWS RESULT
|
||||
above). Diagnostic **LANDED**: `SkiaParagraphEngine.kt` dumps
|
||||
`defaultFont.metrics.{ascent,descent,leading}`, `paragraph.height`,
|
||||
`lineMetrics[0].{ascent,descent,baseline,height}`, `familyName` + `fontPx` per built
|
||||
paragraph when `CDN_TEXT_METRICS=1` (mirrors the `CDN_PROFILE` env-flag convention).
|
||||
Run `CDN_TEXT_METRICS=1 <app> --screen=Buttons` (native) and the JVM parity app on the
|
||||
SAME host, diff the lines. macOS baseline captured (see below). **REMAINING (Windows
|
||||
host):** compare Windows-native vs **skiko-JVM-Windows**. Expectation: `fontPx` +
|
||||
lineHeight + heightMode identical, `metrics.ascent` differs by scaler.
|
||||
**Acceptance test:** Windows-native == skiko-JVM-Windows (→ correct-by-reference, not a
|
||||
bug); if they differ, it's a fork scaler fix (next item).
|
||||
|
||||
**macOS baseline captured (`--screen=Buttons`, DPR 2):** for the fontPx=30 button
|
||||
labels ("Filled Button" / "OutlinedButton" / "TextButton"): font metrics
|
||||
`ascent=-32.07 descent=8.79 leading=0` (font box = **40.86px**), but the line box is
|
||||
`line0[ascent=37.67 descent=10.33 height=48.0]`, `paraHeight=48`. So macOS DOES apply
|
||||
the lineHeight leading (48 > 40.86) and distributes it **asymmetrically — ~5.6px above,
|
||||
~1.5px below** → exactly the "extra space on top of the text" the report describes.
|
||||
**SHARPENED HYPOTHESIS:** this is NOT a mac bug — it's correct lineHeight distribution.
|
||||
The likely Windows story is the MIRROR of §1a: the fork's flat extern-C surface doesn't
|
||||
wire the `ParagraphStyle` height/strut path, so Windows renders **tight to the font box
|
||||
(~41px, no leading)** = the "fitted to text" look. **Sharpened WIN-SMOKE check:** for the
|
||||
SAME label, compare `paraHeight` + `line0.height`. If macOS/JVM = 48 but Windows-native
|
||||
≈ 41, the fork drops lineHeight leading (fork fix: wire `heightMode`/strut, like
|
||||
`replaceTabCharacters`); a Kotlin fallback is a manual `StrutStyle`. If Windows-native
|
||||
== Windows-JVM, it's correct-by-reference.
|
||||
|
||||
**macOS baseline raw lines** (`CDN_TEXT_METRICS=1 demo --screen=Buttons`, DPR 2) — diff
|
||||
Windows-native + Windows-JVM against these; the fontPx=30 rows are the cleanest signal:
|
||||
```
|
||||
text='Filled Button' fontPx=30.0 ascent=-32.070007 descent=8.789978 leading=0.0 paraHeight=48.0 line0[ascent=37.674 descent=10.326 baseline=37.674 height=48.0]
|
||||
text='OutlinedButton' fontPx=30.0 ascent=-32.070007 descent=8.789978 leading=0.0 paraHeight=48.0 line0[ascent=37.674 descent=10.326 baseline=37.674 height=48.0]
|
||||
text='TextButton' fontPx=30.0 ascent=-32.070007 descent=8.789978 leading=0.0 paraHeight=48.0 line0[ascent=37.674 descent=10.326 baseline=37.674 height=48.0]
|
||||
text='Outlined' fontPx=28.0 ascent=-29.932007 descent=8.203980 leading=0.0 paraHeight=40.0 line0[ascent=31.395 descent=8.605 baseline=31.364 height=40.0]
|
||||
```
|
||||
(family='Noto Sans' on all.) Expectation if correct: `fontPx`, `paraHeight`, `line0.height`
|
||||
match JVM-Windows exactly; the raw `ascent`/`descent` may differ from these macOS numbers
|
||||
because the Windows scaler is FreeType (§1c), but the LINE BOX must still match JVM-Windows.
|
||||
- [x] **P1** ~~If Windows-native != Windows-JVM: fix in the fork~~ — **N/A (2026-08-04):** heights
|
||||
MATCH JVM-Windows exactly, so no fork scaler/FontMgr change is needed for vertical metrics.
|
||||
The `makeFromData` typefaces already get the same metric-table selection as skiko-JVM-Windows.
|
||||
The one residual (M3 halfLeading) is a shared-Kotlin fix, not a fork one.
|
||||
- [ ] **P2** (Optional, STRONGER than upstream) If cross-platform pixel-identity is
|
||||
wanted over matching each host's stock Compose Desktop: build a single
|
||||
FreeType/`SkFontMgr_Custom_Empty`-backed `SkFontMgr` used on ALL native targets for
|
||||
the bundled fonts, so NotoSans yields identical ascent/descent everywhere.
|
||||
Intentional departure from per-platform upstream behavior — decide deliberately.
|
||||
- [ ] **DO NOT** "fix" by injecting a default `LineHeightStyle` or clamping ascent in
|
||||
`SkiaParagraphEngine.kt` — that desyncs from upstream `DISABLE_ALL` and masks, not
|
||||
resolves, the metric divergence.
|
||||
|
||||
### 1c. Other fork-vs-official divergences
|
||||
|
||||
- [x] **P1** ~~`FontMgr.default` may be an empty/stub manager on the fork~~ — **DISPROVEN
|
||||
(Windows 2026-08-04).** Native render of CJK (你好世界 日本語 한국어) + **full-COLOR emoji**
|
||||
(😀🎉🚀❤) + control chars = zero tofu. Color emoji + CJK fallback are impossible with an empty
|
||||
custom FreeType manager, so the fork's `FontMgr.default` IS a real DirectWrite-backed system
|
||||
manager with working glyph enumeration/fallback. No fork change needed. `SkiaFonts.kt` wiring
|
||||
stays as-is.
|
||||
- [x] **P1** `FontMgrWithFallback` — ANALYZED + fallback now VERIFIED on Windows. The port
|
||||
does `setDefaultFontManager(fontMgr)` [system] + `setAssetFontManager(provider)`
|
||||
[bundled] (`SkiaFonts.kt:49-52`). This is **functionally equivalent** for missing-glyph
|
||||
fallback on macOS/Linux/Windows-JVM: skiko's shaper tries the asset provider (bundled
|
||||
Noto + aliases) first, then the default/system manager for CJK/emoji fallback, which
|
||||
resolves because the system FontMgr has those fonts. Rewriting to
|
||||
`setDefaultFontManager(FontMgrWithFallback(provider))` risks a macOS fallback
|
||||
regression with no observable gain, and I can't visually verify CJK/emoji fallback on
|
||||
this host. **The real gap was thought to be Windows-fork-only** (fork `FontMgr.default`
|
||||
empty stub → no system fallback) — but that's **DISPROVEN (2026-08-04):** CJK + color-emoji
|
||||
fallback works on native Windows too (item above). So the setDefault/setAsset wiring is fine
|
||||
on ALL targets. **Decision:** leave the Kotlin as-is; nothing to fix in the fork here. Re-open
|
||||
only if a CJK/emoji fallback bug is actually observed.
|
||||
- [x] **P2** ~~ICU/unicode data packaged differently in the fork~~ — **VERIFIED OK (2026-08-04).**
|
||||
Rendered U+00A0 (nbsp → visible space), U+200B (zwsp → zero-width/collapsed), en-dash, and
|
||||
U+0009 (tab, pre-replacement) on native Windows: all classified/handled correctly, no tofu.
|
||||
The force-exported `uloc_*_skiko` symbols do their job; skunicode classification works. (This
|
||||
is also why HarfBuzz shaping + skparagraph bidi work at all — ICU data is present via
|
||||
icudtl.dat.)
|
||||
- [~] **P2** Text gamma/AA edges: a Skia **build-time constant**
|
||||
(`SK_GAMMA_EXPONENT`/`SK_GAMMA_CONTRAST`/`SK_GAMMA_APPLY_TO_A8`), NOT a GL-vs-Metal
|
||||
runtime difference. **Note (2026-08-04):** native Windows text AA looked clean in the render
|
||||
(no visible AA anomaly), so no fork gamma fix is pursued for 1.0. Still worth capturing the
|
||||
fork's values in the fork repo (see the args.gn item below). **Kotlin-side rasterization
|
||||
settled:** the `RASTER_EDGING/HINTING/SUBPIXEL` in `SkiaParagraphEngine` come from
|
||||
`FontRasterizationSettings.PlatformDefault`, which was verified (2026-08-04) to reproduce
|
||||
upstream Compose Desktop's per-OS defaults VERBATIM (Win/mac=Normal hinting, Linux=Slight).
|
||||
**User decision:** keep the upstream-faithful per-OS behavior (match each platform's stock
|
||||
CMP), NOT a forced uniform cross-platform look. So there is no SDL_TTF-era gamma hack left to
|
||||
strip — Skia is configured exactly as upstream does it.
|
||||
- [x] **GL-vs-Metal AA/gamma/color-space — RULED OUT as a primary cause.** Both bridges
|
||||
create the surface `colorSpace = null` (`SkiaGLBridge.kt:68-74`,
|
||||
`SkiaMetalBridge.kt:133-139`) — identical un-color-managed legacy blending. Only
|
||||
deltas are cosmetic-correct: `RGBA_8888`/`BOTTOM_LEFT` (GL) vs `BGRA_8888`/`TOP_LEFT`
|
||||
(Metal) — channel order + Y-flip, matched to buffers. Windows uses OpenGL
|
||||
(`PlatformGpu.kt:20`), same backend as Linux, so GL-vs-Metal can't explain a
|
||||
Windows-vs-Linux gap at all. Deprioritized — no action.
|
||||
- [~] **P1** The fork's Skia build config (GN args). **Lead extracted from git history**
|
||||
(the old `SKIKO-MINGW-FEASIBILITY.md`, deleted as stale — its build recipe lives in
|
||||
`git show bdb5c64d^:SKIKO-MINGW-FEASIBILITY.md`): the fork's Route-1a recipe builds Skia
|
||||
with **`skia_use_freetype`**, i.e. the Windows text scaler is **FreeType**, not
|
||||
DirectWrite and not macOS CoreText — a concrete reason the SAME NotoSans bytes yield
|
||||
different ascent/descent on Windows (different metric-table selection) AND why fork
|
||||
`FontMgr.default` may not enumerate Windows system fonts for glyph fallback (FreeType
|
||||
has no system fontmgr without fontconfig). **CAVEAT:** that's the *recommended recipe*,
|
||||
not a verified dump of the shipped `0.150.1-mingw.1` GN args. **REMAINING:** whoever
|
||||
rebuilds the fork should capture the real `args.gn` (`SK_GAMMA_*`, ICU packaging,
|
||||
freetype-vs-dwrite) alongside the fork sources so drift is auditable — NOT as a doc in
|
||||
this repo (kept lean); a comment in the fork repo or `build-sdl.properties`-style pin.
|
||||
|
||||
---
|
||||
|
||||
## 2. VENDORING & upstream-fidelity debt
|
||||
|
||||
Vendor hygiene is **release-ready**: `check-vendor-drift.py` reports all 10 manual vendors
|
||||
match pin `v1.12.0-beta03+dev4483`; zero commonMain rule-1 violations (the 3 authored
|
||||
`androidx.compose.*` files in commonMain are all provenance-tracked Rule-3 vendors). The
|
||||
macosMain DIAGNOSTIC GAP families (`ui/ui` 12, `foundation` 23: `CoreTextField.macos.kt`,
|
||||
`SelectionManager.macos.kt`, `PlatformClipboard.macos.kt`, …) are **correctly excluded**
|
||||
(AppKit/NSView; the port uses SDL) — keep them in gaps. **Do not spend 1.0.0 effort
|
||||
re-vendoring.** The debt is **completing native-actual stubs**.
|
||||
|
||||
### P0 — blocks fidelity/correctness
|
||||
|
||||
- [~] **P0** Text context menu (right-click copy/paste/select-all) — **RECONCILED: already
|
||||
works, no reimplementation.** Static trace confirmed the menu is wired end-to-end
|
||||
through the vendored LEGACY path: text-level `ContextMenuArea` (`ContextMenu.native.kt`)
|
||||
→ `CommonContextMenuArea` (`vendor/common/.../text/CommonContextMenuArea.kt`) → (native
|
||||
`ComposeFoundationFlags.isNewContextMenuEnabled = false`, so the legacy branch) →
|
||||
`contextmenu.ContextMenuArea` → `contextMenuGestures`/`onRightClickDown`
|
||||
(`isSecondaryPressed`) → `ContextMenuPopup` → `Popup.native.kt` (hosted by
|
||||
`LocalPopupHost` at the app root). Items are real: `TextFieldSelectionManager` /
|
||||
`SelectionManager.contextMenuBuilder` emit Cut/Copy/Paste/SelectAll with real actions.
|
||||
SDL right-click → `PointerButton.Secondary` (`SDL3EventMapper.kt`) →
|
||||
`isSecondaryPressed` (`PointerEventBridge`). The three "NOP" seams belong to the
|
||||
DISABLED *new* context-menu API and are unreachable — **DONE:** their misleading
|
||||
`TODO(CMP-7819)` comments corrected to say so, in all three files.
|
||||
**REMAINING:** an interactive right-click smoke check (no headless driver here — fold
|
||||
into manual/WIN-SMOKE). Known limitation, not a breakage: Paste enablement is
|
||||
plain-text-only (`ClipboardPasteState.hasClip = hasText`, see P2 below).
|
||||
- [ ] **P1** DatePicker/TimePicker localization — VERIFIED current state:
|
||||
`material3/.../internal/PlatformDateFormat.native.kt` is a real kotlinx-datetime
|
||||
formatter that DOES honor CLDR patterns/skeletons (renders "Jul 29, 2026" /
|
||||
"July 2026" correctly — the agent's "always yyyy-MM-dd" finding was stale). Genuine
|
||||
remaining gaps, all narrower than a correctness blocker: field NAMES English-only
|
||||
(`:29 weekdayNames`, `:147-154 MONTH_NAMES/ABBR` — no CLDR data bundled), `:27
|
||||
firstDayOfWeek=1` fixed Sunday, `:67 is24HourFormat()=true` fixed, `:46 parse()`
|
||||
ISO-8601-only. Upstream `darwinMain` uses `NSDateFormatter` (not portable). **Fix
|
||||
(author):** bundle a CLDR subset or K/N i18n lib for localized names + locale-aware
|
||||
first-day/24h; also unblocks `CalendarLocale.native.kt:20` (fixed `"en"`). Ships
|
||||
readable English dates today — polish, not a P0 gate.
|
||||
- [x] **P0** Float pointer coordinates — `SDL3EventMapper.kt` `.toInt()`-truncated SDL's
|
||||
Float `mb.x`/`mb.y`/`mm.x`/`mm.y` and the wheel `mouse_x/y` before the DPR multiply in
|
||||
`ComposeWindow.kt`. On 2× displays a click at logical 100.9 → physical 200 not ~201,
|
||||
quantizing the caret to 2px steps near glyph edges. **DONE:** widened
|
||||
`LegacyPointerEvent.x/y` and `AppEvent.MouseWheel.x/y` to `Float`, dropped the
|
||||
`.toInt()` at the three mapper construction sites, and removed the now-redundant
|
||||
`.toFloat()` at the two `ComposeWindow` read sites. Consumers fully contained (verified
|
||||
by grep). Builds + runs clean on macosArm64.
|
||||
|
||||
### P1 — quality / parity
|
||||
|
||||
- [ ] **P1** RTL text unsupported — `ui-text/.../SkiaParagraph.native.kt:112`
|
||||
`textDirection = ResolvedTextDirection.Ltr` fixed field; `getParagraphDirection()`
|
||||
(`:202`) always Ltr. Bidi run direction (`:204`) does read box direction (partial
|
||||
machinery). Root cause: `text/intl/Locale.native.kt isRtl()` unimplemented. Fix
|
||||
(author) tied to P1.2 engine.
|
||||
- [ ] **P1** `PlatformFontLoader` NOP — `ui-text/.../font/FontFamilyResolver.native.kt`
|
||||
`SdlPlatformFontLoader.loadBlocking`/`awaitLoad` NOP;
|
||||
`PlatformFontFamilyTypefaceAdapter.resolve` returns `Immutable(Unit)`. Raw androidx
|
||||
`Font(bytes)`/`ResourceFont` don't load (compose-resources `Font()` works). Upstream
|
||||
`SkiaFontLoader.skiko.kt` is the vendorable reference. This is the long-deferred P3.2
|
||||
font resolver — **high-risk L**, parity harness wouldn't exercise it. Scope
|
||||
deliberately for 1.0.0 (accept-and-document vs attempt). **NOW ALSO GATES §6** — the
|
||||
2026-08-04 spike found the verbatim engine-vendor routes font resolution through this NOP, so
|
||||
implementing this loader (so `SkiaFonts`' icon/variable-axis model rides upstream's seam) and
|
||||
the §6 engine-vendor are ONE effort. Do them together, with macOS/Linux verification.
|
||||
- [ ] **P1** `CharHelpers.skiko.kt` grapheme-break logic — the ONE cheap selective-vendor
|
||||
win from the hand-rolled text engine. Fixes `findPrecedingBreak`/`findFollowingBreak`
|
||||
splitting emoji/combining marks in `StringHelpers.native.kt:31` +
|
||||
`CharHelpers.native.kt:14`. (Giving `:foundation` a `skikoRenderer` source set would
|
||||
unblock this + `DragAndDropSource.skiko` — L-effort structural move, per §13a.)
|
||||
- [~] **P1** `Serif`/`Cursive` generic families — RE-SCOPED after reading `SkiaFonts.kt`.
|
||||
They do NOT silently collapse to sans: `baseTypeface()` routes `generic:serif` /
|
||||
`generic:cursive` through `resolveGeneric()` → `fontMgr.matchFamilyStyle()` over a
|
||||
per-OS candidate list (`genericFamilyAliases`: Times/Noto Serif/… on each host), only
|
||||
falling back to the bundled default if NONE is installed. So serif/cursive already
|
||||
render as a real serif/cursive wherever the OS ships one (all three target OSes do).
|
||||
The only true gap is **cross-platform pixel-identity** / a host with no serif installed,
|
||||
which needs a BUNDLED Noto Serif — an app-level opt-in (buildSrc `downloadNotoFonts` +
|
||||
data.kres size cost), not a library correctness bug. **Decision:** leave library
|
||||
resolution as-is; offer bundled-serif as an opt-in later if an app needs it.
|
||||
- [x] **P1** Document the hand-rolled text engine as an **accepted architectural
|
||||
deviation** — **DONE:** rationale header added to the top of
|
||||
`ui-text/compose-fork.txt` (the exact place someone would go to "fill" the gaps),
|
||||
explaining the reduced local engine, the flat-source-set reason, and that RTL /
|
||||
stroke-DrawStyle / grapheme reductions are tracked here (§2), not fixed by vendoring.
|
||||
Also reflected in `SkiaParagraphEngine.kt`'s header and CLAUDE.md's renderer summary.
|
||||
|
||||
### P1/P2 — reduced-coverage + accessibility
|
||||
|
||||
- [ ] **P1** Text paint path handles only `SolidColor`
|
||||
(`SkiaParagraph.native.kt:253-256`) — no brush/gradient text fill, `drawStyle`
|
||||
(stroke), or non-`SrcOver` blendMode. Part of the reduced style coverage; extend on
|
||||
the local engine.
|
||||
- [ ] **P1** `SkiaParagraphEngine.kt:150-164` span segmentation drops partial `SpanStyle`
|
||||
overlaps (keeps only fully-covering `start<=segStart && end>=segEnd`). Documented
|
||||
divergence (`b63-upstream-text-mingw.md`) — widen to true interval segmentation.
|
||||
- [~] **P1** Accessibility absent — `ComposeOwner.kt:~340` builds a `semanticsOwner` never
|
||||
traversed to any OS a11y API; `SemanticsRegion.native.kt intersect()/difference()`
|
||||
hardcode `false`. Not vendorable (NSAccessibility/UIA/AT-SPI). **Decision for 1.0.0:
|
||||
out of scope for desktop.** The full a11y pipeline stays P2/out.
|
||||
- [x] **P0** No-op `PlatformScreenReader` — `CompositionLocals.native.kt`
|
||||
`LocalPlatformScreenReader` default no longer throws (`error(...)`); it returns an
|
||||
`InactivePlatformScreenReader` (`isActive = false`) so a11y-gated vendored code
|
||||
degrades to "no reader present" instead of crashing. **DONE** — builds + runs clean.
|
||||
- [ ] **P2** `SkiaFonts.kt` fidelity: `resolveCache` unbounded (font-cache half of P3.4
|
||||
deferred); variation key passes `null` density (`:65,78`) so density-dependent `opsz`
|
||||
in sp resolves wrong. `NamedFont.equals/hashCode` axis identity reportedly fixed —
|
||||
re-verify.
|
||||
|
||||
### P2 — cosmetic / edge
|
||||
|
||||
- [ ] **P2** `BlendMode.Multiply` renders wrong — opaque cyan×yellow reads back blue;
|
||||
likely Metal premultiply in graphics-layer flatten, not the `toSkia()` map. Isolate on
|
||||
the Skia draw path.
|
||||
- [ ] **P2** Drag-OUT of window NOP — `ui/.../draganddrop/Sdl3DragAndDropOwner.kt:39`
|
||||
`requestDragAndDropTransfer`; `DragAndDropSource.native.kt:36` no drag-shadow. SDL3
|
||||
has no portable start-drag → needs NSDraggingSession/DoDragDrop/XDND per-OS.
|
||||
Drop-INTO works. Accepted 1.0 gap (SDL platform limit) — document.
|
||||
- [ ] **P2** `NativeStringDelegate.native.kt:17` `toUpper/toLowerCase` ignore `locale`
|
||||
(Turkish i). Upstream `darwin` unvendorable (NSString).
|
||||
- [ ] **P2** Prefetch scheduler NOP — `platform/PrefetchLocals.native.kt:18`, lazy lists
|
||||
skip ahead-of-time composition (scroll-in jank).
|
||||
- [ ] **P2** `ClipboardPasteState.hasClip` aliased to `hasText`
|
||||
(`TextFieldSelectionState.native.kt:43`) — image-only clipboard undetected.
|
||||
- [ ] **P2** `ComposeOwner.kt` gaps: `autofill`/`autofillManager` null (`:324`),
|
||||
`hapticFeedBack` NOP (`:279`), deprecated `clipboardManager` NOP (`:287`, real
|
||||
`LocalClipboard` works), `textToolbar` stub (`:311`, touch-gated). No multi-monitor
|
||||
enumeration (`SDL_GetDisplays` unused). Most match upstream desktop — verify, then
|
||||
leave or fill.
|
||||
- [x] **P2** `installGlobals()` → `registerGenericFonts()`: VERIFIED not a hot-path cost —
|
||||
`registerGenericFonts()` already early-returns on a `fRegistered` flag
|
||||
(`GenericFonts.kt:20-26`), so the per-event call is a single boolean check, not a
|
||||
re-register. Original premise inaccurate; no change needed. (Minor residue: the
|
||||
`ImeBridge.onSessionActiveChange` lambda is reassigned per event — negligible, left
|
||||
as-is since it repoints per active window.)
|
||||
|
||||
---
|
||||
|
||||
## 3. SDL slimming
|
||||
|
||||
Static lib built from source per host (`scripts/build-sdl/build-all.py`, ref
|
||||
`release-3.4.12`); linked into the exe via `sdl3.def` (`staticLibraries=libSDL3.a`). Today
|
||||
only tests/examples + Windows D3D12/GPU are off (`build-all.py:311-319`). `SDL_Init` uses
|
||||
**`SDL_INIT_VIDEO` only** (`SDL3Backend.kt:49`).
|
||||
|
||||
**USED (keep ON):** Video/window, Events, Clipboard, Dialog (file open/save — no cheaper
|
||||
substitute; Linux uses portal/zenity, keep deps), OpenGL + Metal contexts, Render (2D CPU-
|
||||
raster blit path: `SkiaSurfaceBridge` uploads to `SDL_Texture` + `SDL_RenderPresent`),
|
||||
Filesystem, Cursor, Locale, System theme, Text input/IME, OpenURL, timing/hints.
|
||||
|
||||
**UNUSED (disable — zero references, no transitive need):** Audio, Joystick (→disables
|
||||
Gamepad), Haptic, HIDAPI, Sensor, Power, Camera, GPU API, Offscreen video driver, virtual
|
||||
joystick.
|
||||
|
||||
- [x] **P1** Added to `build-all.py` shared `vExtra`: `-DSDL_AUDIO=OFF`,
|
||||
`-DSDL_JOYSTICK=OFF`, `-DSDL_HAPTIC=OFF`, `-DSDL_HIDAPI=OFF`, `-DSDL_SENSOR=OFF`,
|
||||
`-DSDL_POWER=OFF`, `-DSDL_CAMERA=OFF`, `-DSDL_GPU=OFF`, `-DSDL_OFFSCREEN=OFF`,
|
||||
`-DSDL_VIRTUAL_JOYSTICK=OFF`. Promoted `-DSDL_GPU=OFF` from the Windows-only branch to
|
||||
the shared list; kept the per-host `-DSDL_RENDER_D3D12=OFF` (Windows). (Skipped
|
||||
`-DSDL_DISABLE_INSTALL_DOCS` — not a real SDL3 option; the build's `cmake --install`
|
||||
is relied upon.) All ten confirmed zero-reference by grep before disabling.
|
||||
- [~] **P1** After the flag change, rebuild + run apps to confirm no regression.
|
||||
**DONE on macOS/Metal:** `build-all.py` rebuilt libSDL3.a clean with the new flags;
|
||||
`:demo` links + runs to a settled screenshot (TextField screen, no visual regression);
|
||||
`:apidemo` links clean too. **PENDING:** both apps on Linux + Windows hosts (fold into
|
||||
WIN-SMOKE §5). If audio/gamepad is ever needed by a consumer app, re-enable the flag
|
||||
and rebuild the static lib (build-time only).
|
||||
- [x] **P2** Prune now-unreferenced macOS frameworks from `sdl3.def`. **DONE:** verified
|
||||
against the regenerated `libs/SDL3/lib/pkgconfig/sdl3.pc` `Libs:` line (source of
|
||||
truth), which after slimming no longer references `CoreAudio`, `AudioToolbox`,
|
||||
`AVFoundation`, `GameController`, `ForceFeedback`, or weak `CoreHaptics` — removed all
|
||||
six from `linkerOpts.osx` (kept `CoreMedia`, which the `.pc` still lists). Demo relinks
|
||||
clean. (Left Linux `linkerOpts` alone — those `-l` entries serve Skia/GL/X11, not the
|
||||
disabled SDL subsystems.)
|
||||
- [ ] **P2** (higher-risk, flag-don't-apply) Render-driver pruning to software-only. Only
|
||||
the CPU-raster fallback uses `SDL_Render`; GL/Metal go direct. But
|
||||
`SDL_CreateRenderer(window, null)` (`SDL3Backend.kt:117`) lets SDL pick the first
|
||||
driver — with only software present it picks software (fine) but couples the fallback
|
||||
to that assumption. **Conservative: leave render drivers alone.**
|
||||
|
||||
**Do NOT** add `-DSDL_DISABLE_INSTALL` — the build relies on `cmake --install`
|
||||
(`build-all.py:342`) to stage `libSDL3.a` + headers. `SDL_DYNAMIC_API` auto-disables for
|
||||
static builds (no flag needed). **Risk:** a future consumer app needing SDL audio/gamepad
|
||||
requires a static-lib rebuild (build-time, not a code change).
|
||||
|
||||
---
|
||||
|
||||
## 4. Performance
|
||||
|
||||
Prior PLAN Phases 0–3 landed; §12 measurement showed **no steady-state perf gap on
|
||||
macOS/Metal**. Remaining perf question (if any) is Windows-GL or heavy-interaction
|
||||
specific. Retained-layer engine is byte-for-byte upstream — not the gap.
|
||||
|
||||
- [ ] **P2** WON'T-FIX confirmed, keep as-is (don't relitigate): P0.4 per-frame full-surface
|
||||
clear + P0.6 per-frame Metal drawable reacquire (`CAMetalDrawable` single-use per
|
||||
frame, matches upstream `MetalRedrawer`).
|
||||
- [ ] **P2** P2.2 upstream `GlobalSnapshotManager` invalidation-driven scheduling —
|
||||
deferred; coalescing half done by P1.3; full manager is skiko-windowing-coupled
|
||||
(an explicit renderer non-goal — skiko-windowing-coupled). Leave deferred unless a
|
||||
Windows-GL perf gap surfaces.
|
||||
- [ ] **P2** `SkiaImageCache` font-cache half of P3.4 — evicted typefaces stay referenced by
|
||||
live paragraphs + `TypefaceFontProvider` (no clean unregister) so eviction frees no
|
||||
memory. Native-resource lifecycle backstop is the periodic `GC.collect()` nudge (top-
|
||||
level `Paragraph` a live `Text` holds has no Compose dispose seam). Accept for 1.0 or
|
||||
add a provider-unregister path.
|
||||
- [ ] **P2** P4.1 second-renderer manual-vendor reversal (restore upstream RenderNode
|
||||
shadows via `SkiaGraphicsContext.setLightingInfo` + relocate
|
||||
`prepareTransformationMatrix`; deltas D2–D6). Buys vendoring cleanliness at
|
||||
shadow-lighting/hit-test regression risk (current `NativeShadowCanvas` shadows render,
|
||||
draw=0.08ms). Low ROI — deferred, needs full verify-mac + parity.
|
||||
- [ ] **P2** Profile the shipped **Windows-GL** binary (`CDN_PROFILE=1`) under heavy
|
||||
interaction before ship (present phase is vsync-capped by display refresh — profile on
|
||||
the target monitor). Only open perf unknown.
|
||||
|
||||
---
|
||||
|
||||
## 5. API coverage / release mechanics
|
||||
|
||||
API coverage vs upstream is **99%** (8665/8751 decls via `compose-coverage.py`). Misses are
|
||||
AppKit/UIKit/web host actuals (N/A), `ImageComposeScene`/`renderComposeScene` (test util),
|
||||
and the `ui-text.platform` font layer (= P3.2 font resolver). The 1076 "extra" decls are
|
||||
umbrella-repo modules the tool can't compare + version skew, not invented surface.
|
||||
|
||||
- [ ] **P0** Re-pin refs to Compose **1.12.0 stable** when it ships (currently
|
||||
`v1.12.0-beta03+dev4483` — no clean beta03 tag on Maven). Bump both
|
||||
`scripts/compose-fork/compose.properties` refs, re-sync (`scripts/compose-fork/sync.sh`),
|
||||
let the build surface breakage.
|
||||
- [ ] **P0** Bump `vComposeJvmVersion` in `:demo` / `:apidemo` / `:material-symbols` (JVM
|
||||
parity leg currently forced to beta02 by documented skew — native leads).
|
||||
- [ ] **P1** Run `check-vendor-drift.py` after the ref bump — re-stamp `VENDOR-BASE` on the
|
||||
10 manual vendors, hand-reconcile any that actually changed base..pin.
|
||||
- [~] **P0** WIN-SMOKE fidelity pass (Windows host only, pre-ship gate): the Mac runbook
|
||||
cannot cover the shipped mingwX64 binary. Assert: (1) NotoSans `FontMetrics` dump
|
||||
native-vs-JVM-Windows (§1b acceptance) — **✅ DONE 2026-08-04 (`--metricsprobe`==`--metrics`)**;
|
||||
(2) `\t`/control-char render clean (§1a) — **✅ DONE**; (3) the Windows-only `PrintWindow`
|
||||
probe — pending (interactive); (4) the common-metadata publish job — compile gate now
|
||||
**✅ GREEN (2026-08-04, see the fix below)**; the actual publish + downstream-consume smoke
|
||||
still owed. Remaining: (3) `PrintWindow` probe + the interactive caret-height check for the
|
||||
§1b LineMetrics fix.
|
||||
- [ ] **P0** apiDump is **host-specific** — do NOT commit macOS dumps. Only the **Windows
|
||||
publish job compiles common metadata** (owns the root KotlinMultiplatform publications
|
||||
— the only host declaring every target, so only its `.module` files carry the full
|
||||
variant table; macOS-published roots left v0.1.15 without mingwX64 variants). Test
|
||||
`gradlew :<module>:compileCommonMainKotlinMetadata` before tagging; publish from
|
||||
Windows.
|
||||
- [x] **P0** **FIXED (2026-08-04) — common-metadata compile is now GREEN** (`compileCommonMainKotlin
|
||||
Metadata` across all modules → BUILD SUCCESSFUL). Was RED, blocking the Windows publish.
|
||||
Running `:material3:compileCommonMainKotlinMetadata` (or the aggregate) had failed at
|
||||
`:foundation:compileNativeMainKotlinMetadata` (pulled in via `:foundation:allMetadataJar`):
|
||||
`Scrollbar.skiko.kt` + `v2/Scrollbar.skiko.kt` — `Declaration annotated with
|
||||
'@OptionalExpectation' can only be used in common module sources`
|
||||
(`OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE`). **Root cause:** upstream keeps these
|
||||
`.skiko.kt` files in `skikoMain`, which INCLUDES the JVM-desktop target, so `@JvmName`/
|
||||
`@JvmStatic`/`@file:JvmName` resolve; the port vendors them into **native-only `nativeMain`**
|
||||
(no JVM target under it), so those JVM annotations are orphaned and the *metadata* compile
|
||||
rejects them (per-target compile tolerates them — that's why the APPS build fine, but the
|
||||
publish's `allMetadataJar` recompiles nativeMain metadata and dies). `:ui` and below pass;
|
||||
`:foundation` is the first break. **Blast radius:** 14 vendored native files use `@Jvm*`
|
||||
(`grep -rE "@file:JvmName|@JvmName|@JvmStatic" .../src/vendor/native .../src/nativeMain`); 11
|
||||
are in the GITIGNORED, sync-regenerated `src/vendor/native/` tree, so per-file edits DON'T
|
||||
survive a re-sync. **Fix (build/sync-level, not per-file):** either (a) a `sync.sh`
|
||||
post-step that injects `@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")`
|
||||
(or strips `@Jvm*`, which are no-ops with no JVM library target) into vendored native files —
|
||||
there's precedent: `components-resources/…/ResourceState.blocking.kt` already carries that
|
||||
suppress; or (b) a KGP-level relaxation of the native metadata compilation. Needs a real
|
||||
publish + downstream-consume verification after. Dedicated effort — likely THE reason the
|
||||
Windows metadata publish never went green.
|
||||
**FIX APPLIED (2026-08-04):** the root turned out narrower — sync.py ALREADY injects the K2
|
||||
`@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")`
|
||||
into every `src/vendor/` .kt (`sync.py:313`), so the regenerated tree was fine. The only gap
|
||||
was the two COMMITTED manual vendors in `src/nativeMain` that sync doesn't regenerate:
|
||||
`foundation/…/Scrollbar.skiko.kt` + `v2/Scrollbar.skiko.kt`. Added the identical `@file:Suppress`
|
||||
to both (provenance comment updated; `check-vendor-drift` still clean). Full
|
||||
`compileCommonMainKotlinMetadata` now green. **Remaining before publish:** run the actual
|
||||
publish + a downstream-consume smoke (the compile gate is necessary, not proof the `.module`
|
||||
variant table is complete).
|
||||
- [ ] **P1** Version bump to `1.0.0` across published coords once the above are green.
|
||||
- [x] **P0** Doc-hygiene blocker: `CLAUDE.md` documentation map referenced `RENDERER.md`,
|
||||
`SKIKO-MINGW-FEASIBILITY.md`, and `TODO.md`. **DONE:** removed `RENDERER.md` +
|
||||
`SKIKO-MINGW-FEASIBILITY.md` (stale/historical — current renderer essentials inlined in
|
||||
CLAUDE.md's doc map, the rest recoverable from git history); fixed the `CLAUDE.md`
|
||||
line-1 `ean` typo; dropped the `TODO.md` link (never committed) → its content is PLAN.md
|
||||
§2. `git grep` confirms no remaining links to any deleted doc.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vendor the upstream skiko text engine (next major refactor — approved 2026-08-04)
|
||||
|
||||
**Goal:** honour "VENDOR ALL WE CAN". Delete the hand-rolled reduced text engine
|
||||
(`SkiaParagraph.native.kt` + `NativeParagraphOps` seam + `SkiaParagraphEngine.kt` +
|
||||
`FontRasterizationSettings.native.kt` port) and vendor upstream's skiko text subsystem VERBATIM.
|
||||
|
||||
**Why it was hand-rolled (the REAL reason, not SDL_TTF):** `Paragraph` is `expect sealed interface`
|
||||
(`vendor/common/…/Paragraph.kt:56`) whose `actual` sits in the **skiko-FREE `nativeMain`**, so a
|
||||
sealed implementer can't touch skiko → the hand seam. `nativeMain` is skiko-free because it's the
|
||||
shared parent of the **official-skiko** (mac/linux, `libs.skiko`) and **fork-skiko** (mingw,
|
||||
`com.bitsycore.skiko:skiko`) legs — two different artifacts, so the shared parent can carry neither.
|
||||
|
||||
**Why it's feasible NOW (all-skiko + srcDir reuse):** every native leg already reaches
|
||||
`src/skikoRendererMain/kotlin` (mingw via `kotlin.srcDir(...)` in `skikoRendererMingwSharedMain`,
|
||||
`ui-text/build.gradle.kts:54`), and the fork exposes the SAME `org.jetbrains.skia.*` Kotlin API. So
|
||||
the `Paragraph` **actual + engine can move DOWN into the skiko source set** (served to all targets)
|
||||
instead of skiko-free `nativeMain` — which lets upstream's skiko files compile as-is.
|
||||
|
||||
**⚠️ SPIKE FINDING (2026-08-04) — the real gate is the FONT LOADER, not the source-set move.**
|
||||
A read-only feasibility map (no code changed) found two layers:
|
||||
- **Layer 1 — source-set / sealed move: FEASIBLE.** The `actual sealed interface Paragraph`
|
||||
(`vendor/native/Paragraph.native.kt:36`, signature-only mirror) + its implementer + the 11 factory
|
||||
actuals can move to `skikoRendererMain` (all leaves reach it; sealed same-source-set holds). Just
|
||||
mechanical.
|
||||
- **Layer 2 — font model: THIS is the blocker, and it's the SAME task as §2-P1 `PlatformFontLoader`.**
|
||||
Upstream's engine (`SkiaParagraphIntrinsics.skiko.kt:38,61`) resolves typefaces through
|
||||
`FontFamily.Resolver` → `PlatformFontLoader`. The port's `SdlPlatformFontLoader`
|
||||
(`font/FontFamilyResolver.native.kt:30-49`) is a **NOP** (`loadBlocking`/`awaitLoad`→`Unit`,
|
||||
`resolve`→`Immutable(Unit)`). The port's hand-rolled engine exists precisely to BYPASS that NOP by
|
||||
resolving through `SkiaFonts` (icon fonts + variable axes + data.kres bytes). So vendoring the
|
||||
upstream engine verbatim routes font resolution through the NOP → **all text goes blank/tofu**
|
||||
until the loader is real.
|
||||
- **Consequence:** the verbatim engine-vendor REQUIRES first implementing upstream's font-loader path
|
||||
(vendor `SkiaFontLoader.skiko.kt` as the reference) so the port's `SkiaFonts` icon/variable-axis
|
||||
model rides upstream's `PlatformFontLoader`/`FontLoadResult` seam. This is the deferred high-risk P1
|
||||
in §2 — it and this §6 are one effort. **Intermediate option:** vendor the engine files but keep
|
||||
`SkiaFonts` via a Rule-3 edit at the one resolve call site (vendors most files verbatim, 1–2 stay
|
||||
hand-reconciled). **Verification needs macOS + Linux hosts** (parity), so this is a dedicated
|
||||
effort, not a Windows-only session — do it with mac access, on its own branch.
|
||||
|
||||
**Plan (spike on a branch, verify on THIS Windows host — build + render mingwX64):**
|
||||
1. Move the `Paragraph`/`ParagraphIntrinsics` **actuals** into `skikoRendererMain` (reused by mingw
|
||||
via the existing `srcDir`). Confirm the `expect sealed` actual is accepted there for every leaf
|
||||
target (the b63 memo hit a sealed-in-`nativeMain` wall — test whether hosting the actual in the
|
||||
skiko set clears it; the sealed same-module rule is per-target-compilation, which should hold).
|
||||
2. Vendor VERBATIM into `src/vendor/skikoRenderer/`: `SkiaParagraph.skiko.kt`,
|
||||
`ParagraphBuilder.skiko.kt`, `SkiaParagraphIntrinsics.skiko.kt`, `SkiaTextPaint.skiko.kt`,
|
||||
`ParagraphLayouter.skiko.kt`, `PlatformFont.skiko.kt`, `FontRasterizationSettings.skiko.kt`,
|
||||
`TextStyle.skiko.kt`; add them to `ui-text/compose-fork.txt`; delete the hand-rolled files.
|
||||
3. Reconcile the ~7 hierarchy-crossing `expect/actual` pairs the inverted layout needed; keep the
|
||||
`SkiaFonts` family/variable-axis model (it's the port's real value-add) behind upstream's seams.
|
||||
4. **Fork-only Rule-3 edit:** upstream's code calls `paragraph.lineMetrics` — the fork's extern-C
|
||||
binding mis-decodes ascent/descent (§1b finding). Either carry the guarded reconstruction (from
|
||||
`SkiaParagraphOps.lineMetrics()`) into the vendored `SkiaParagraph.skiko.kt` as a Rule-3 edit, or
|
||||
fix it in the fork DLL. **Bonus:** vendoring `ParagraphBuilder.skiko.kt` verbatim wires
|
||||
`halfLeading` → the §1b M3-baseline drift disappears for free.
|
||||
5. Verify: mingwX64 build + `--screen=BasicText/Buttons/AnnotatedString` render clean +
|
||||
`--metricsprobe` == JVM `--metrics`. Then macOS/Linux verify + parity (needs those hosts).
|
||||
|
||||
**Sequencing:** AFTER the current shared-Kotlin fixes land (this session). If the sealed-actual move
|
||||
proves infeasible, fall back to keeping the seam but at least converting the hand files to tracked
|
||||
Rule-3 vendors (like `FontRasterizationSettings.native.kt` now has a `VENDOR-BASE` line).
|
||||
|
||||
## Accepted 1.0.0 gaps (documented, not fixed)
|
||||
|
||||
Drag-OUT of window (SDL platform limit; drop-IN works), full accessibility pipeline (out of
|
||||
scope for desktop 1.0 — but ship the non-throwing `PlatformScreenReader` no-op), the
|
||||
hand-rolled text engine as an architectural deviation (§2 P1), per-focus
|
||||
`SDL_StartTextInput/StopTextInput`, `loadImageBitmap`/`loadSvgPainter` (JVM `InputStream`
|
||||
signatures, N/A on K/N).
|
||||
@@ -18,20 +18,21 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
|
||||
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
|
||||
`animation`, and `material3` are copied from Compose Multiplatform verbatim
|
||||
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`.
|
||||
- **Rendering is Skia everywhere.** macOS and Linux link the official Skiko
|
||||
klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships
|
||||
Skiko and Skia together in `skiko-windows-x64.dll`.
|
||||
- **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.
|
||||
- **Lean distributables.** SDL3 is built as a static library and linked in. On
|
||||
macOS and Linux a distributable is just the executable plus a `data.kres`
|
||||
resource bundle; on Windows it also ships `skiko-windows-x64.dll` next to the
|
||||
exe (auto-provisioned by the bridge plugin).
|
||||
|
||||
| Platform | Gradle target | Default renderer |
|
||||
|----------|---------------|------------------|
|
||||
| macOS arm64 | `macosArm64` | Skia (Metal) |
|
||||
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) |
|
||||
| Windows | `mingwX64` | SDL3 |
|
||||
| Platform | Gradle target | Renderer |
|
||||
|----------|---------------|----------|
|
||||
| macOS arm64 | `macosArm64` | Skia (Metal) — official Skiko |
|
||||
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) — official Skiko |
|
||||
| Windows | `mingwX64` | Skia — bitsycore Skiko fork |
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -71,7 +72,7 @@ saved state behave as they do on Android.
|
||||
|
||||
```kotlin
|
||||
commonMain.dependencies {
|
||||
implementation(project(":window")) // window shell + main loop
|
||||
implementation(project(":desktop-native-window")) // window shell + main loop
|
||||
implementation(project(":material3")) // Material 3 widgets
|
||||
implementation(project(":material-symbols")) // icon-font composables (optional)
|
||||
}
|
||||
@@ -79,7 +80,10 @@ commonMain.dependencies {
|
||||
|
||||
### Building from your own project: the bridge plugin
|
||||
|
||||
The klibs publish to GitHub Packages under `com.bitsycore.compose.sdl:*`. Apply
|
||||
The klibs publish to [maven.bitsycore.com](https://maven.bitsycore.com/releases)
|
||||
(no auth) and GitHub Packages (authenticated fallback) under per-area coordinates that mirror
|
||||
upstream — `com.bitsycore.compose.ui:ui`, `com.bitsycore.compose.foundation:foundation`,
|
||||
… (the `com.bitsycore` fork of each `org.jetbrains.compose.*`). 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.
|
||||
@@ -102,7 +106,15 @@ commonMain.dependencies {
|
||||
}
|
||||
```
|
||||
|
||||
Repositories, credentials, and version pinning:
|
||||
**Two GitHub Packages repos are needed.** A consumer declares the port's repo
|
||||
(`bitsycore/compose-desktop-native`) **and** the skiko fork's (`bitsycore/skiko`).
|
||||
The Windows (mingwX64) target renders through the bitsycore skiko fork, and
|
||||
GitHub Packages binds each package name to a **single** repository — so
|
||||
`com.bitsycore.skiko` can't be mirrored into the port's repo, it stays in its
|
||||
own. Scope them with `includeGroup`/`excludeGroup("com.bitsycore.skiko")`
|
||||
(macOS/Linux pull the official skiko from Maven Central instead; if you build no
|
||||
mingwX64 you can omit the fork repo). The exact `repositories {}` snippet,
|
||||
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
|
||||
@@ -122,7 +134,7 @@ 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.bat :demo:runDebugExecutableMingwX64 # Windows (Skia / Skiko fork)
|
||||
./gradlew :demo:run # JVM Compose Desktop (reference)
|
||||
```
|
||||
|
||||
@@ -138,14 +150,11 @@ client certificates.
|
||||
./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
|
||||
`compose/` tree. The renderer lives in `:ui` and the `sdl3` cinterop in
|
||||
`:sdl-core`; `:desktop-native-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.
|
||||
|
||||
@@ -159,7 +168,7 @@ compatible artifacts are in [CLAUDE.md](CLAUDE.md).
|
||||
Build the native libraries once per machine, then build any app target:
|
||||
|
||||
```bash
|
||||
python3 scripts/build-sdl/build-all.py # SDL3, SDL3_ttf, SDL3_image, FreeType (static)
|
||||
python3 scripts/build-sdl/build-all.py # SDL3 (static)
|
||||
./gradlew :demo:runDebugExecutableMacosArm64
|
||||
```
|
||||
|
||||
|
||||
-263
@@ -1,263 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,217 +0,0 @@
|
||||
# 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 H)
|
||||
are lower priority than the cross-cutting platform gaps 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.
|
||||
|
||||
## Completed
|
||||
|
||||
Platform / cross-cutting:
|
||||
|
||||
- **Locale / i18n** (`5d03711f`) — `Locale.current` / `LocaleList.current` read the
|
||||
OS locale via SDL, unlocking the 40+ Material 3 translations. The date/number
|
||||
format tail remains (section C).
|
||||
- **IME text input** (`500c49e4`) — a real `PlatformTextInputSession` routes SDL
|
||||
committed + composing text to the focused field (commit replaces the
|
||||
composition); `SDL_SetTextInputArea` positions the OS candidate window.
|
||||
- **Cursor icons** (`1fcc0828`) — `Modifier.pointerHoverIcon` drives the OS cursor
|
||||
via SDL (I-beam over text, hand over links).
|
||||
- **WindowInfo** (`bad23073`) — `isWindowFocused` + `containerSize` / `containerDpSize`
|
||||
report real values (were hardcoded `true` / `Zero`).
|
||||
- **ImageBitmap from bytes** (SDL leg) — `ByteArray.decodeToImageBitmap()` /
|
||||
`createImageBitmap(bytes)` decodes via SDL3_image; was
|
||||
`UnsupportedOperationException`. Verified `demo --imagebytestest` on both legs.
|
||||
- **`FontFamily.Monospace`** — generic families map to `"generic:<name>"`;
|
||||
`registerGenericFonts` registers bundled NotoSansMono under `generic:monospace`
|
||||
(bundled only when the app references it). Verified `demo --fonttest`.
|
||||
|
||||
SDL renderer fidelity (all verified against the Skia leg via `demo --<name>test`):
|
||||
|
||||
- **DashPathEffect** (`--dashtest`), **gradient TileMode** repeat/mirror/decal
|
||||
(`--tilemodetest`), **drawPoints/drawRawPoints** Points/Lines/Polygon
|
||||
(`--pointstest`), **rotated images** via textured `SDL_RenderGeometry`
|
||||
(`--rotimgtest`).
|
||||
- **BlendMode** (`774271eb`, `--blendtest`) — Plus/Modulate/Multiply/Src via SDL
|
||||
hardware blend (`withBlend`); other separable modes fall back to SrcOver.
|
||||
- **Stroke joins + Square cap** (`3518521c`, `--jointest`) — miter/bevel/round
|
||||
(`emitJoin`) + round/square end caps (`emitCap`), pixel-identical to Skia.
|
||||
- **ColorFilter on shapes** (`169a51f0`, `--filtertest`) — tint / colorMatrix
|
||||
(grayscale/saturation) / lighting per-vertex; pixel-exact vs Skia.
|
||||
- **Stroked elliptical ovals** (`efaf20d9`, `--ovaltest`) — true ellipse band, not
|
||||
a circular ring on the averaged radius.
|
||||
|
||||
---
|
||||
|
||||
## Release-blocking shortlist for stable 1.12
|
||||
|
||||
The cross-cutting items to weigh first (all detailed below):
|
||||
|
||||
1. **No copy/paste context menu** (section B). Keyboard copy works; there is no
|
||||
right-click or selection toolbar (the desktop path is the right-click context
|
||||
menu, upstream TODO CMP-7819).
|
||||
2. **Accessibility is entirely absent** (section A). Decide whether stable
|
||||
requires any screen-reader support at all.
|
||||
3. Fonts (section D): compose-resources `Font(Res.font.x)` works on both legs, and
|
||||
`FontFamily.Monospace` now renders NotoSansMono. Remaining: `Serif`/`Cursive`
|
||||
generics (no bundled font) and the androidx `Font(bytes)`/`PlatformFontLoader`
|
||||
path. Nice-to-have, not a blocker.
|
||||
|
||||
---
|
||||
|
||||
## 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 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`. NOTE: the floating toolbar is gated on `isInTouchMode`, so on desktop (mouse) it never shows anyway — the desktop path is the right-click context menu below. **Nice-to-have (touch only).**
|
||||
- `compose/foundation/.../text/selection/SelectionActuals.native.kt:42` · `addSelectionContainerTextContextMenuComponents` NOP (upstream TODO CMP-7819). No right-click copy/select-all menu for `SelectionContainer`. **Blocker for desktop text UX.**
|
||||
- `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`. **Blocker for desktop text UX.**
|
||||
|
||||
## C. Locale / i18n tail (needs locale-aware DATA, no ICU on K/N)
|
||||
|
||||
`Locale.current` / `LocaleList.current` are wired to the OS (see Completed). The
|
||||
rest needs month/weekday name tables and date/number patterns, which Kotlin/Native
|
||||
has no ICU for; SDL reports language + country only (no script subtag, no
|
||||
calendar/clock/number prefs). These require a bundled CLDR subset or a K/N i18n lib.
|
||||
|
||||
- `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`; needs its own wiring so `CalendarModel`/`PlatformDateFormat` can vary by locale. Inert until `PlatformDateFormat` is localized. **Nice-to-have (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 for localized DatePicker.**
|
||||
- `:32` `formatWithPattern`/`formatWithSkeleton` ignore the pattern/skeleton and always emit ISO `yyyy-MM-dd`. **Blocker for localized DatePicker.**
|
||||
- `: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` · `Locale.isRtl()` not implemented; the pipeline is LTR-only (`SdlParagraph.native.kt` `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.**
|
||||
|
||||
## D. Font resolution
|
||||
|
||||
**Custom fonts via compose-resources work.** `org.jetbrains.compose.resources.Font(Res.font.x)`
|
||||
loads the bytes and registers them with the project font registry (IconFont → NamedFont)
|
||||
on both renderer legs (`FontResources.sdl.kt`, shared by the skiko leg via a srcDir
|
||||
alias), so the standard CMP way to bundle a font renders correctly.
|
||||
|
||||
- `FontFamily.Serif` / `FontFamily.Cursive` still collapse to the default sans — no bundled
|
||||
serif/cursive font yet (`downloadNotoFonts` fetches Sans + SansMono only). Register one under
|
||||
`generic:serif` / `generic:cursive` the same way to enable them. **Nice-to-have.**
|
||||
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:32` · `SdlPlatformFontLoader.loadBlocking`/`awaitLoad` are NOP; the androidx `PlatformFontLoader` path (`ResourceFont` / `Font(bytes)` / `LoadedFontFamily`) doesn't load. Use the working compose-resources `Font()` instead. **Nice-to-have.**
|
||||
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:38` · `PlatformFontFamilyTypefaceAdapter.resolve` returns `TypefaceResult.Immutable(Unit)` (the renderer resolves by name, so harmless). **Nice-to-have.**
|
||||
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:69` · deprecated `createFontFamilyResolver(fontResourceLoader)` ignores the supplied loader. **Cosmetic.**
|
||||
|
||||
## E. 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.**
|
||||
|
||||
## F. 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.**
|
||||
|
||||
## G. Window and platform info
|
||||
|
||||
- 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.**
|
||||
|
||||
## H. 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. (Several
|
||||
SDL fidelity gaps are now closed — see **Completed** above.)
|
||||
|
||||
The biggest remaining value here is the **offscreen-layer work** (blur + real
|
||||
`saveLayer`); the two are the same infrastructure and warrant a dedicated pass
|
||||
with the full parity + verify-mac gate, not a probe.
|
||||
|
||||
- **Blur / RenderEffect** — `compose/ui/ui/src/sdlRendererMain/.../graphics/RenderEffect.sdl.kt:16` `RenderEffect.isSupported()` is `false`; `Modifier.blur()` and `graphicsLayer{renderEffect=...}` do nothing; `.../graphics/shadow/Blur.native.kt:33` `Paint.setBlurFilter` NOP. Needs an offscreen render target + a blur pass (box-blur infra already exists in `Sdl3ShadowCache`). **Nice-to-have (Windows visual parity).**
|
||||
- **`saveLayer` has no offscreen buffer** — `Sdl3Canvas.kt:436`: 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. Same offscreen infra as blur. **Nice-to-have (blocker for correct group-alpha over overlapping content).**
|
||||
- **`clipPath` degrades to a bounding box** — `Sdl3Canvas.kt:509`: an arbitrary path clip collapses to its AABB; a rotated/sheared `clipRect` also collapses to an AABB, so non-rect clips leak. (Rounded/difference clips are real, with feathered AA.) **Nice-to-have.**
|
||||
- **`ActualImageShader` / `ActualCompositeShader` are stubs** — `CanvasPaintActuals.native.kt:106`: an image/composite `ShaderBrush` degrades to a solid white/black fill (the tessellator emits per-vertex colours, not UVs). **Nice-to-have.**
|
||||
- **`PathFillType` (NonZero vs EvenOdd)** — mostly ignored beyond the 2-contour border-ring case; interior holes and self-intersections are not cut out, and concave fills self-overlap (fan triangulation) (`Sdl3DrawScope.kt:366,717`). Needs a scanline/tessellation fill. **Nice-to-have.**
|
||||
- **`drawVertices`** (custom vertex meshes) is still a NOP in the DrawScope. **Nice-to-have.**
|
||||
- **ColorFilter/blur on IMAGE blits** — image blits honor only `BlendModeColorFilter` tint (`Sdl3Canvas.kt:1281`); a `ColorMatrix`/lighting filter or blur on a bitmap needs per-pixel work. Capture-mode (`graphicsLayer`) block-replay also drops the shape colorFilter. **Nice-to-have.**
|
||||
- **`SdlImageBitmap.readPixels` is a no-op** (`Sdl3Offscreen.kt:117`) — can't read back an offscreen-rendered bitmap. **Cosmetic / nice-to-have.**
|
||||
- **Drop shadow is approximated** (`Sdl3Canvas.kt:785`) — 9-slice / stacked rings, not a true gaussian; ambient vs spot largely collapsed; AA is a ~1px geometry fringe, not analytic coverage. **Cosmetic.**
|
||||
- `DashPathEffect` `corner`/`chain`/`stamped` variants remain NOP (only the interval dash is implemented). **Cosmetic.**
|
||||
|
||||
## I. Skia renderer (macOS/Linux default) anomalies
|
||||
|
||||
The Skia leg is normally the fidelity reference, so its bugs are rarely tracked
|
||||
here. This one surfaced while adding SDL BlendMode parity (`demo --blendtest`):
|
||||
|
||||
- `BlendMode.Multiply` renders wrong on the Skia leg. A filled `drawRect` with
|
||||
opaque `Color.Cyan` over opaque `Color.Yellow` reads back `(0,0,255)` blue;
|
||||
the multiply formula can only yield `(0,255,0)` green for opaque cyan×yellow,
|
||||
and the SDL leg produces exactly that. `Plus` and `Modulate` composite
|
||||
correctly on the same path, so `paint.blendMode` IS applied — the fault is
|
||||
specific to `MULTIPLY` (likely a Metal-backend / premultiply interaction in
|
||||
the graphics-layer flatten, not the `BlendMode.toSkia()` map, which is
|
||||
correct). Needs isolating on the Skia draw path. **Nice-to-have (Skia leg).**
|
||||
|
||||
## J. Needs further review (2026-07 bug audit)
|
||||
|
||||
An audit (3 parallel passes over the renderer, recent SDL features, and the
|
||||
text/input/event actuals) surfaced these. The clear, contained bugs were fixed
|
||||
in the same pass (see below); the items here are left open because they are
|
||||
either invasive, risky, or a cosmetic-AA judgement call.
|
||||
|
||||
Fixed in the audit (for reference, not open work): the `roundRectCore`
|
||||
inverted-rect crash; `drawArc(useCenter=false)` now fills the segment (was always
|
||||
a pie sector); `BlendMode.Src` no longer punches the AA fringe out via
|
||||
`SDL_BLENDMODE_NONE`; the cover-fill fast-path no longer drops the paint's
|
||||
`colorFilter`; `saveLayer` in capture mode keeps the save/restore stack balanced;
|
||||
stroked oval / round-rect inner radius is clamped ≥ 0; the per-vertex
|
||||
`ColorMatrix` copy is hoisted out of the sample loop; `Locale.region` no longer
|
||||
returns the language for a single-subtag tag; and a `MOUSE_LEAVE` now clears
|
||||
hover so a widget doesn't stay highlighted after the cursor exits.
|
||||
|
||||
Open — platform / input:
|
||||
|
||||
- **Pointer coords are truncated to Int before DPR scaling** (`SDL3EventMapper.kt` ~78-97, `LegacyPointerEvent.kt` x/y are `Int`, consumed in `ComposeWindow.kt:756`). On a 2× display a click at logical (100.9, 50.4) becomes physical (200,100) instead of (~201,~100), so hit-testing / caret placement quantizes to 2-physical-px steps near glyph boundaries. Fix means widening the pointer event to `Float` through the pipeline — invasive, hence deferred. **Review.**
|
||||
- **A window created unfocused is still promoted to RESUMED** (`ComposeWindow.kt:575-576, 713-719`). `windowFocused`/`windowVisible` default `true`, so a second `Window {}` opened while another holds focus (or one opened minimized) reports `RESUMED` until SDL later delivers a focus/minimize event. Should query `SDL_GetWindowFlags` at creation. **Review.**
|
||||
- **IME text-input area is only pushed on the first `TEXT_EDITING`** (`ComposeWindow.kt:803`; `updateImeArea()` not called on focus gain / first `StartTextInput` / caret move). The first candidate popup can appear at (0,0) before correcting. The rect math itself is correct. **Review.**
|
||||
- **`Snapshot.sendApplyNotifications()` is called synchronously inside the global write observer** (`ComposeWindow.kt:143-146`). Compose Desktop's `GlobalSnapshotManager` deliberately defers it to a separate dispatch; calling it per-write is redundant (the main loop already calls it each frame) and risks re-entrancy during snapshot application. Verify against the runtime's guard before changing — behaviour-sensitive. **Review (risky).**
|
||||
|
||||
Open — SDL renderer (cosmetic AA / draw-time):
|
||||
|
||||
- **Stroke joins are not antialiased and reach ~0.5px past the band** (`Sdl3DrawScope.kt` `emitJoin`). The segment band feathers to `halfW − kAaHalf` solid + fringe, but joins fill solid to full `halfW` with a hard edge, so corners read as slightly darker/crisper nubs — visible on thin or translucent strokes. Needs feathered join geometry. **Review (cosmetic).**
|
||||
- **Round join over-draws the stroke body** (`Sdl3DrawScope.kt` `emitJoin`, `StrokeJoin.Round`): a full disc overlaps the adjacent segments, so a semi-transparent stroke double-blends darker at each corner. Fill only the convex wedge. **Review (cosmetic).**
|
||||
- **Text / icon draws ignore the draw-time `colorFilter`** — `activeColorFilter` is consulted only by the tessellation sampler; glyph/icon runs blit through the text renderer with a pre-resolved colour, so a `BlendModeColorFilter` tint applied at draw time to a `Text`/icon node isn't honored (acceptable if callers pre-fold the tint into the glyph colour). **Review.**
|
||||
- **Rounded-clip containment ignores stroke half-width** (`Sdl3Canvas.kt` `admitDraw`/`tryDrawRectUnderPendingClips`): a stroked shape whose fill-box is inside a pending rounded clip but whose stroke pokes past it skips mask realization, so the stroke's outer half is bounded only by the AABB, not the rounded outline. Marginal (only bites at the exact clip boundary). **Review.**
|
||||
+60
-20
@@ -8,18 +8,27 @@ For architecture, source-set layout, and vendoring rules, see
|
||||
|
||||
## 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`.
|
||||
SDL3 is built from source as a static library and linked straight into the
|
||||
executable — the windowing, input, and platform-integration layer. One script
|
||||
does it on every OS. Output lands in the gitignored `libs/`; the version is
|
||||
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
|
||||
python3 scripts/build-sdl/build-all.py # sdl3
|
||||
python3 scripts/build-sdl/build-all.py sdl3 # rebuild the 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.
|
||||
absent). Run it once per machine, or after bumping the pinned version.
|
||||
|
||||
The static lib is built **slim**: the port uses SDL only for video/window,
|
||||
events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render`
|
||||
blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py`
|
||||
disables every unused subsystem — audio, joystick, haptic, hidapi, sensor,
|
||||
power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus
|
||||
tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of
|
||||
these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and
|
||||
rebuild — a build-time change, no code edit.
|
||||
|
||||
## Vendoring upstream Compose
|
||||
|
||||
@@ -63,16 +72,16 @@ python3 scripts/compose-coverage.py --missing ui-text # list uncovered decl
|
||||
### 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:
|
||||
macOS or Linux it exercises the Skia renderer 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).
|
||||
It runs: 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
|
||||
|
||||
@@ -110,6 +119,21 @@ CDN_PROFILE=1 CDN_FORCERENDER=1 ./demo/build/bin/macosArm64/debugExecutable/demo
|
||||
Note: `present` is vsync-blocking, so profile on the target refresh rate before
|
||||
drawing conclusions about a frame-rate gap.
|
||||
|
||||
### Text metrics diagnostic
|
||||
|
||||
Set `CDN_TEXT_METRICS=1` and run any app to dump, per built paragraph, the font
|
||||
scaler's `ascent`/`descent`/`leading`, the resolved `familyName` + `fontPx`, and
|
||||
the resulting paragraph line box (`paraHeight`, `line0[ascent/descent/baseline/
|
||||
height]`). Its job is the mac-vs-Windows vertical-spacing question (PLAN.md §1b):
|
||||
the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per
|
||||
host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different
|
||||
metric tables. Run it on native AND on the JVM parity app on the SAME host and
|
||||
diff the lines — Windows-native must match skiko-JVM-Windows.
|
||||
|
||||
```bash
|
||||
CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp
|
||||
```
|
||||
|
||||
### Demo probes
|
||||
|
||||
`:demo` ships headless regression probes driven by CLI flags, used by the
|
||||
@@ -135,6 +159,13 @@ gradlew.bat :demo:runDebugExecutableMingwX64
|
||||
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||
```
|
||||
|
||||
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
||||
GitHub Packages as `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (macOS/Linux use
|
||||
official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
|
||||
to the executable by the bridge plugin — no manual copy. The fork itself is
|
||||
published by a separate GitHub Actions workflow in the fork repo, out of band
|
||||
from this repo's release flow.
|
||||
|
||||
## Consuming the port
|
||||
|
||||
To build a third-party app against the published klibs, apply the bridge Gradle
|
||||
@@ -151,11 +182,11 @@ 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`. |
|
||||
| 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`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` — see `groupFor()` in the root build. | 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. |
|
||||
| Skiko | `skiko` in `gradle/libs.versions.toml`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.1` from GitHub Packages), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
||||
| SDL3 | `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
|
||||
@@ -168,8 +199,8 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
||||
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).
|
||||
4. Build and fix any fallout.
|
||||
5. `scripts/verify-mac.sh` (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.
|
||||
@@ -180,16 +211,25 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
||||
|
||||
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`.
|
||||
2. On a Windows host, run `gradlew :desktop-native-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`.
|
||||
3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run
|
||||
`./gradlew apiDump` and commit any diff. Not a hard gate yet — the API is
|
||||
pre-stable and `apiCheck` isn't wired — but regenerating each release keeps
|
||||
the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred
|
||||
from the linux/mingw ABIs (accurate for the target-independent Compose
|
||||
surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps)
|
||||
are excluded via `apiValidation.ignoredProjects`. Best host: Windows — it
|
||||
builds the mingw slice (the fork's unique surface) for real and only infers
|
||||
macos; the macos ABI is target-independent for the tracked pure-Kotlin modules.
|
||||
4. `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
|
||||
under their per-area `com.bitsycore.compose.*` coords. 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
|
||||
|
||||
@@ -81,7 +81,7 @@ kotlin {
|
||||
}
|
||||
nativeMain {
|
||||
dependencies {
|
||||
implementation(project(":window"))
|
||||
implementation(project(":desktop-native-window"))
|
||||
implementation(libs.ktor.client.curl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,7 +908,7 @@ private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerCh
|
||||
|
||||
/** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1),
|
||||
the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n)
|
||||
scan, memoised — replaces the old gutter pre-pass that ran measurer.wrap() on
|
||||
scan, memoised — replaces the old gutter pre-pass that measured wrapping on
|
||||
every one of N lines on each width change (the other half of the freeze). */
|
||||
private fun lineStartOffsets(inText: String): IntArray {
|
||||
val vStarts = ArrayList<Int>(64)
|
||||
@@ -963,10 +963,10 @@ internal fun BodyView(
|
||||
val vGutterWidth = (vDigits * 7 + 4).dp
|
||||
// Body wrap width (physical px, from onSizeChanged), reported by the body
|
||||
// Box once it's laid out (0 on the first frame). The gutter numbering
|
||||
// depends on it — see vNumbers. NativeTextMeasurer.wrap expects the font
|
||||
// depends on it — see vNumbers. Wrap measurement expects the font
|
||||
// size + max-width in the SAME unit (both physical px on Retina), so we
|
||||
// scale 12.sp through the current density here, matching what SdlParagraph
|
||||
// does when it builds the body's real paragraph.
|
||||
// scale 12.sp through the current density here, matching what the paragraph
|
||||
// engine does when it builds the body's real paragraph.
|
||||
var vBodyWidthPx by remember { mutableStateOf(0) }
|
||||
val vFontPx = with(LocalDensity.current) { 12.sp.toPx().toInt() }
|
||||
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
|
||||
|
||||
@@ -16,7 +16,13 @@ import com.compose.sdl.res.painterResource
|
||||
import com.compose.sdl.revealInFileManager
|
||||
import com.compose.sdl.showOpenFileDialog
|
||||
import com.compose.sdl.showSaveFileDialog
|
||||
import com.compose.sdl.text.currentTextMeasurer
|
||||
import com.compose.sdl.text.namedFontFamily
|
||||
import androidx.compose.ui.text.Paragraph
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.curl.*
|
||||
import okio.FileSystem
|
||||
@@ -49,8 +55,31 @@ actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
|
||||
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
||||
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
|
||||
// Cached resolver for wrappedRowCount — the skiko engine resolves fonts by name
|
||||
// (projectFontName), so this is just the required non-null Paragraph argument.
|
||||
private val mRowCountFontResolver = createFontFamilyResolver()
|
||||
|
||||
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int {
|
||||
if (inText.isEmpty()) return 1
|
||||
// inFontPx is already physical pixels, so measure at density 1 (fontSize.value
|
||||
// * density = inFontPx) and wrap at inMaxWidthPx px by building a Paragraph.
|
||||
// lineCount comes from skiko's paragraph layout.
|
||||
val vStyle = TextStyle(
|
||||
fontSize = inFontPx.sp,
|
||||
fontFamily = inFamilyName?.let { namedFontFamily(it) },
|
||||
)
|
||||
return Paragraph(
|
||||
text = inText,
|
||||
style = vStyle,
|
||||
constraints = Constraints(maxWidth = inMaxWidthPx.coerceAtLeast(0)),
|
||||
density = Density(1f),
|
||||
fontFamilyResolver = mRowCountFontResolver,
|
||||
spanStyles = emptyList(),
|
||||
placeholders = emptyList(),
|
||||
maxLines = Int.MAX_VALUE,
|
||||
ellipsis = false,
|
||||
).lineCount
|
||||
}
|
||||
|
||||
actual var editorTabWidth: Int
|
||||
get() = TextLayoutConfig.tabWidth
|
||||
|
||||
+60
-15
@@ -12,14 +12,49 @@ plugins {
|
||||
apiValidation {
|
||||
@OptIn(ExperimentalBCVApi::class)
|
||||
klib { enabled = true }
|
||||
ignoredProjects.addAll(listOf("demo", "apidemo"))
|
||||
// demo/apidemo: apps, no published API. sdl-core: naked SDL cinterop — its
|
||||
// "API" is the generated sdl3.* bindings (per-OS, can't infer macos), not our
|
||||
// surface. material-symbols: auto-generated icon codepoint maps + its jvm
|
||||
// dump trips BCV's ASM on newer JDKs (class file major 69). Track the
|
||||
// hand-authored Compose surface, not generated/binding code.
|
||||
ignoredProjects.addAll(listOf("demo", "apidemo", "sdl-core", "material-symbols"))
|
||||
}
|
||||
|
||||
// Version is driven by PUBLISH_VERSION (set from the git tag in the publish
|
||||
// workflow, `v1.2.3` → `1.2.3`). Local dev / demo runs default to SNAPSHOT.
|
||||
val vPublishVersion = (System.getenv("PUBLISH_VERSION") ?: "0.0.0-SNAPSHOT").removePrefix("v")
|
||||
|
||||
// Published groups mirror the org.jetbrains.compose.<area> originals under the
|
||||
// com.bitsycore fork name, so each module is the obvious 1:1 fork of its upstream
|
||||
// coord (e.g. org.jetbrains.compose.ui:ui → com.bitsycore.compose.ui:ui). Modules
|
||||
// with no upstream area (project-only: :sdl-core, :material-symbols) fall through
|
||||
// to the default com.bitsycore.compose.sdl group; :desktop-native-window is the
|
||||
// headline app-shell artifact, published under com.bitsycore.compose.
|
||||
val kDefaultGroup = "com.bitsycore.compose.sdl"
|
||||
val kAreaGroups = mapOf(
|
||||
":ui" to "com.bitsycore.compose.ui",
|
||||
":ui-graphics" to "com.bitsycore.compose.ui",
|
||||
":ui-text" to "com.bitsycore.compose.ui",
|
||||
":ui-unit" to "com.bitsycore.compose.ui",
|
||||
":ui-geometry" to "com.bitsycore.compose.ui",
|
||||
":ui-util" to "com.bitsycore.compose.ui",
|
||||
":ui-backhandler" to "com.bitsycore.compose.ui",
|
||||
":ui-tooling-preview" to "com.bitsycore.compose.ui",
|
||||
":foundation" to "com.bitsycore.compose.foundation",
|
||||
":foundation-layout" to "com.bitsycore.compose.foundation",
|
||||
":animation" to "com.bitsycore.compose.animation",
|
||||
":animation-core" to "com.bitsycore.compose.animation",
|
||||
":animation-graphics" to "com.bitsycore.compose.animation",
|
||||
":material3" to "com.bitsycore.compose.material3",
|
||||
":material-ripple" to "com.bitsycore.compose.material",
|
||||
":components-resources" to "com.bitsycore.compose.components",
|
||||
":navigation3-ui" to "com.bitsycore.navigation3",
|
||||
":desktop-native-window" to "com.bitsycore.compose",
|
||||
)
|
||||
fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup
|
||||
|
||||
allprojects {
|
||||
group = "com.bitsycore.compose.sdl"
|
||||
group = groupFor(path)
|
||||
version = vPublishVersion
|
||||
}
|
||||
|
||||
@@ -37,12 +72,13 @@ allprojects {
|
||||
|
||||
val kAppModules = setOf(":demo", ":apidemo")
|
||||
val kPublishedLibs = setOf(
|
||||
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
|
||||
":ui-tooling-preview",
|
||||
":sdl-core",
|
||||
":ui", ":ui-util", ":ui-geometry", ":ui-graphics", ":ui-text",
|
||||
":ui-unit", ":ui-backhandler", ":ui-tooling-preview",
|
||||
":animation-core", ":animation", ":animation-graphics",
|
||||
":foundation", ":foundation-layout",
|
||||
":material3", ":material-ripple",
|
||||
":window", ":material-symbols",
|
||||
":desktop-native-window", ":material-symbols",
|
||||
":navigation3-ui", ":components-resources",
|
||||
)
|
||||
|
||||
@@ -76,7 +112,7 @@ subprojects {
|
||||
kPublishedLibs.forEach { modulePath ->
|
||||
val vArtifactId = modulePath.removePrefix(":")
|
||||
substitute(project(modulePath))
|
||||
.using(module("com.bitsycore.compose.sdl:$vArtifactId:$kConsumeVersion"))
|
||||
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
|
||||
.because("-PuseGithubPackages=true")
|
||||
}
|
||||
}
|
||||
@@ -105,6 +141,14 @@ subprojects {
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
maven {
|
||||
name = "Bitsycore"
|
||||
url = uri("https://maven.bitsycore.com/releases")
|
||||
credentials {
|
||||
username = System.getenv("BITSYCORE_MAVEN_USER")
|
||||
password = System.getenv("BITSYCORE_MAVEN_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
publications.withType<MavenPublication>().configureEach {
|
||||
pom {
|
||||
@@ -135,10 +179,11 @@ allprojects {
|
||||
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"))
|
||||
// ui-graphics / ui-text are their own modules (split out of :ui,
|
||||
// upstream layout). Declare each DIRECTLY so the app commonMain sees
|
||||
// them under the granular-metadata visibility rule.
|
||||
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui-graphics"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui-text"))
|
||||
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"))
|
||||
@@ -160,11 +205,11 @@ allprojects {
|
||||
}
|
||||
|
||||
// Whether the current host can build the mingwX64 target. Kotlin/Native can
|
||||
// only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf /
|
||||
// sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by
|
||||
// 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 `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h.
|
||||
// only cross-compile the mingwX64 sdl3 cinterop from a Windows host — it needs
|
||||
// the Windows SDL3 headers under libs/ (produced by 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 the sdl3 cinterop tries to
|
||||
// include SDL3's headers.
|
||||
// Override with `-PforceMingw=true` if you actually have the headers wired.
|
||||
val vHostSupportsMingw = System.getProperty("os.name").startsWith("Windows") ||
|
||||
(findProperty("forceMingw") as? String)?.toBoolean() == true
|
||||
|
||||
@@ -49,9 +49,11 @@ class ComposeFontBundlingConfig {
|
||||
var bundleMaterialSymbols: Boolean = false
|
||||
|
||||
/**
|
||||
* hb-subset each bundled Material Symbols font to the glyphs the sources reference
|
||||
* (scripts/subset-material-symbols.py + hb-subset; full font when hb-subset is
|
||||
* absent). Needs [bundleMaterialSymbols]; still gated by -PsubsetIcons.
|
||||
* Subset each bundled Material Symbols font to the glyphs the sources reference
|
||||
* (scripts/subset-material-symbols.py picks the codepoints; the subset task uses
|
||||
* hb-subset when present, else fontTools via Python — auto-installed — and only
|
||||
* bundles the full font if neither is available). Needs [bundleMaterialSymbols];
|
||||
* still gated by -PsubsetIcons.
|
||||
*/
|
||||
var enableIconSubsetting: Boolean = false
|
||||
}
|
||||
@@ -247,8 +249,10 @@ private fun Project.registerIconSubsetPipeline(inUsedStyles: List<String>): Map<
|
||||
}
|
||||
|
||||
/**
|
||||
* hb-subset inStyle's downloaded TTF to the codepoints in usage-codepoint.txt, into this
|
||||
* app's build/icons/. Bundles the full font instead when hb-subset is not on PATH.
|
||||
* Subset inStyle's downloaded TTF to the codepoints in usage-codepoint.txt, into this
|
||||
* app's build/icons/. Prefers hb-subset (harfbuzz); falls back to fontTools via Python
|
||||
* (variable-font-aware, auto-installed via pip) when hb-subset is absent; bundles the
|
||||
* full font only if neither subsetter is available.
|
||||
*/
|
||||
private fun Project.registerSubsetTask(
|
||||
inStyle: String,
|
||||
@@ -279,30 +283,67 @@ private fun Project.registerSubsetTask(
|
||||
vOut.parentFile.mkdirs()
|
||||
val vInputFile = vInputProvider.get().asFile
|
||||
val vBefore = vInputFile.length()
|
||||
// ProcessBuilder: project.exec isn't usable inside doLast on Gradle 9.
|
||||
// hb-subset is optional : without it, bundle the full font.
|
||||
val vProc = try {
|
||||
ProcessBuilder(
|
||||
"hb-subset",
|
||||
vInputFile.absolutePath,
|
||||
"-o", vOut.absolutePath,
|
||||
"--unicodes=$vUnicodes",
|
||||
).redirectErrorStream(true).start()
|
||||
} catch (_: java.io.IOException) {
|
||||
// project.exec isn't usable inside doLast on Gradle 9, so shell out via
|
||||
// ProcessBuilder. Returns (exitedZero, combinedOutput); a missing binary
|
||||
// (IOException) yields (false, message) so callers can fall through.
|
||||
fun runCmd(vararg inCmd: String): Pair<Boolean, String> =
|
||||
try {
|
||||
val vProc = ProcessBuilder(*inCmd).redirectErrorStream(true).start()
|
||||
val vText = vProc.inputStream.bufferedReader().readText()
|
||||
(vProc.waitFor() == 0) to vText
|
||||
} catch (e: java.io.IOException) {
|
||||
false to (e.message ?: "not found")
|
||||
}
|
||||
|
||||
fun logShrink(inTool: String) {
|
||||
val vAfter = vOut.length()
|
||||
val vPct = if (vBefore == 0L) 0 else (100 - 100 * vAfter / vBefore).coerceAtLeast(0)
|
||||
logger.lifecycle(
|
||||
"[subset $inStyle] ${vBefore / 1024}KB → ${vAfter / 1024}KB (-$vPct%) · " +
|
||||
"${vCodepoints.size} glyphs kept [$inTool]"
|
||||
)
|
||||
}
|
||||
|
||||
// 1. hb-subset (harfbuzz) — fastest path when it is on PATH.
|
||||
if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first
|
||||
&& vOut.length() > 0L
|
||||
) {
|
||||
logShrink("hb-subset"); return@doLast
|
||||
}
|
||||
|
||||
// 2. fontTools (pyftsubset) via Python — the portable fallback. Python is
|
||||
// already a build prerequisite (SDL build + vendor sync) and fontTools is
|
||||
// variable-font-aware, so the FILL/wght/GRAD/opsz axes survive. It is
|
||||
// auto-installed if missing, so no manual harfbuzz install is ever required.
|
||||
val vPython = listOf("python3", "python", "py").firstOrNull { runCmd(it, "--version").first }
|
||||
if (vPython != null) {
|
||||
if (!runCmd(vPython, "-c", "import fontTools").first) {
|
||||
logger.lifecycle("[subset $inStyle] fontTools not present — installing it via pip…")
|
||||
// --user (no root) → --break-system-packages (PEP 668) → plain.
|
||||
listOf(listOf("--user"), listOf("--break-system-packages"), emptyList<String>())
|
||||
.firstOrNull { vExtra ->
|
||||
runCmd(
|
||||
vPython, "-m", "pip", "install", "-q", "--disable-pip-version-check",
|
||||
*vExtra.toTypedArray(), "fonttools"
|
||||
).first
|
||||
}
|
||||
}
|
||||
val (vFtOk, vFtOut) = runCmd(
|
||||
vPython, "-m", "fontTools.subset", vInputFile.absolutePath,
|
||||
"--unicodes=$vUnicodes", "--output-file=${vOut.absolutePath}"
|
||||
)
|
||||
if (vFtOk && vOut.length() > 0L) { logShrink("fontTools"); return@doLast }
|
||||
logger.warn("[subset $inStyle] fontTools subset failed :\n$vFtOut")
|
||||
}
|
||||
|
||||
// 3. Last resort : the full font — correct, just larger.
|
||||
vInputFile.copyTo(vOut, overwrite = true)
|
||||
logger.warn(
|
||||
"[subset $inStyle] hb-subset not found on PATH : bundling the full font " +
|
||||
"(${vBefore / 1024}KB). Install harfbuzz to shrink it (brew install harfbuzz / " +
|
||||
"pacman -S mingw-w64-x86_64-harfbuzz / apt install harfbuzz-utils)."
|
||||
"[subset $inStyle] no working subsetter found (hb-subset or python3+fontTools) : " +
|
||||
"bundling the full font (${vBefore / 1024}KB). Install harfbuzz " +
|
||||
"(brew install harfbuzz / apt install harfbuzz-utils / " +
|
||||
"pacman -S mingw-w64-x86_64-harfbuzz) or put python3 on PATH."
|
||||
)
|
||||
return@doLast
|
||||
}
|
||||
val vOutput = vProc.inputStream.bufferedReader().readText()
|
||||
val vCode = vProc.waitFor()
|
||||
if (vCode != 0) throw GradleException("hb-subset failed (exit $vCode):\n$vOutput")
|
||||
val vAfter = vOut.length()
|
||||
val vPct = if (vBefore == 0L) 0 else ((100 - 100 * vAfter / vBefore)).coerceAtLeast(0)
|
||||
logger.lifecycle("[subset $inStyle] ${vBefore / 1024}KB → ${vAfter / 1024}KB (-$vPct%) · ${vCodepoints.size} glyphs kept")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:components-resources>
|
||||
// Library unique name: <com.bitsycore.compose.components: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]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte
|
||||
// upstream (painterResource / stringResource / Font / qualifiers / Res codegen
|
||||
// compatibility); the platform actuals are this port's: data.kres reading,
|
||||
// SDL3_image decoding, SDL locale/theme environment. Apps' JVM targets keep
|
||||
// using the official Maven artifact — this module is native-only.
|
||||
// image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM
|
||||
// targets keep using the official Maven artifact — this module is native-only.
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
@@ -17,7 +17,6 @@ repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
}
|
||||
|
||||
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
@@ -41,52 +40,11 @@ kotlin {
|
||||
}
|
||||
nativeMain {
|
||||
kotlin.srcDir("src/vendor/native/kotlin")
|
||||
}
|
||||
|
||||
// ============
|
||||
// Renderer roots — same conditional wiring as :ui: only the source
|
||||
// sets that will actually be attached are created.
|
||||
val sdlRendererMain = create("sdlRendererMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/vendor/sdlRenderer/kotlin")
|
||||
}
|
||||
if (vHostSupportsMingw) {
|
||||
val sdlRendererMingwMain = create("sdlRendererMingwMain") { dependsOn(sdlRendererMain) }
|
||||
get("mingwX64Main").dependsOn(sdlRendererMingwMain)
|
||||
}
|
||||
|
||||
val macosArm64Main = get("macosArm64Main")
|
||||
val linuxX64Main = get("linuxX64Main")
|
||||
val linuxArm64Main = get("linuxArm64Main")
|
||||
|
||||
if (useSdl3Everywhere) {
|
||||
val sdlRendererMacosMain = create("sdlRendererMacosMain") { dependsOn(sdlRendererMain) }
|
||||
val sdlRendererLinuxMain = create("sdlRendererLinuxMain") { dependsOn(sdlRendererMain) }
|
||||
macosArm64Main.dependsOn(sdlRendererMacosMain)
|
||||
linuxX64Main.dependsOn(sdlRendererLinuxMain)
|
||||
linuxArm64Main.dependsOn(sdlRendererLinuxMain)
|
||||
} else {
|
||||
// The Font/Image actuals for this module are pure project code
|
||||
// (:ui's IconFont / NamedFont / decodeEncodedImageBitmap in nativeMain)
|
||||
// so they work identically under either renderer. Only one renderer
|
||||
// source set is attached to a given target, so pointing skikoRenderer
|
||||
// at sdlRendererMain's srcDir doesn't cause duplicate actuals.
|
||||
// Upstream's skikoMain vendored files here referenced Compose-Desktop
|
||||
// Skia extensions (SystemFont / toComposeImageBitmap / nativeCanvas)
|
||||
// that this port's :ui doesn't expose — sharing the SDL actuals sidesteps
|
||||
// that gap entirely.
|
||||
val skikoRendererMain = create("skikoRendererMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/sdlRendererMain/kotlin")
|
||||
dependencies {
|
||||
implementation(libs.skiko)
|
||||
}
|
||||
}
|
||||
val skikoRendererMacosMain = create("skikoRendererMacosMain") { dependsOn(skikoRendererMain) }
|
||||
val skikoRendererLinuxMain = create("skikoRendererLinuxMain") { dependsOn(skikoRendererMain) }
|
||||
macosArm64Main.dependsOn(skikoRendererMacosMain)
|
||||
linuxX64Main.dependsOn(skikoRendererLinuxMain)
|
||||
linuxArm64Main.dependsOn(skikoRendererLinuxMain)
|
||||
// src/nativeMain/kotlin also holds the Font / Image resource actuals —
|
||||
// pure project code that delegates to the project font registry
|
||||
// (IconFont / NamedFont) and the :ui-graphics Skia decoder
|
||||
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
|
||||
// every target.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
# mingwX64/linux variants, so the runtime library is vendored and the platform
|
||||
# actuals are project code:
|
||||
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL),
|
||||
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser)
|
||||
# src/sdlRendererMain — image decode via the :ui SDL3_image hook, SVG unsupported,
|
||||
# Font via the project font registry
|
||||
# vendor/skikoRenderer — upstream skikoMain Skia-bound files, verbatim
|
||||
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
|
||||
# and the Font/Image resource actuals: renderer-agnostic
|
||||
# project code (image decode via the :ui-graphics Skia
|
||||
# decoder, Font via the project font registry)
|
||||
# Re-sync: python scripts/compose-fork/sync.py components/resources/library
|
||||
|
||||
SET_REPO=https://github.com/JetBrains/compose-multiplatform@<COMPOSE_REF>
|
||||
@@ -54,10 +54,9 @@ skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/
|
||||
|
||||
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia
|
||||
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
|
||||
# doesn't expose. Since the sdlRenderer actuals (src/sdlRendererMain/kotlin/) use
|
||||
# pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap) that work
|
||||
# on either renderer, the module's build shares those with skikoRendererMain
|
||||
# instead of vendoring the upstream files. Opt them out of the sync.
|
||||
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
|
||||
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
|
||||
# instead, so opt the upstream Skia-bound files out.
|
||||
!skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt
|
||||
!skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
|
||||
!skikoMain/kotlin/org/jetbrains/compose/resources/SvgPainter.kt
|
||||
|
||||
+3
-3
@@ -15,16 +15,16 @@ import com.compose.sdl.icons.IconFont
|
||||
import com.compose.sdl.text.NamedFont
|
||||
|
||||
// ==================
|
||||
// MARK: Font actuals — SDL renderer
|
||||
// MARK: Font actuals — Skia text stack via the project font registry
|
||||
// ==================
|
||||
|
||||
/** The SDL text stack resolves fonts by FAMILY NAME through the project font
|
||||
/** The Skia text stack resolves fonts by FAMILY NAME through the project font
|
||||
registry (IconFont.registerIcon — the general byte-font table, not just
|
||||
icons). A font resource therefore loads its bytes through the resource
|
||||
reader, registers them under a per-resource family name, and returns a
|
||||
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the
|
||||
standard project pipeline. Weight/style are carried on the NamedFont;
|
||||
variation axes apply at draw time via the renderer's axis support. */
|
||||
variation axes apply at draw time via Skia's axis support. */
|
||||
|
||||
private val emptyFont: Font = NamedFont("res-font:<loading>")
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
||||
|
||||
package org.jetbrains.compose.resources
|
||||
|
||||
import org.jetbrains.compose.resources.InternalResourceApi
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.compose.sdl.graphics.decodeEncodedImageBitmap
|
||||
import com.compose.sdl.graphics.decodeSvgAt
|
||||
import com.compose.sdl.graphics.svgIntrinsicSize
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// ==================
|
||||
// MARK: Image actuals — Skia decode via the :ui-graphics hook
|
||||
// ==================
|
||||
|
||||
/** Decoding goes through the :ui-graphics Skia hook the SDL backend registers
|
||||
at init (decodeEncodedImageBitmap). resourceDensity/targetDensity are ignored:
|
||||
under this port's Option-B density flow layout runs in physical pixels and
|
||||
drawables ship at a single density, so no decode-time rescale applies. */
|
||||
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
|
||||
decodeEncodedImageBitmap(this)
|
||||
?: error("Image decode failed — is the render backend initialised before painterResource ran?")
|
||||
|
||||
/** SVG element = the raw document bytes; rendering is size-driven (see SvgPainter). */
|
||||
internal actual class SvgElement(val bytes: ByteArray)
|
||||
|
||||
internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
|
||||
|
||||
/** Resolution-independent SVG painter: reports the document's intrinsic size for
|
||||
layout, but RE-RASTERISES the vector at the actual draw size each time the draw
|
||||
size changes (1-entry size cache), so a scaled-up SVG stays crisp instead of
|
||||
upscaling one intrinsic-size bitmap. Mirrors upstream desktop's SVGPainter. */
|
||||
internal actual fun SvgElement.toSvgPainter(density: Density): Painter = SvgPainter(bytes)
|
||||
|
||||
private class SvgPainter(private val bytes: ByteArray) : Painter() {
|
||||
|
||||
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
|
||||
|
||||
private var fAlpha: Float = 1f
|
||||
private var fColorFilter: ColorFilter? = null
|
||||
private var fCacheKey: Long = -1L
|
||||
private var fCacheBitmap: ImageBitmap? = null
|
||||
|
||||
override fun applyAlpha(alpha: Float): Boolean { fAlpha = alpha; return true }
|
||||
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { fColorFilter = colorFilter; return true }
|
||||
|
||||
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
||||
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
|
||||
if (vKey == fCacheKey && fCacheBitmap != null) return fCacheBitmap
|
||||
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
|
||||
fCacheKey = vKey
|
||||
fCacheBitmap = vBitmap
|
||||
return vBitmap
|
||||
}
|
||||
|
||||
override fun DrawScope.onDraw() {
|
||||
val vWpx = size.width.roundToInt()
|
||||
val vHpx = size.height.roundToInt()
|
||||
if (vWpx <= 0 || vHpx <= 0) return
|
||||
val vBitmap = rasterFor(vWpx, vHpx) ?: return
|
||||
drawImage(
|
||||
vBitmap,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = IntSize(vBitmap.width, vBitmap.height),
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = IntSize(vWpx, vHpx),
|
||||
alpha = fAlpha,
|
||||
colorFilter = fColorFilter,
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
-34
@@ -4,46 +4,24 @@ package org.jetbrains.compose.resources
|
||||
|
||||
import org.jetbrains.compose.resources.InternalResourceApi
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.get
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.pointed
|
||||
import kotlinx.cinterop.ptr
|
||||
import kotlinx.cinterop.toKString
|
||||
import kotlinx.cinterop.IntVar
|
||||
import kotlinx.cinterop.value
|
||||
import sdl3.SDL_GetPreferredLocales
|
||||
import sdl3.SDL_GetSystemTheme
|
||||
import sdl3.SDL_SystemTheme
|
||||
import sdl3.SDL_free
|
||||
import com.compose.sdl.res.preferredLocaleProvider
|
||||
import com.compose.sdl.res.systemThemeIsDarkProvider
|
||||
|
||||
// ==================
|
||||
// MARK: ResourceEnvironment — SDL actual
|
||||
// MARK: ResourceEnvironment — platform-env-seam actual
|
||||
// ==================
|
||||
|
||||
/** Non-composable system environment for qualifier resolution (values-fr,
|
||||
drawable-dark, …): locale from SDL_GetPreferredLocales, theme from
|
||||
SDL_GetSystemTheme. Density is reported as 1f — under this port's Option-B
|
||||
density flow layout runs in physical pixels and drawables are bundled at a
|
||||
single density, so the mdpi bucket is always the right one. The COMPOSABLE
|
||||
path (rememberResourceEnvironment) doesn't use this: it reads
|
||||
LocalDensity / isSystemInDarkTheme from the composition. */
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
drawable-dark, …): locale + theme come from the platform-env seams installed
|
||||
by the SDL layer, so :components-resources carries no dependency on the sdl3
|
||||
cinterop. Density is reported as 1f — under this port's Option-B density flow
|
||||
layout runs in physical pixels and drawables are bundled at a single density,
|
||||
so the mdpi bucket is always the right one. The COMPOSABLE path
|
||||
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
|
||||
isSystemInDarkTheme from the composition. */
|
||||
internal actual fun getSystemEnvironment(): ResourceEnvironment {
|
||||
var vLanguage = ""
|
||||
var vRegion = ""
|
||||
memScoped {
|
||||
val vCount = alloc<IntVar>()
|
||||
val vLocales = SDL_GetPreferredLocales(vCount.ptr)
|
||||
if (vLocales != null && vCount.value > 0) {
|
||||
val vFirst = vLocales[0]?.pointed
|
||||
vLanguage = vFirst?.language?.toKString() ?: ""
|
||||
vRegion = vFirst?.country?.toKString() ?: ""
|
||||
SDL_free(vLocales)
|
||||
}
|
||||
}
|
||||
val vDark = SDL_GetSystemTheme() == SDL_SystemTheme.SDL_SYSTEM_THEME_DARK
|
||||
val (vLanguage, vRegion) = preferredLocaleProvider?.invoke() ?: ("" to "")
|
||||
val vDark = systemThemeIsDarkProvider?.invoke() ?: false
|
||||
return ResourceEnvironment(
|
||||
language = LanguageQualifier(vLanguage),
|
||||
// SDL locales carry language+country only — no script subtag.
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
||||
|
||||
package org.jetbrains.compose.resources
|
||||
|
||||
import org.jetbrains.compose.resources.InternalResourceApi
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.Density
|
||||
import com.compose.sdl.graphics.decodeEncodedImageBitmap
|
||||
|
||||
// ==================
|
||||
// MARK: Image actuals — SDL renderer
|
||||
// ==================
|
||||
|
||||
/** Decoding goes through the :ui hook the SDL backend registers at init
|
||||
(SDL3_image, IMG auto-detect). resourceDensity/targetDensity are ignored:
|
||||
under this port's Option-B density flow layout runs in physical pixels and
|
||||
drawables ship at a single density, so no decode-time rescale applies. */
|
||||
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
|
||||
decodeEncodedImageBitmap(this)
|
||||
?: 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),
|
||||
so the "element" is just the raw bytes and the painter is the rasterised
|
||||
bitmap. No DOM, no vector scaling — prefer XML vector drawables for
|
||||
resolution-independent art on this renderer. */
|
||||
internal actual class SvgElement(val bytes: ByteArray)
|
||||
|
||||
internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
|
||||
|
||||
internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
|
||||
BitmapPainter(
|
||||
decodeEncodedImageBitmap(bytes)
|
||||
?: error("SVG rasterisation failed — is the render backend initialised before painterResource ran?")
|
||||
)
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:animation-core>
|
||||
// Library unique name: <com.bitsycore.compose.animation:animation-core>
|
||||
open annotation class androidx.compose.animation.core/ExperimentalAnimationSpecApi : kotlin/Annotation { // androidx.compose.animation.core/ExperimentalAnimationSpecApi|null[0]
|
||||
constructor <init>() // androidx.compose.animation.core/ExperimentalAnimationSpecApi.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||
|
||||
// :animation-core — androidx.compose.animation.core.* vendored VERBATIM from
|
||||
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
|
||||
// vector math). Split out of :core so :core stays about renderer+ui glue only.
|
||||
// vector math). Split out of :ui so :ui stays about renderer+ui glue only.
|
||||
//
|
||||
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and
|
||||
@@ -16,8 +16,7 @@ plugins {
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:animation-graphics>
|
||||
// Library unique name: <com.bitsycore.compose.animation:animation-graphics>
|
||||
open annotation class androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi : kotlin/Annotation { // androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi|null[0]
|
||||
constructor <init>() // androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:animation>
|
||||
// Library unique name: <com.bitsycore.compose.animation:animation>
|
||||
open annotation class androidx.compose.animation/ExperimentalAnimationApi : kotlin/Annotation { // androidx.compose.animation/ExperimentalAnimationApi|null[0]
|
||||
constructor <init>() // androidx.compose.animation/ExperimentalAnimationApi.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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:desktop-native-window>
|
||||
abstract interface com.compose.sdl/ApplicationScope { // com.compose.sdl/ApplicationScope|null[0]
|
||||
abstract fun exitApplication() // com.compose.sdl/ApplicationScope.exitApplication|exitApplication(){}[0]
|
||||
}
|
||||
|
||||
final class com.compose.sdl/AppWindowIcon { // com.compose.sdl/AppWindowIcon|null[0]
|
||||
constructor <init>(kotlin.collections/List<kotlin/String>, kotlin.collections/List<kotlin/String> = ...) // com.compose.sdl/AppWindowIcon.<init>|<init>(kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>){}[0]
|
||||
|
||||
final val dark // com.compose.sdl/AppWindowIcon.dark|{}dark[0]
|
||||
final fun <get-dark>(): kotlin.collections/List<kotlin/String> // com.compose.sdl/AppWindowIcon.dark.<get-dark>|<get-dark>(){}[0]
|
||||
final val light // com.compose.sdl/AppWindowIcon.light|{}light[0]
|
||||
final fun <get-light>(): kotlin.collections/List<kotlin/String> // com.compose.sdl/AppWindowIcon.light.<get-light>|<get-light>(){}[0]
|
||||
}
|
||||
|
||||
final val com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop|#static{}com_compose_sdl_AppWindowIcon$stableprop[0]
|
||||
|
||||
final var com.compose.sdl/disableInfiniteAnimations // com.compose.sdl/disableInfiniteAnimations|{}disableInfiniteAnimations[0]
|
||||
final fun <get-disableInfiniteAnimations>(): kotlin/Boolean // com.compose.sdl/disableInfiniteAnimations.<get-disableInfiniteAnimations>|<get-disableInfiniteAnimations>(){}[0]
|
||||
final fun <set-disableInfiniteAnimations>(kotlin/Boolean) // com.compose.sdl/disableInfiniteAnimations.<set-disableInfiniteAnimations>|<set-disableInfiniteAnimations>(kotlin.Boolean){}[0]
|
||||
final var com.compose.sdl/useVirtualFrameTime // com.compose.sdl/useVirtualFrameTime|{}useVirtualFrameTime[0]
|
||||
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
|
||||
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
|
||||
|
||||
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
|
||||
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
// :window — the module apps depend on. Owns nativeComposeWindow() (main loop,
|
||||
// :desktop-native-window — the module apps depend on. Owns nativeComposeWindow() (main loop,
|
||||
// recomposer lifecycle, event dispatch, Snapshot apply notifications).
|
||||
// Renderer selection happens entirely inside :core via source-set wiring
|
||||
// (skikoRendererMain vs sdlRendererMain) — this module just calls
|
||||
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :core and
|
||||
// Renderer selection happens entirely inside :ui via source-set wiring
|
||||
// (skikoRendererMain — Skia-only) — this module just calls
|
||||
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
|
||||
// the right symbol resolves per target.
|
||||
// Publication artifactId (when set up): compose-desktop-native.
|
||||
|
||||
@@ -23,15 +23,15 @@ kotlin {
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
// sdl3.* types are api-exposed via :ui's cinterop klib — no separate
|
||||
// sdl3 cinterop here.
|
||||
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib — no
|
||||
// separate sdl3 cinterop here.
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// api so apps depending on :window also get the compose re-impl,
|
||||
// Res/resources, GpuMode, and the renderer pipeline from :core, plus
|
||||
// api so apps depending on :desktop-native-window also get the compose re-impl,
|
||||
// Res/resources, GpuMode, and the renderer pipeline from :ui, plus
|
||||
// the foundation / animation-core / animation modules that were split
|
||||
// out of :core (upstream Compose layout).
|
||||
// out of :ui (upstream Compose layout).
|
||||
// Material widgets used to be re-exported from :material here; the
|
||||
// module was retired when :apidemo and :demo migrated to :material3.
|
||||
// Apps that want Material 3 widgets pull `implementation(project(":material3"))`
|
||||
+100
-213
@@ -22,10 +22,8 @@ import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.lifecycle.enableSavedStateHandles
|
||||
import com.compose.sdl.node.ComposeRootHost
|
||||
import com.compose.sdl.res.currentImageLoader
|
||||
import com.compose.sdl.text.currentTextMeasurer
|
||||
import com.compose.sdl.window.LocalPopupHost
|
||||
import com.compose.sdl.window.PopupLayer
|
||||
import com.compose.sdl.window.createPopupHostState
|
||||
@@ -36,7 +34,11 @@ import kotlinx.cinterop.toKString
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.ptr
|
||||
import kotlinx.cinterop.COpaquePointer
|
||||
import kotlinx.cinterop.pointed
|
||||
import sdl3.SDL_Delay
|
||||
import sdl3.SDL_GetCurrentDisplayMode
|
||||
import sdl3.SDL_GetDisplayForWindow
|
||||
import sdl3.SDL_GetPerformanceCounter
|
||||
import sdl3.SDL_GetPerformanceFrequency
|
||||
import sdl3.SDL_GetTicks
|
||||
@@ -65,7 +67,7 @@ import sdl3.SDL_WaitEventTimeout
|
||||
routes events per SDL window id.
|
||||
|
||||
Per-window pumping order matters: the render-bridge globals
|
||||
(currentTextMeasurer / currentImageLoader / viewport) are per-renderer, so the
|
||||
(currentImageLoader / viewport) are per-renderer, so the
|
||||
loop installs a window's globals before recomposing / laying out / drawing it.
|
||||
|
||||
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
|
||||
@@ -143,8 +145,12 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
val appScope = ApplicationScopeImpl(runtime)
|
||||
|
||||
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
|
||||
// Only SCHEDULE a frame here; the apply is coalesced to once per loop
|
||||
// iteration (top of loop + before each pump + before layout in
|
||||
// renderFrame) instead of walking every snapshot observer on every
|
||||
// individual state write. Mirrors upstream GlobalSnapshotManager,
|
||||
// which schedules rather than applying inline.
|
||||
runtime.markAllNeedFrame()
|
||||
Snapshot.sendApplyNotifications()
|
||||
}
|
||||
|
||||
appComposition.setContent { appScope.content() }
|
||||
@@ -217,6 +223,10 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
// Per-window pump + render.
|
||||
var vAnyRendered = false
|
||||
var vAllVsync = true
|
||||
// Non-vsync fallback cap: the shortest frame interval among the
|
||||
// rendered non-vsync windows' displays (60Hz default). Only used when
|
||||
// no window is vsync-paced (Software renderer / vsync unavailable).
|
||||
var vFallbackDelayMs = 16u
|
||||
val vAppPending = appRecomposer.hasPendingWork
|
||||
for (vW in runtime.windows.toList()) {
|
||||
vW.installGlobals()
|
||||
@@ -229,7 +239,10 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
if (vW.shouldRender()) {
|
||||
vW.renderFrame()
|
||||
vAnyRendered = true
|
||||
if (!vW.backend.vsyncEnabled) vAllVsync = false
|
||||
if (!vW.backend.vsyncEnabled) {
|
||||
vAllVsync = false
|
||||
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
|
||||
}
|
||||
}
|
||||
FrameProfiler.phase("render")
|
||||
}
|
||||
@@ -247,12 +260,19 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
// ============
|
||||
// Pace / idle-skip.
|
||||
if (vAnyRendered) {
|
||||
SDL_Delay(if (vAllVsync) 1u else 16u)
|
||||
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
|
||||
} else if (!vAppPending) {
|
||||
// Nothing invalidated anywhere: block for events instead of
|
||||
// spinning. Keep FPS windows anchored to active periods.
|
||||
SDL_WaitEventTimeout(null, 10)
|
||||
// spinning. A real input/redraw event wakes this immediately; the
|
||||
// timeout only bounds how often async work (dispatcher/timers) is
|
||||
// re-checked while truly idle, so keep it coarse to cut wakeups.
|
||||
SDL_WaitEventTimeout(null, 100)
|
||||
for (vW in runtime.windows) vW.resetFpsWindow()
|
||||
} else {
|
||||
// App composition has pending work but nothing rendered this
|
||||
// iteration — yield briefly instead of spinning at full speed
|
||||
// until the composition settles into a renderable state.
|
||||
SDL_Delay(1u)
|
||||
}
|
||||
|
||||
// ============
|
||||
@@ -291,79 +311,6 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** CDN_FORCERENDER=1 forces every frame to render so sustained steady-state
|
||||
timings can be measured on otherwise-idle screens (see TOOLING.md, frame
|
||||
profiler). */
|
||||
private val kForceRender: Boolean = platform.posix.getenv("CDN_FORCERENDER") != null
|
||||
|
||||
// ==================
|
||||
// MARK: Probe / screenshot support (render-to-quiescence)
|
||||
// ==================
|
||||
@@ -420,6 +367,18 @@ fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: f
|
||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
||||
|
||||
/** Frame interval (ms) of the window's current display, for the non-vsync
|
||||
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
|
||||
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
private fun displayFrameDelayMs(window: COpaquePointer?): UInt {
|
||||
val vWindow = window ?: return 16u
|
||||
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
|
||||
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
|
||||
val vHz = vMode.pointed.refresh_rate
|
||||
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
|
||||
}
|
||||
|
||||
/** Single-window compatibility wrapper — the pre-multi-window entry point.
|
||||
Closing the window exits the app, exactly as before. */
|
||||
fun nativeComposeWindow(
|
||||
@@ -530,12 +489,15 @@ internal class WindowInstance(
|
||||
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
|
||||
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
|
||||
) {
|
||||
private val gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||
val backend = SDL3Backend(
|
||||
initialTitle, inWidth, inHeight, gpuMode = gpuMode,
|
||||
iconLightResourcePaths = inIcon?.light ?: emptyList(),
|
||||
iconDarkResourcePaths = inIcon?.dark ?: emptyList(),
|
||||
)
|
||||
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
|
||||
// a Skia GPU context can't be created (RDP / headless / missing GL driver).
|
||||
private var gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||
private val initialWidth = inWidth
|
||||
private val initialHeight = inHeight
|
||||
private val icon = inIcon
|
||||
// Assigned by init() from the first backend that comes up (see makeBackend()).
|
||||
lateinit var backend: SDL3Backend
|
||||
private set
|
||||
private var renderBackend: RenderBackend? = null
|
||||
lateinit var host: ComposeRootHost
|
||||
private set
|
||||
@@ -606,14 +568,42 @@ internal class WindowInstance(
|
||||
needsFrame = true
|
||||
}
|
||||
|
||||
fun init(inScope: CoroutineScope): Boolean {
|
||||
if (!backend.init()) return false
|
||||
backend.updateWindowSize()
|
||||
// Creates + initialises an SDL3Backend for [mode] and its RenderBackend. On
|
||||
// success commits `backend` and returns the renderer; else tears everything
|
||||
// down and returns null so the caller can retry with a different mode.
|
||||
private fun makeBackend(mode: GpuMode): RenderBackend? {
|
||||
val vBackend = SDL3Backend(
|
||||
initialTitle, initialWidth, initialHeight, gpuMode = mode,
|
||||
iconLightResourcePaths = icon?.light ?: emptyList(),
|
||||
iconDarkResourcePaths = icon?.dark ?: emptyList(),
|
||||
)
|
||||
if (!vBackend.init()) {
|
||||
vBackend.destroy(inQuitSdl = false)
|
||||
return null
|
||||
}
|
||||
vBackend.updateWindowSize()
|
||||
val vRender = createRenderBackend(vBackend, mode)
|
||||
if (vRender == null || !vRender.ensureSize(vBackend.pixelWidth, vBackend.pixelHeight)) {
|
||||
vRender?.destroy()
|
||||
vBackend.destroy(inQuitSdl = false)
|
||||
return null
|
||||
}
|
||||
backend = vBackend
|
||||
return vRender
|
||||
}
|
||||
|
||||
val vRender = createRenderBackend(backend, gpuMode)
|
||||
if (vRender == null || !vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)) {
|
||||
println("Failed to init render backend for $gpuMode")
|
||||
backend.destroy(inQuitSdl = false)
|
||||
fun init(inScope: CoroutineScope): Boolean {
|
||||
// Try the resolved gpuMode; if it's a Skia GPU mode whose context/bridge
|
||||
// can't come up (RDP / headless / missing GL driver), fall back once to
|
||||
// CPU raster (Software) so the window still opens instead of failing.
|
||||
var vRender = makeBackend(gpuMode)
|
||||
if (vRender == null && gpuMode is GpuMode.Skia) {
|
||||
println("GPU renderer ($gpuMode) unavailable — falling back to CPU raster (Software)")
|
||||
gpuMode = GpuMode.Software
|
||||
vRender = makeBackend(gpuMode)
|
||||
}
|
||||
if (vRender == null) {
|
||||
println("Failed to init render backend")
|
||||
return false
|
||||
}
|
||||
renderBackend = vRender
|
||||
@@ -740,13 +730,18 @@ internal class WindowInstance(
|
||||
measuring, or drawing this window's tree. */
|
||||
fun installGlobals() {
|
||||
val vRender = renderBackend ?: return
|
||||
currentTextMeasurer = vRender.textMeasurer
|
||||
currentImageLoader = vRender.imageLoader
|
||||
com.compose.sdl.text.currentViewportWidth = backend.pixelWidth
|
||||
com.compose.sdl.text.currentViewportHeight = backend.pixelHeight
|
||||
// Register bundled generic fonts (FontFamily.Monospace → NotoSansMono) once
|
||||
// data.kres is loadable; idempotent, no-op if the font isn't bundled.
|
||||
com.compose.sdl.text.registerGenericFonts()
|
||||
// Position the OS IME candidate window the moment a field in THIS window
|
||||
// gains focus, not only on the first TEXT_EDITING event (repointed per
|
||||
// window here so multi-window focus targets the right SDL window).
|
||||
com.compose.sdl.text.input.ImeBridge.onSessionActiveChange = { active ->
|
||||
if (active) updateImeArea()
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
@@ -768,8 +763,8 @@ internal class WindowInstance(
|
||||
// SDL3 delivers mouse coords in logical points on HiDPI — multiply by
|
||||
// DPR so hit-testing lands in the pixel space layout uses.
|
||||
val vDpr = backend.pixelDensity
|
||||
val vPx = inEvent.event.x.toFloat() * vDpr
|
||||
val vPy = inEvent.event.y.toFloat() * vDpr
|
||||
val vPx = inEvent.event.x * vDpr
|
||||
val vPy = inEvent.event.y * vDpr
|
||||
if (inEvent.event.type == PointerEventType.Press) {
|
||||
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
||||
}
|
||||
@@ -794,7 +789,7 @@ internal class WindowInstance(
|
||||
needsFrame = true
|
||||
installGlobals()
|
||||
val vDpr = backend.pixelDensity
|
||||
host.onWheel(inEvent.x.toFloat() * vDpr, inEvent.y.toFloat() * vDpr, inEvent.deltaX, inEvent.deltaY, SDL_GetTicks().toLong())
|
||||
host.onWheel(inEvent.x * vDpr, inEvent.y * vDpr, inEvent.deltaX, inEvent.deltaY, SDL_GetTicks().toLong())
|
||||
}
|
||||
|
||||
fun onKeyEvent(inEvent: AppEvent.Key) {
|
||||
@@ -907,6 +902,11 @@ internal class WindowInstance(
|
||||
|
||||
backend.updateWindowSize()
|
||||
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
||||
// Charge the GPU back-buffer acquire to its own phase. On Metal this is
|
||||
// where nextDrawable() blocks on vsync (the natural frame pacing), so
|
||||
// folding it into "layout" made the profiler read the vsync wait as
|
||||
// layout cost — a ~6-7ms phantom that hid the real (tiny) layout time.
|
||||
FrameProfiler.phase(" acquire")
|
||||
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
||||
// Deliver any state written by this iteration's frame-clock continuations
|
||||
// (withFrameNanos animations — notably smooth wheel scrolling) BEFORE we lay
|
||||
@@ -985,118 +985,5 @@ internal class WindowInstance(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: WindowArchitectureOwner
|
||||
// ==================
|
||||
|
||||
/** Per-window architecture-components owner, modeled on upstream desktop's
|
||||
DefaultArchitectureComponentsOwner (compose/ui skikoMain
|
||||
PlatformOwnerProvider.skiko.kt): one object implements LifecycleOwner +
|
||||
ViewModelStoreOwner + SavedStateRegistryOwner (+ the SavedState-aware
|
||||
default ViewModel factory), so viewModel(), SavedStateHandle and
|
||||
rememberSaveable-backed registries all resolve against the WINDOW scope.
|
||||
|
||||
The lifecycle registry uses createUnsafe (no main-thread enforcement) —
|
||||
same as the root owner this replaces; the SDL loop is single-threaded
|
||||
anyway. RESUMED from construction; destroy() moves to DESTROYED and clears
|
||||
the ViewModelStore (onCleared runs). SavedState restores from nothing (no
|
||||
process-death persistence on desktop — upstream desktop passes null too). */
|
||||
private class WindowArchitectureOwner :
|
||||
androidx.lifecycle.LifecycleOwner,
|
||||
androidx.lifecycle.ViewModelStoreOwner,
|
||||
androidx.lifecycle.HasDefaultViewModelProviderFactory,
|
||||
androidx.savedstate.SavedStateRegistryOwner {
|
||||
|
||||
override val lifecycle = androidx.lifecycle.LifecycleRegistry.createUnsafe(this)
|
||||
override val viewModelStore = androidx.lifecycle.ViewModelStore()
|
||||
|
||||
private val savedStateController = androidx.savedstate.SavedStateRegistryController.create(this)
|
||||
override val savedStateRegistry: androidx.savedstate.SavedStateRegistry
|
||||
get() = savedStateController.savedStateRegistry
|
||||
|
||||
override val defaultViewModelProviderFactory = androidx.lifecycle.SavedStateViewModelFactory()
|
||||
override val defaultViewModelCreationExtras: androidx.lifecycle.viewmodel.CreationExtras
|
||||
get() = androidx.lifecycle.viewmodel.MutableCreationExtras().also {
|
||||
it[androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY] = this
|
||||
it[androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY] = this
|
||||
}
|
||||
|
||||
init {
|
||||
savedStateController.performAttach()
|
||||
savedStateController.performRestore(null)
|
||||
// SavedStateHandle support for WINDOW-scoped ViewModels — must run while
|
||||
// the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer
|
||||
// calls this at the same point. With it, `viewModel { ... }` against the
|
||||
// window owner (the activityViewModels() analog) can take a
|
||||
// SavedStateHandle instead of needing a saved-state-less child owner.
|
||||
enableSavedStateHandles()
|
||||
// CREATED (not RESUMED) until the first composition is done — code that
|
||||
// runs enableSavedStateHandles() during composition (nav3's decorators,
|
||||
// rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and
|
||||
// upstream desktop windows likewise compose first and resume after.
|
||||
lifecycle.currentState = androidx.lifecycle.Lifecycle.State.CREATED
|
||||
}
|
||||
|
||||
/** Focus/visibility-driven state (see WindowInstance.onActivationEvent).
|
||||
Ignored once destroyed — a stray SDL event during teardown must not
|
||||
resurrect the registry. */
|
||||
fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) {
|
||||
if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED &&
|
||||
lifecycle.currentState != inState
|
||||
) {
|
||||
lifecycle.currentState = inState
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
lifecycle.currentState = androidx.lifecycle.Lifecycle.State.DESTROYED
|
||||
viewModelStore.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: Input helpers
|
||||
// ==================
|
||||
|
||||
/** Escape → back: port of upstream desktop's BackNavigationEventInput. An
|
||||
unconsumed Escape KeyDown completes a back navigation on the dispatcher
|
||||
this input is registered with (BackHandler / PredictiveBackHandler
|
||||
consumers: m3 SearchBar collapse, dialog dismissal, …). */
|
||||
private class BackNavigationInput : androidx.navigationevent.NavigationEventInput() {
|
||||
fun onKeyEvent(inEvent: androidx.compose.ui.input.key.KeyEvent): Boolean {
|
||||
if (inEvent.type == androidx.compose.ui.input.key.KeyEventType.KeyDown &&
|
||||
inEvent.key == androidx.compose.ui.input.key.Key.Escape
|
||||
) {
|
||||
dispatchOnBackCompleted()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-dispatches committed text (SDL TEXT_INPUT) as one synthetic typed
|
||||
KeyDown per Unicode codepoint (surrogate pairs folded). Key.Unknown +
|
||||
codePoint + no modifiers matches both vendored text stacks' isTypedEvent
|
||||
criteria; the SDL key mapper leaves codePoint = 0 on real key events, so
|
||||
the physical KeyDown and the synthetic one can never double-insert. */
|
||||
private fun dispatchTypedText(inHost: ComposeRootHost, inText: String) {
|
||||
var vI = 0
|
||||
while (vI < inText.length) {
|
||||
val vHigh = inText[vI]
|
||||
val vCodepoint: Int
|
||||
if (vHigh.isHighSurrogate() && vI + 1 < inText.length && inText[vI + 1].isLowSurrogate()) {
|
||||
vCodepoint = 0x10000 + ((vHigh.code - 0xD800) shl 10) + (inText[vI + 1].code - 0xDC00)
|
||||
vI += 2
|
||||
} else {
|
||||
vCodepoint = vHigh.code
|
||||
vI += 1
|
||||
}
|
||||
inHost.dispatchKeyEvent(
|
||||
androidx.compose.ui.input.key.KeyEvent(
|
||||
key = androidx.compose.ui.input.key.Key.Unknown,
|
||||
type = androidx.compose.ui.input.key.KeyEventType.KeyDown,
|
||||
codePoint = vCodepoint,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
// WindowArchitectureOwner -> WindowArchitectureOwner.kt
|
||||
// BackNavigationInput / dispatchTypedText -> WindowInputHelpers.kt
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.compose.sdl
|
||||
|
||||
import kotlinx.cinterop.toKString
|
||||
import sdl3.SDL_GetPerformanceCounter
|
||||
import sdl3.SDL_GetPerformanceFrequency
|
||||
import sdl3.SDL_GetTicks
|
||||
|
||||
// ==================
|
||||
// MARK: FrameProfiler
|
||||
// ==================
|
||||
|
||||
/** 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"
|
||||
}
|
||||
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + "\n"
|
||||
val vFile = platform.posix.fopen(fPath, "a")
|
||||
if (vFile != null) {
|
||||
platform.posix.fputs(vLine, vFile)
|
||||
platform.posix.fclose(vFile)
|
||||
}
|
||||
fSum.clear(); fMax.clear()
|
||||
fFrames = 0
|
||||
fLastPrintMs = vNowMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** CDN_FORCERENDER=1 forces every frame to render so sustained steady-state
|
||||
timings can be measured on otherwise-idle screens (see TOOLING.md, frame
|
||||
profiler). */
|
||||
internal val kForceRender: Boolean = platform.posix.getenv("CDN_FORCERENDER") != null
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.compose.sdl
|
||||
|
||||
import androidx.lifecycle.enableSavedStateHandles
|
||||
|
||||
// ==================
|
||||
// MARK: WindowArchitectureOwner
|
||||
// ==================
|
||||
|
||||
/** Per-window architecture-components owner, modeled on upstream desktop's
|
||||
DefaultArchitectureComponentsOwner (compose/ui skikoMain
|
||||
PlatformOwnerProvider.skiko.kt): one object implements LifecycleOwner +
|
||||
ViewModelStoreOwner + SavedStateRegistryOwner (+ the SavedState-aware
|
||||
default ViewModel factory), so viewModel(), SavedStateHandle and
|
||||
rememberSaveable-backed registries all resolve against the WINDOW scope.
|
||||
|
||||
The lifecycle registry uses createUnsafe (no main-thread enforcement) —
|
||||
same as the root owner this replaces; the SDL loop is single-threaded
|
||||
anyway. RESUMED from construction; destroy() moves to DESTROYED and clears
|
||||
the ViewModelStore (onCleared runs). SavedState restores from nothing (no
|
||||
process-death persistence on desktop — upstream desktop passes null too). */
|
||||
internal class WindowArchitectureOwner :
|
||||
androidx.lifecycle.LifecycleOwner,
|
||||
androidx.lifecycle.ViewModelStoreOwner,
|
||||
androidx.lifecycle.HasDefaultViewModelProviderFactory,
|
||||
androidx.savedstate.SavedStateRegistryOwner {
|
||||
|
||||
override val lifecycle = androidx.lifecycle.LifecycleRegistry.createUnsafe(this)
|
||||
override val viewModelStore = androidx.lifecycle.ViewModelStore()
|
||||
|
||||
private val savedStateController = androidx.savedstate.SavedStateRegistryController.create(this)
|
||||
override val savedStateRegistry: androidx.savedstate.SavedStateRegistry
|
||||
get() = savedStateController.savedStateRegistry
|
||||
|
||||
override val defaultViewModelProviderFactory = androidx.lifecycle.SavedStateViewModelFactory()
|
||||
override val defaultViewModelCreationExtras: androidx.lifecycle.viewmodel.CreationExtras
|
||||
get() = androidx.lifecycle.viewmodel.MutableCreationExtras().also {
|
||||
it[androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY] = this
|
||||
it[androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY] = this
|
||||
}
|
||||
|
||||
init {
|
||||
savedStateController.performAttach()
|
||||
savedStateController.performRestore(null)
|
||||
// SavedStateHandle support for WINDOW-scoped ViewModels — must run while
|
||||
// the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer
|
||||
// calls this at the same point. With it, `viewModel { ... }` against the
|
||||
// window owner (the activityViewModels() analog) can take a
|
||||
// SavedStateHandle instead of needing a saved-state-less child owner.
|
||||
enableSavedStateHandles()
|
||||
// CREATED (not RESUMED) until the first composition is done — code that
|
||||
// runs enableSavedStateHandles() during composition (nav3's decorators,
|
||||
// rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and
|
||||
// upstream desktop windows likewise compose first and resume after.
|
||||
lifecycle.currentState = androidx.lifecycle.Lifecycle.State.CREATED
|
||||
}
|
||||
|
||||
/** Focus/visibility-driven state (see WindowInstance.onActivationEvent).
|
||||
Ignored once destroyed — a stray SDL event during teardown must not
|
||||
resurrect the registry. */
|
||||
fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) {
|
||||
if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED &&
|
||||
lifecycle.currentState != inState
|
||||
) {
|
||||
lifecycle.currentState = inState
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
lifecycle.currentState = androidx.lifecycle.Lifecycle.State.DESTROYED
|
||||
viewModelStore.clear()
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
@file:OptIn(androidx.compose.ui.InternalComposeUiApi::class)
|
||||
|
||||
package com.compose.sdl
|
||||
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import com.compose.sdl.node.ComposeRootHost
|
||||
|
||||
// ==================
|
||||
// MARK: Input helpers
|
||||
// ==================
|
||||
|
||||
/** Escape → back: port of upstream desktop's BackNavigationEventInput. An
|
||||
unconsumed Escape KeyDown completes a back navigation on the dispatcher
|
||||
this input is registered with (BackHandler / PredictiveBackHandler
|
||||
consumers: m3 SearchBar collapse, dialog dismissal, …). */
|
||||
internal class BackNavigationInput : androidx.navigationevent.NavigationEventInput() {
|
||||
fun onKeyEvent(inEvent: androidx.compose.ui.input.key.KeyEvent): Boolean {
|
||||
if (inEvent.type == androidx.compose.ui.input.key.KeyEventType.KeyDown &&
|
||||
inEvent.key == androidx.compose.ui.input.key.Key.Escape
|
||||
) {
|
||||
dispatchOnBackCompleted()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-dispatches committed text (SDL TEXT_INPUT) as one synthetic typed
|
||||
KeyDown per Unicode codepoint (surrogate pairs folded). Key.Unknown +
|
||||
codePoint + no modifiers matches both vendored text stacks' isTypedEvent
|
||||
criteria; the SDL key mapper leaves codePoint = 0 on real key events, so
|
||||
the physical KeyDown and the synthetic one can never double-insert. */
|
||||
internal fun dispatchTypedText(inHost: ComposeRootHost, inText: String) {
|
||||
var vI = 0
|
||||
while (vI < inText.length) {
|
||||
val vHigh = inText[vI]
|
||||
val vCodepoint: Int
|
||||
if (vHigh.isHighSurrogate() && vI + 1 < inText.length && inText[vI + 1].isLowSurrogate()) {
|
||||
vCodepoint = 0x10000 + ((vHigh.code - 0xD800) shl 10) + (inText[vI + 1].code - 0xDC00)
|
||||
vI += 2
|
||||
} else {
|
||||
vCodepoint = vHigh.code
|
||||
vI += 1
|
||||
}
|
||||
inHost.dispatchKeyEvent(
|
||||
androidx.compose.ui.input.key.KeyEvent(
|
||||
key = androidx.compose.ui.input.key.Key.Unknown,
|
||||
type = androidx.compose.ui.input.key.KeyEventType.KeyDown,
|
||||
codePoint = vCodepoint,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:foundation-layout>
|
||||
// Library unique name: <com.bitsycore.compose.foundation:foundation-layout>
|
||||
open annotation class androidx.compose.foundation.layout/ExperimentalFlexBoxApi : kotlin/Annotation { // androidx.compose.foundation.layout/ExperimentalFlexBoxApi|null[0]
|
||||
constructor <init>() // androidx.compose.foundation.layout/ExperimentalFlexBoxApi.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:foundation>
|
||||
// Library unique name: <com.bitsycore.compose.foundation:foundation>
|
||||
open annotation class androidx.compose.foundation.lazy.grid/LazyGridScopeMarker : kotlin/Annotation { // androidx.compose.foundation.lazy.grid/LazyGridScopeMarker|null[0]
|
||||
constructor <init>() // androidx.compose.foundation.lazy.grid/LazyGridScopeMarker.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||
|
||||
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.*
|
||||
// (non-core) vendored VERBATIM from upstream. Sits on :core (which still owns
|
||||
// androidx.compose.ui.*, both renderers, cinterops) + :animation-core.
|
||||
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
|
||||
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
|
||||
//
|
||||
// Why the two upstream modules (`:animation` and `:foundation`) are combined
|
||||
// here: upstream splits `foundation-layout` off from `foundation`, then has
|
||||
@@ -20,9 +20,9 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
|
||||
// and its actual lives here as a plain nativeMain kt file (see
|
||||
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single
|
||||
// stub returning false. The upstream skiko/sdl per-renderer actuals were dropped
|
||||
// when :foundation was split from :core: expect/actual must live in the same
|
||||
// module, and duplicating the skikoRenderer/sdlRenderer source-set hierarchy on
|
||||
// stub returning false. The upstream skiko per-renderer actuals were dropped
|
||||
// when :foundation was split from :ui: expect/actual must live in the same
|
||||
// module, and duplicating the skikoRenderer source-set hierarchy on
|
||||
// :foundation just to read `LocalSystemTheme` on the Skia path was more build
|
||||
// wiring than is worth it for one function no in-repo app calls.
|
||||
//
|
||||
@@ -34,8 +34,7 @@ plugins {
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -517,6 +517,11 @@ skikoMain/kotlin/ -> src/vendor/native/kotlin/
|
||||
!skikoMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.skiko.kt
|
||||
!skikoMain/kotlin/androidx/compose/foundation/Clickable.skiko.kt
|
||||
!skikoMain/kotlin/androidx/compose/foundation/DarkTheme.skiko.kt
|
||||
# Scrollbar.skiko.kt + v2/Scrollbar.skiko.kt are MANUALLY VENDORED to
|
||||
# src/nativeMain/.../foundation/{,v2}/ (VENDOR-BASE headers there): the flat
|
||||
# native source set can't hold the v2 `expect fun runBlockingIfPossible` and its
|
||||
# actual together, so it's inlined to runBlocking. Refused here so sync doesn't
|
||||
# shadow the hand-vendored copies.
|
||||
!skikoMain/kotlin/androidx/compose/foundation/Scrollbar.skiko.kt
|
||||
!skikoMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.skiko.kt
|
||||
!skikoMain/kotlin/androidx/compose/foundation/text/CupertinoTextFieldPointerModifier.skiko.kt
|
||||
|
||||
-342
@@ -1,342 +0,0 @@
|
||||
package com.compose.sdl.scrollbar
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter =
|
||||
remember(scrollState) { ScrollStateScrollbarAdapter(scrollState) }
|
||||
|
||||
private class ScrollStateScrollbarAdapter(private val state: ScrollState) : ScrollbarAdapter {
|
||||
override val scrollOffset: Double get() = state.value.toDouble()
|
||||
override val contentSize: Double get() = state.contentSize.toDouble()
|
||||
override val viewportSize: Double get() = state.viewportSize.toDouble()
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
// Scrollbar thumb drag → instant scroll-to. dispatchRawDelta takes a
|
||||
// delta in pixels; convert the target offset into a delta from where
|
||||
// we are now (clamping at the scroll edges is the dispatchRawDelta
|
||||
// contract, so we don't have to coerce here).
|
||||
state.dispatchRawDelta((scrollOffset - state.value.toDouble()).toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)
|
||||
+20
-39
@@ -1,38 +1,31 @@
|
||||
package com.compose.sdl.text
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontVariation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
// ==================
|
||||
// MARK: IconText — project-only text composable for icon fonts
|
||||
// MARK: IconText — icon-font text on the standard text path
|
||||
// ==================
|
||||
|
||||
/**
|
||||
A minimal project text composable that renders a codepoint through a named
|
||||
icon font with optional variable-font axis settings (Material Symbols).
|
||||
icon font with optional variable-font axis settings (Material Symbols FILL /
|
||||
wght / GRAD / opsz).
|
||||
|
||||
Sits outside `androidx.compose.foundation.text.BasicText` so BasicText can
|
||||
remain a byte-identical match of upstream. Icons need:
|
||||
- `fontFamily: String` — the registered icon font family name (project's
|
||||
`FontFamily.Named`, not upstream `FontFamily.Resolver`).
|
||||
- `fontVariationSettings` — per-usage variable-font axis values (FILL,
|
||||
wght, GRAD, opsz for Material Symbols).
|
||||
Now just a [BasicText]: the icon font family and its variable axes are threaded
|
||||
through [namedFontFamily] (`axes = …`), which the skiko text engine reads via
|
||||
`FontFamily.projectFontVariations()` and applies to the typeface. So icons
|
||||
measure + draw through the same skiko `skparagraph` path as ordinary text — no
|
||||
separate renderer/measurer seam.
|
||||
|
||||
Upstream `TextStyle` has neither: fontFamily routes through the
|
||||
`FontFamily.Resolver`, and axis values are set per-Font at construction
|
||||
time. Rather than fight that abstraction for one composable, icons use
|
||||
this project path — the TextDrawElement modifier feeds directly into
|
||||
SdlParagraph / Sdl3Canvas.drawNativeText.
|
||||
|
||||
Material `Icon(codepoint = ..., fontFamily = ...)` uses this. Everything
|
||||
else (Text, BasicText, TextField) goes through the upstream-shaped path.
|
||||
Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText /
|
||||
TextField go through the upstream-shaped path already.
|
||||
*/
|
||||
@Composable
|
||||
fun IconText(
|
||||
@@ -44,28 +37,16 @@ fun IconText(
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
fontVariationSettings: List<FontVariation.Setting>? = null,
|
||||
) {
|
||||
// Layout runs in physical pixels (LocalDensity = DPR), so the icon font
|
||||
// size must also convert `sp → px`. Matches SdlParagraph's `fontSize.value
|
||||
// * density`. Without this the icons render at half size on Retina.
|
||||
val vDensity = LocalDensity.current.density
|
||||
val vFontPx = (fontSize.value * vDensity).toInt().coerceAtLeast(1)
|
||||
Box(modifier = modifier) {
|
||||
androidx.compose.ui.layout.Layout(
|
||||
modifier = TextDrawElement(
|
||||
BasicText(
|
||||
text = text,
|
||||
spans = null,
|
||||
modifier = modifier,
|
||||
style = TextStyle(
|
||||
color = if (color == Color.Unspecified) Color.Black else color,
|
||||
fontSizePx = vFontPx,
|
||||
fontSize = fontSize,
|
||||
fontFamily = namedFontFamily(fontFamily, axes = fontVariationSettings),
|
||||
textAlign = textAlign,
|
||||
softWrap = false,
|
||||
fontFamily = fontFamily,
|
||||
fontVariations = fontVariationSettings,
|
||||
),
|
||||
) { _, constraints ->
|
||||
val vSize = currentTextMeasurer.measure(text, vFontPx, Int.MAX_VALUE, fontFamily, fontVariationSettings)
|
||||
val w = vSize.width.coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
val h = vSize.height.coerceIn(constraints.minHeight, constraints.maxHeight)
|
||||
layout(w, h) {}
|
||||
}
|
||||
}
|
||||
softWrap = false,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-3
@@ -2,9 +2,10 @@ package androidx.compose.foundation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import sdl3.SDL_GetSystemTheme
|
||||
import sdl3.SDL_SystemTheme
|
||||
import com.compose.sdl.res.systemThemeIsDarkProvider
|
||||
|
||||
// System dark-theme flag via the platform-env seam (installed by the SDL layer),
|
||||
// so :foundation carries no dependency on the sdl3 cinterop.
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
internal actual fun _isSystemInDarkTheme(): Boolean = SDL_GetSystemTheme() == SDL_SystemTheme.SDL_SYSTEM_THEME_DARK
|
||||
internal actual fun _isSystemInDarkTheme(): Boolean = systemThemeIsDarkProvider?.invoke() ?: false
|
||||
|
||||
+1191
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -15,7 +15,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
/**
|
||||
Byte-close mirror of upstream macosMain — desktop tap / selection gestures
|
||||
delegate to the default helpers baked into TextFieldSelectionState.
|
||||
addBasicTextFieldTextContextMenuComponents = no-op (TODO CMP-7819).
|
||||
addBasicTextFieldTextContextMenuComponents is a NO-OP by design: it belongs to
|
||||
the NEW text-context-menu API (ComposeFoundationFlags.isNewContextMenuEnabled),
|
||||
which is false on native. The working right-click Cut/Copy/Paste/SelectAll menu
|
||||
goes through the vendored LEGACY CommonContextMenuArea path instead.
|
||||
ClipboardPasteState uses our project Clipboard's plaintext presence — no
|
||||
NSPasteboard access on native.
|
||||
*/
|
||||
|
||||
+5
-3
@@ -36,9 +36,11 @@ internal actual fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean =
|
||||
/** No selection magnifier on desktop — mobile-only affordance. */
|
||||
internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier = this
|
||||
|
||||
/** Desktop text-context-menu doesn't add SelectionContainer-specific
|
||||
items yet. TODO(CMP-7819): wire the paste-into-selection / clear
|
||||
/ select-all extras through here when the toolbar lands. */
|
||||
/** NO-OP by design: this seam belongs to the NEW text-context-menu API
|
||||
(ComposeFoundationFlags.isNewContextMenuEnabled), which is false on native, so
|
||||
it's never reached. The working SelectionContainer right-click menu (Copy /
|
||||
SelectAll) goes through the vendored LEGACY CommonContextMenuArea path via
|
||||
SelectionManager.contextMenuBuilder. */
|
||||
internal actual fun Modifier.addSelectionContainerTextContextMenuComponents(
|
||||
selectionManager: SelectionManager
|
||||
): Modifier = this
|
||||
|
||||
+5
-1
@@ -20,7 +20,11 @@ internal actual fun TextFieldSelectionManager.isSelectionHandleInVisibleBound(
|
||||
isStartHandle: Boolean,
|
||||
): Boolean = isSelectionHandleInVisibleBoundDefault(isStartHandle)
|
||||
|
||||
// TODO(CMP-7819): wire text-field context-menu components when the desktop toolbar lands.
|
||||
// NO-OP by design: this seam belongs to the NEW text-context-menu API
|
||||
// (ComposeFoundationFlags.isNewContextMenuEnabled = false on native), so it's
|
||||
// unreachable. The working legacy-field right-click menu goes through the vendored
|
||||
// CommonContextMenuArea path (TextFieldSelectionManager.contextMenuBuilder → Cut /
|
||||
// Copy / Paste / SelectAll).
|
||||
internal actual fun Modifier.addBasicTextFieldTextContextMenuComponents(
|
||||
manager: TextFieldSelectionManager,
|
||||
coroutineScope: CoroutineScope,
|
||||
|
||||
+509
@@ -0,0 +1,509 @@
|
||||
// VENDOR-BASE: compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/v2/Scrollbar.skiko.kt @ v1.12.0-beta03+dev4483
|
||||
// MANUAL VENDOR: expect fun runBlockingIfPossible inlined to runBlocking (flat native source set can't hold expect+actual together).
|
||||
// Also carries the K2 @file:Suppress below (same as sync.py injects into src/vendor) so the native metadata compile / Windows publish accepts the orphaned JVM annotation.
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")
|
||||
@file:JvmName("Scrollbar_desktopKt")
|
||||
|
||||
package androidx.compose.foundation.v2
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.text.TextFieldScrollState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import kotlin.js.JsName
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Defines how to scroll the scrollable component and how to display a scrollbar for it.
|
||||
*
|
||||
* The values of this interface are typically in pixels, but do not have to be.
|
||||
* It's possible to create an adapter with any scroll range of `Double` values.
|
||||
*/
|
||||
interface ScrollbarAdapter {
|
||||
|
||||
// We use `Double` values here in order to allow scrolling both very large (think LazyList with
|
||||
// millions of items) and very small (think something whose natural coordinates are less than 1)
|
||||
// content.
|
||||
|
||||
/**
|
||||
* Scroll offset of the content inside the scrollable component.
|
||||
*
|
||||
* For example, a value of `100` could mean the content is scrolled by 100 pixels from the
|
||||
* start.
|
||||
*/
|
||||
val scrollOffset: Double
|
||||
|
||||
/**
|
||||
* The size of the scrollable content, on the scrollable axis.
|
||||
*/
|
||||
val contentSize: Double
|
||||
|
||||
/**
|
||||
* The size of the viewport, on the scrollable axis.
|
||||
*/
|
||||
val viewportSize: Double
|
||||
|
||||
/**
|
||||
* Instantly jump to [scrollOffset].
|
||||
*
|
||||
* @param scrollOffset target offset to jump to, value will be coerced to the valid
|
||||
* scroll range.
|
||||
*/
|
||||
suspend fun scrollTo(scrollOffset: Double)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum scroll offset of the scrollable content.
|
||||
*/
|
||||
val ScrollbarAdapter.maxScrollOffset: Double
|
||||
get() = (contentSize - viewportSize).coerceAtLeast(0.0)
|
||||
|
||||
internal class ScrollableScrollbarAdapter(
|
||||
private val scrollState: ScrollState
|
||||
) : ScrollbarAdapter {
|
||||
|
||||
override val scrollOffset: Double get() = scrollState.value.toDouble()
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
scrollState.scrollTo(scrollOffset.roundToInt())
|
||||
}
|
||||
|
||||
override val contentSize: Double
|
||||
// This isn't strictly correct, as the actual content can be smaller
|
||||
// than the viewport when scrollState.maxValue is 0, but the scrollbar
|
||||
// doesn't really care as long as contentSize <= viewportSize; it's
|
||||
// just not showing itself
|
||||
get() = scrollState.maxValue + viewportSize
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = scrollState.viewportSize.toDouble()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for [LazyListScrollbarAdapter] and [LazyGridScrollbarAdapter],
|
||||
* and in the future maybe other lazy widgets that lay out their content in lines.
|
||||
*/
|
||||
internal abstract class LazyLineContentAdapter: ScrollbarAdapter {
|
||||
|
||||
// Implement the adapter in terms of "lines", which means either rows,
|
||||
// (for a vertically scrollable widget) or columns (for a horizontally
|
||||
// scrollable one).
|
||||
// For LazyList this translates directly to items; for LazyGrid, it
|
||||
// translates to rows/columns of items.
|
||||
|
||||
class VisibleLine(
|
||||
val index: Int,
|
||||
val offset: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Return the first visible line, if any.
|
||||
*/
|
||||
protected abstract fun firstVisibleLine(): VisibleLine?
|
||||
|
||||
/**
|
||||
* Return the total number of lines.
|
||||
*/
|
||||
protected abstract fun totalLineCount(): Int
|
||||
|
||||
/**
|
||||
* The sum of content padding (before+after) on the scrollable axis.
|
||||
*/
|
||||
protected abstract fun contentPadding(): Int
|
||||
|
||||
/**
|
||||
* Scroll immediately to the given line, and offset it by [scrollOffset] pixels.
|
||||
*/
|
||||
protected abstract suspend fun snapToLine(lineIndex: Int, scrollOffset: Int)
|
||||
|
||||
/**
|
||||
* Scroll from the current position by the given amount of pixels.
|
||||
*/
|
||||
protected abstract suspend fun scrollBy(value: Float)
|
||||
|
||||
/**
|
||||
* Return the average size (on the scrollable axis) of the visible lines.
|
||||
*/
|
||||
protected abstract fun averageVisibleLineSize(): Double
|
||||
|
||||
/**
|
||||
* The spacing between lines.
|
||||
*/
|
||||
protected abstract val lineSpacing: Int
|
||||
|
||||
@JsName("averageVisibleLineSizeProperty")
|
||||
private val averageVisibleLineSize by derivedStateOf {
|
||||
if (totalLineCount() == 0)
|
||||
0.0
|
||||
else
|
||||
averageVisibleLineSize()
|
||||
}
|
||||
|
||||
private val averageVisibleLineSizeWithSpacing get() = averageVisibleLineSize + lineSpacing
|
||||
|
||||
override val scrollOffset: Double
|
||||
get() {
|
||||
val firstVisibleLine = firstVisibleLine()
|
||||
return if (firstVisibleLine == null)
|
||||
0.0
|
||||
else
|
||||
firstVisibleLine.index * averageVisibleLineSizeWithSpacing - firstVisibleLine.offset
|
||||
}
|
||||
|
||||
override val contentSize: Double
|
||||
get() {
|
||||
val totalLineCount = totalLineCount()
|
||||
return averageVisibleLineSize * totalLineCount +
|
||||
lineSpacing * (totalLineCount - 1).coerceAtLeast(0) +
|
||||
contentPadding()
|
||||
}
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
val distance = scrollOffset - this@LazyLineContentAdapter.scrollOffset
|
||||
|
||||
// if we scroll less than viewport we need to use scrollBy function to avoid
|
||||
// undesirable scroll jumps (when an item size is different)
|
||||
//
|
||||
// if we scroll more than viewport we should immediately jump to this position
|
||||
// without recreating all items between the current and the new position
|
||||
if (abs(distance) <= viewportSize) {
|
||||
scrollBy(distance.toFloat())
|
||||
} else {
|
||||
snapTo(scrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun snapTo(scrollOffset: Double) {
|
||||
val scrollOffsetCoerced = scrollOffset.coerceIn(0.0, maxScrollOffset)
|
||||
|
||||
val index = (scrollOffsetCoerced / averageVisibleLineSizeWithSpacing)
|
||||
.toInt()
|
||||
.coerceAtLeast(0)
|
||||
.coerceAtMost(totalLineCount() - 1)
|
||||
|
||||
val offset = (scrollOffsetCoerced - index * averageVisibleLineSizeWithSpacing)
|
||||
.toInt()
|
||||
.coerceAtLeast(0)
|
||||
|
||||
snapToLine(lineIndex = index, scrollOffset = offset)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class LazyListScrollbarAdapter(
|
||||
private val scrollState: LazyListState
|
||||
) : LazyLineContentAdapter() {
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = with(scrollState.layoutInfo) {
|
||||
if (orientation == Orientation.Vertical)
|
||||
viewportSize.height
|
||||
else
|
||||
viewportSize.width
|
||||
}.toDouble()
|
||||
|
||||
/**
|
||||
* A heuristic that tries to ignore the "currently stickied" header because it breaks the other
|
||||
* computations in this adapter:
|
||||
* - The currently stickied header always appears in the list of visible items, with its
|
||||
* regular index. This makes [firstVisibleLine] always return its index, even if the list has
|
||||
* been scrolled far beyond it.
|
||||
* - [averageVisibleLineSize] calculates the average size in O(1) by assuming that items don't
|
||||
* overlap, and the stickied item breaks this assumption.
|
||||
*
|
||||
* Attempts to return the index into `visibleItemsInfo` of the first non-currently-stickied (it
|
||||
* could be sticky, but not stickied to the top of the list right now) item, if there is one.
|
||||
*
|
||||
* Note that this heuristic breaks down if the sticky header covers the entire list, so that
|
||||
* it's the only visible item for some portion of the scroll range. But there's currently no
|
||||
* known better way to solve it, and it's a relatively unusual case.
|
||||
*/
|
||||
private fun firstFloatingVisibleItemIndex() = with(scrollState.layoutInfo.visibleItemsInfo){
|
||||
when (size) {
|
||||
0 -> null
|
||||
1 -> 0
|
||||
else -> {
|
||||
val first = this[0]
|
||||
val second = this[1]
|
||||
// If either the indices or the offsets aren't continuous, then the first item is
|
||||
// sticky, so we return 1
|
||||
if ((first.index < second.index - 1) ||
|
||||
(first.offset + first.size + lineSpacing > second.offset))
|
||||
1
|
||||
else
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun firstVisibleLine(): VisibleLine? {
|
||||
val firstFloatingVisibleIndex = firstFloatingVisibleItemIndex() ?: return null
|
||||
val firstFloatingItem = scrollState.layoutInfo.visibleItemsInfo[firstFloatingVisibleIndex]
|
||||
return VisibleLine(
|
||||
index = firstFloatingItem.index,
|
||||
offset = firstFloatingItem.offset
|
||||
)
|
||||
}
|
||||
|
||||
override fun totalLineCount() = scrollState.layoutInfo.totalItemsCount
|
||||
|
||||
override fun contentPadding() = with(scrollState.layoutInfo){
|
||||
beforeContentPadding + afterContentPadding
|
||||
}
|
||||
|
||||
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||
scrollState.scrollToItem(lineIndex, scrollOffset)
|
||||
}
|
||||
|
||||
override suspend fun scrollBy(value: Float) {
|
||||
scrollState.scrollBy(value)
|
||||
}
|
||||
|
||||
override fun averageVisibleLineSize() = with(scrollState.layoutInfo.visibleItemsInfo){
|
||||
val firstFloatingIndex = firstFloatingVisibleItemIndex() ?: return@with 0.0
|
||||
val first = this[firstFloatingIndex]
|
||||
val last = last()
|
||||
val count = size - firstFloatingIndex
|
||||
(last.offset + last.size - first.offset - (count-1)*lineSpacing).toDouble() / count
|
||||
}
|
||||
|
||||
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||
|
||||
}
|
||||
|
||||
|
||||
internal class LazyGridScrollbarAdapter(
|
||||
private val scrollState: LazyGridState
|
||||
): LazyLineContentAdapter() {
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = with(scrollState.layoutInfo) {
|
||||
if (orientation == Orientation.Vertical)
|
||||
viewportSize.height
|
||||
else
|
||||
viewportSize.width
|
||||
}.toDouble()
|
||||
|
||||
private val isVertical = scrollState.layoutInfo.orientation == Orientation.Vertical
|
||||
|
||||
private val unknownLine = with(LazyGridItemInfo) {
|
||||
if (isVertical) UnknownRow else UnknownColumn
|
||||
}
|
||||
|
||||
private fun LazyGridItemInfo.line() = if (isVertical) row else column
|
||||
|
||||
private fun LazyGridItemInfo.mainAxisSize() = with (size) {
|
||||
if (isVertical) height else width
|
||||
}
|
||||
|
||||
private fun LazyGridItemInfo.mainAxisOffset() = with(offset) {
|
||||
if (isVertical) y else x
|
||||
}
|
||||
|
||||
private fun lineOfIndex(index: Int) = index / scrollState.slotsPerLine
|
||||
|
||||
private fun indexOfFirstInLine(line: Int) = line * scrollState.slotsPerLine
|
||||
|
||||
override fun firstVisibleLine(): VisibleLine? {
|
||||
return scrollState.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.line() != unknownLine } // Skip exiting items
|
||||
?.let { firstVisibleItem ->
|
||||
VisibleLine(
|
||||
index = firstVisibleItem.line(),
|
||||
offset = firstVisibleItem.mainAxisOffset()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun totalLineCount(): Int{
|
||||
val itemCount = scrollState.layoutInfo.totalItemsCount
|
||||
return if (itemCount == 0)
|
||||
0
|
||||
else
|
||||
lineOfIndex(itemCount - 1) + 1
|
||||
}
|
||||
|
||||
override fun contentPadding() = with(scrollState.layoutInfo){
|
||||
beforeContentPadding + afterContentPadding
|
||||
}
|
||||
|
||||
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||
scrollState.scrollToItem(
|
||||
index = indexOfFirstInLine(lineIndex),
|
||||
scrollOffset = scrollOffset
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun scrollBy(value: Float) {
|
||||
scrollState.scrollBy(value)
|
||||
}
|
||||
|
||||
private val lineIsKnown = { itemInfo: LazyGridItemInfo -> itemInfo.line() != unknownLine }
|
||||
|
||||
override fun averageVisibleLineSize(): Double {
|
||||
val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
|
||||
|
||||
// First and last visible, non-exiting LazyGridItemInfo
|
||||
val first = visibleItemsInfo.firstOrNull(lineIsKnown) ?: return 0.0
|
||||
val last = visibleItemsInfo.last(lineIsKnown)
|
||||
|
||||
// Compute the size (e.g. height for vertical grid) of the last line
|
||||
val lastLine = last.line()
|
||||
val lastLineSize = visibleItemsInfo
|
||||
.asReversed()
|
||||
.asSequence()
|
||||
.filter(lineIsKnown)
|
||||
.takeWhile { it.line() == lastLine }
|
||||
.maxOf { it.mainAxisSize() }
|
||||
|
||||
val lineCount = last.line() - first.line() + 1
|
||||
val lineSpacingSum = (lineCount - 1) * lineSpacing
|
||||
return (
|
||||
last.mainAxisOffset() + lastLineSize - first.mainAxisOffset() - lineSpacingSum
|
||||
).toDouble() / lineCount
|
||||
}
|
||||
|
||||
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal class TextFieldScrollbarAdapter(
|
||||
private val scrollState: TextFieldScrollState
|
||||
): ScrollbarAdapter {
|
||||
|
||||
override val scrollOffset: Double
|
||||
get() = scrollState.offset.toDouble()
|
||||
|
||||
override val contentSize: Double
|
||||
get() = scrollState.maxOffset + viewportSize
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = scrollState.viewportSize.toDouble()
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
scrollState.offset = scrollOffset.toFloat().coerceIn(0f, scrollState.maxOffset)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class SliderAdapter(
|
||||
val adapter: ScrollbarAdapter,
|
||||
private val trackSize: Int,
|
||||
private val minHeight: Float,
|
||||
private val reverseLayout: Boolean,
|
||||
private val isVertical: Boolean,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) {
|
||||
|
||||
private val contentSize get() = adapter.contentSize
|
||||
private val visiblePart: Double
|
||||
get() {
|
||||
val contentSize = contentSize
|
||||
return if (contentSize == 0.0)
|
||||
1.0
|
||||
else
|
||||
(adapter.viewportSize / contentSize).coerceAtMost(1.0)
|
||||
}
|
||||
|
||||
val thumbSize
|
||||
get() = (trackSize * visiblePart).coerceAtLeast(minHeight.toDouble())
|
||||
|
||||
private val scrollScale: Double
|
||||
get() {
|
||||
val extraScrollbarSpace = trackSize - thumbSize
|
||||
val extraContentSpace = adapter.maxScrollOffset // == contentSize - viewportSize
|
||||
return if (extraContentSpace == 0.0) 1.0 else extraScrollbarSpace / extraContentSpace
|
||||
}
|
||||
|
||||
private val rawPosition: Double
|
||||
get() = scrollScale * adapter.scrollOffset
|
||||
|
||||
val position: Double
|
||||
get() = if (reverseLayout) trackSize - thumbSize - rawPosition else rawPosition
|
||||
|
||||
val bounds get() = position..position + thumbSize
|
||||
|
||||
// How much of the current drag was ignored because we've reached the end of the scrollbar area
|
||||
private var unscrolledDragDistance = 0.0
|
||||
|
||||
/** Called when the thumb dragging starts */
|
||||
fun onDragStarted() {
|
||||
unscrolledDragDistance = 0.0
|
||||
}
|
||||
|
||||
private suspend fun setPosition(value: Double) {
|
||||
val rawPosition = if (reverseLayout) {
|
||||
trackSize - thumbSize - value
|
||||
} else {
|
||||
value
|
||||
}
|
||||
adapter.scrollTo(rawPosition / scrollScale)
|
||||
}
|
||||
|
||||
private val dragMutex = Mutex()
|
||||
|
||||
/** Called on every movement while dragging the thumb */
|
||||
fun onDragDelta(offset: Offset) {
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
// Mutex is used to ensure that all earlier drag deltas were applied
|
||||
// before calculating a new raw position
|
||||
dragMutex.withLock {
|
||||
val dragDelta = if (isVertical) offset.y else offset.x
|
||||
val maxScrollPosition = adapter.maxScrollOffset * scrollScale
|
||||
val currentPosition = position
|
||||
val targetPosition =
|
||||
(currentPosition + dragDelta + unscrolledDragDistance).coerceIn(
|
||||
0.0,
|
||||
maxScrollPosition
|
||||
)
|
||||
val sliderDelta = targetPosition - currentPosition
|
||||
|
||||
// Have to add to position for smooth content scroll if the items are of different size
|
||||
val newPos = position + sliderDelta
|
||||
setPosition(newPos)
|
||||
unscrolledDragDistance += dragDelta - sliderDelta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Because k/js and k/wasm don't have runBlocking. This port targets only
|
||||
// native desktop (macos/linux/mingw), which all have runBlocking, so the
|
||||
// upstream expect/actual is inlined here — our flat native source set can't
|
||||
// hold the expect (in this .skiko.kt) and its actual in the same set.
|
||||
internal fun runBlockingIfPossible(block: suspend CoroutineScope.() -> Unit) =
|
||||
kotlinx.coroutines.runBlocking(block = block)
|
||||
+1
-1
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.sp
|
||||
|
||||
The four Material Symbols variable-font axes (fill / weight / grade /
|
||||
opticalSize) come in via `fontVariationSettings`; use `MaterialIconAxes(…)`
|
||||
to build the list. Skia honours the axes; SDL3_ttf 3.2 ignores them. */
|
||||
to build the list. Skia honours the axes. */
|
||||
@Composable
|
||||
fun IconFontIcon(
|
||||
codepoint: Int,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:material-ripple>
|
||||
// Library unique name: <com.bitsycore.compose.material: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]
|
||||
|
||||
@@ -13,8 +13,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:material3>
|
||||
// Library unique name: <com.bitsycore.compose.material3:material3>
|
||||
open annotation class androidx.compose.material3/ExperimentalMaterial3Api : kotlin/Annotation { // androidx.compose.material3/ExperimentalMaterial3Api|null[0]
|
||||
constructor <init>() // androidx.compose.material3/ExperimentalMaterial3Api.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ kotlin {
|
||||
api(project(":animation-core"))
|
||||
api(project(":material-ripple"))
|
||||
// androidx.collection is used by material3 internals (MutableIntObjectMap, …).
|
||||
// Already on the classpath via :core's runtime; declare here for clarity.
|
||||
// Already on the classpath via :ui's runtime; declare here for clarity.
|
||||
implementation("androidx.collection:collection:1.5.0")
|
||||
// androidx.graphics.shapes.* — MaterialShapes (RoundedPolygon /
|
||||
// Morph / CornerRounding) that WavyProgress / LoadingIndicator use.
|
||||
|
||||
+92
-10
@@ -13,10 +13,12 @@ import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
/**
|
||||
Upstream ships this via NSDateFormatter (darwinMain) or java.text.DateFormat
|
||||
(desktopMain). Neither is available for K/N on Linux/Windows, so we ship a
|
||||
minimal English-only stub that unblocks DatePicker + TimePicker compilation.
|
||||
Localization is TODO; format strings are matched via a small dispatcher on the
|
||||
subset of patterns material3 actually asks for.
|
||||
(desktopMain). Neither is available for K/N on Linux/Windows, so this actual is
|
||||
a cross-platform kotlinx-datetime formatter that HONOURS the requested
|
||||
CLDR-style pattern / skeleton (so DatePicker/TimePicker headlines read
|
||||
"Jul 29, 2026" / "July 2026" instead of a raw ISO date). Field VALUES are
|
||||
localized by the requested TimeZone.UTC calendar; field NAMES (month / weekday)
|
||||
stay English — full CLDR name localization needs ICU data we don't bundle.
|
||||
*/
|
||||
@OptIn(ExperimentalTime::class)
|
||||
internal actual class PlatformDateFormat actual constructor(
|
||||
@@ -33,13 +35,13 @@ internal actual class PlatformDateFormat actual constructor(
|
||||
utcTimeMillis: Long,
|
||||
pattern: String,
|
||||
cache: MutableMap<String, Any>,
|
||||
): String = defaultFormat(utcTimeMillis)
|
||||
): String = formatPattern(utcTimeMillis, pattern)
|
||||
|
||||
actual fun formatWithSkeleton(
|
||||
utcTimeMillis: Long,
|
||||
skeleton: String,
|
||||
cache: MutableMap<String, Any>,
|
||||
): String = defaultFormat(utcTimeMillis)
|
||||
): String = formatPattern(utcTimeMillis, skeletonToPattern(skeleton))
|
||||
|
||||
actual fun parse(
|
||||
date: String,
|
||||
@@ -64,11 +66,91 @@ internal actual class PlatformDateFormat actual constructor(
|
||||
|
||||
actual fun is24HourFormat(): Boolean = true
|
||||
|
||||
private fun defaultFormat(utcTimeMillis: Long): String {
|
||||
val vInstant = Instant.fromEpochMilliseconds(utcTimeMillis)
|
||||
val vDt = vInstant.toLocalDateTime(TimeZone.UTC)
|
||||
return "${vDt.year}-${vDt.monthNumber.pad2()}-${vDt.dayOfMonth.pad2()}"
|
||||
// ============
|
||||
// Pattern / skeleton formatting
|
||||
|
||||
/** Format [utcTimeMillis] against a CLDR-style [pattern] (y/M/d/E/H/h/m/s/a
|
||||
field runs; `'...'` quotes literal text). */
|
||||
private fun formatPattern(utcTimeMillis: Long, pattern: String): String {
|
||||
val dt = Instant.fromEpochMilliseconds(utcTimeMillis).toLocalDateTime(TimeZone.UTC)
|
||||
val out = StringBuilder()
|
||||
var i = 0
|
||||
while (i < pattern.length) {
|
||||
val c = pattern[i]
|
||||
when {
|
||||
c == '\'' -> {
|
||||
// Quoted literal; '' is an escaped single quote.
|
||||
var j = i + 1
|
||||
while (j < pattern.length && pattern[j] != '\'') { out.append(pattern[j]); j++ }
|
||||
i = j + 1
|
||||
}
|
||||
c.isLetter() -> {
|
||||
var j = i
|
||||
while (j < pattern.length && pattern[j] == c) j++
|
||||
out.append(formatField(c, j - i, dt))
|
||||
i = j
|
||||
}
|
||||
else -> { out.append(c); i++ }
|
||||
}
|
||||
}
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
private fun formatField(field: Char, count: Int, dt: kotlinx.datetime.LocalDateTime): String = when (field) {
|
||||
'y' -> if (count == 2) (dt.year % 100).pad2() else dt.year.toString()
|
||||
'M' -> when {
|
||||
count >= 4 -> MONTH_NAMES[dt.monthNumber - 1]
|
||||
count == 3 -> MONTH_ABBR[dt.monthNumber - 1]
|
||||
count == 2 -> dt.monthNumber.pad2()
|
||||
else -> dt.monthNumber.toString()
|
||||
}
|
||||
'd' -> if (count >= 2) dt.dayOfMonth.pad2() else dt.dayOfMonth.toString()
|
||||
'E' -> {
|
||||
val idx = dt.dayOfWeek.ordinal // kotlinx DayOfWeek: MONDAY=0 → matches weekdayNames order
|
||||
if (count >= 4) weekdayNames[idx].first else weekdayNames[idx].second
|
||||
}
|
||||
'H' -> if (count >= 2) dt.hour.pad2() else dt.hour.toString()
|
||||
'h' -> {
|
||||
val h12 = ((dt.hour + 11) % 12) + 1
|
||||
if (count >= 2) h12.pad2() else h12.toString()
|
||||
}
|
||||
'm' -> if (count >= 2) dt.minute.pad2() else dt.minute.toString()
|
||||
's' -> if (count >= 2) dt.second.pad2() else dt.second.toString()
|
||||
'a' -> if (dt.hour < 12) "AM" else "PM"
|
||||
else -> "" // unsupported field: drop rather than echo raw pattern letters
|
||||
}
|
||||
|
||||
/** Map the CLDR skeletons material3 requests (unordered field sets) to concrete
|
||||
English patterns. Unknown skeletons fall back to the skeleton itself, which
|
||||
still renders the correct field VALUES (just without separators). */
|
||||
private fun skeletonToPattern(skeleton: String): String = when (skeleton) {
|
||||
"yMMMM" -> "MMMM yyyy"
|
||||
"yMMMMd", "yMMMMdd" -> "MMMM d, yyyy"
|
||||
"yMMMd" -> "MMM d, yyyy"
|
||||
"yMMM" -> "MMM yyyy"
|
||||
"yMMMMEEEEd" -> "EEEE, MMMM d, yyyy"
|
||||
"yMMMEd" -> "EEE, MMM d, yyyy"
|
||||
"MMMMEEEEd" -> "EEEE, MMMM d"
|
||||
"MMMMd" -> "MMMM d"
|
||||
"MMMd" -> "MMM d"
|
||||
"MMMEd" -> "EEE, MMM d"
|
||||
"y" -> "yyyy"
|
||||
"MMMM" -> "MMMM"
|
||||
"Hm" -> "HH:mm"
|
||||
"hm", "hma" -> "h:mm a"
|
||||
else -> skeleton
|
||||
}
|
||||
|
||||
private fun Int.pad2(): String = if (this < 10) "0$this" else "$this"
|
||||
|
||||
private companion object {
|
||||
val MONTH_NAMES = listOf(
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December",
|
||||
)
|
||||
val MONTH_ABBR = listOf(
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// 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.native:window>
|
||||
final fun com.compose.desktop.native/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.desktop.native/GpuMode = ..., kotlin/Function2<com.compose.desktop.native/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.desktop.native/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.desktop.native/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.desktop.native.GpuMode;kotlin.Function2<com.compose.desktop.native.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.desktop.native.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:ui-backhandler>
|
||||
// Library unique name: <com.bitsycore.compose.ui:ui-backhandler>
|
||||
final class androidx.compose.ui.backhandler/BackEventCompat { // androidx.compose.ui.backhandler/BackEventCompat|null[0]
|
||||
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Int) // androidx.compose.ui.backhandler/BackEventCompat.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Int){}[0]
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:ui-geometry>
|
||||
// Library unique name: <com.bitsycore.compose.ui:ui-geometry>
|
||||
final class androidx.compose.ui.geometry/MutableRect { // androidx.compose.ui.geometry/MutableRect|null[0]
|
||||
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // androidx.compose.ui.geometry/MutableRect.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
// :ui-graphics — androidx.compose.ui.graphics.* split out of :ui (upstream layout).
|
||||
// Canvas / Paint / Path / Brush / Color / ImageBitmap / Shader / GraphicsLayer +
|
||||
// the Skia actuals (SkiaBackedCanvas, SkiaImageCache, …). SDL-free — image bytes
|
||||
// come through the composeResourceReader seam, so it depends only on skiko +
|
||||
// the low ui primitives, NOT on :sdl-core.
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
}
|
||||
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
linuxX64()
|
||||
macosArm64()
|
||||
if (vHostSupportsMingw) mingwX64()
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
kotlin.srcDir("src/vendor/common/kotlin")
|
||||
dependencies {
|
||||
api(project(":ui-geometry"))
|
||||
api(project(":ui-unit"))
|
||||
api(project(":ui-util"))
|
||||
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
// androidx.collection comes transitively via :ui-util (api).
|
||||
}
|
||||
}
|
||||
nativeMain {
|
||||
kotlin.srcDir("src/vendor/native/kotlin")
|
||||
}
|
||||
|
||||
val skikoRendererMain = create("skikoRendererMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
|
||||
dependencies {
|
||||
implementation(libs.skiko)
|
||||
}
|
||||
}
|
||||
val skikoRendererMacosMain = create("skikoRendererMacosMain") { dependsOn(skikoRendererMain) }
|
||||
val skikoRendererLinuxMain = create("skikoRendererLinuxMain") { dependsOn(skikoRendererMain) }
|
||||
get("macosArm64Main").dependsOn(skikoRendererMacosMain)
|
||||
get("linuxX64Main").dependsOn(skikoRendererLinuxMain)
|
||||
get("linuxArm64Main").dependsOn(skikoRendererLinuxMain)
|
||||
|
||||
if (vHostSupportsMingw) {
|
||||
val skikoRendererMingwSharedMain = create("skikoRendererMingwSharedMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/skikoRendererMain/kotlin")
|
||||
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
|
||||
dependencies {
|
||||
implementation("com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse("0.150.1-mingw.1")}")
|
||||
}
|
||||
}
|
||||
val skikoRendererMingwMain = create("skikoRendererMingwMain") { dependsOn(skikoRendererMingwSharedMain) }
|
||||
get("mingwX64Main").dependsOn(skikoRendererMingwMain)
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xcollection-literals",
|
||||
"-Xexpect-actual-classes",
|
||||
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
|
||||
"-opt-in=androidx.compose.ui.InternalComposeUiApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
SET_REPO=https://github.com/JetBrains/compose-multiplatform-core@<COMPOSE_CORE_REF>
|
||||
SET_FOLDER=compose/ui/ui-graphics/src
|
||||
|
||||
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Bezier.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Bezier.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/BlendMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/BlendMode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Brush.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Canvas.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/ClipOp.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/ClipOp.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Color.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Color.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/ColorFilter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/ColorFilter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Degrees.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Degrees.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/FilterQuality.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/FilterQuality.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Float16.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/GraphicsContext.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/GraphicsContext.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/InlineClassHelper.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/InlineClassHelper.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Interpolatable.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Interpolatable.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/LayerOutsets.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/LayerOutsets.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Matrix.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Outline.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Paint.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PaintingStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PaintingStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Path.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Path.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathEffect.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathFillType.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathFillType.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathGeometry.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathGeometry.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathHitTester.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathHitTester.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathIterator.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathIterator.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathOperation.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathOperation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathSegment.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathSegment.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PathSvg.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PathSvg.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PixelMap.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/PointMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/PointMode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/RectangleShape.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/RectangleShape.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Shader.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Shadow.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Shape.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Shape.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/StrokeCap.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/StrokeCap.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/StrokeJoin.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/StrokeJoin.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/TileMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/TileMode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/VertexMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/VertexMode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/Vertices.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/Vertices.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Adaptation.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Adaptation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpace.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/ColorSpaces.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Connector.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Illuminant.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Lab.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Lab.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Oklab.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Oklab.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/RenderIntent.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/RenderIntent.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Rgb.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/colorspace/Xyz.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/colorspace/Xyz.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawContext.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/DrawContext.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeMarker.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeMarker.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawTransform.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/DrawTransform.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/drawscope/EmptyCanvas.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/drawscope/EmptyCanvas.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/layer/ChildLayerDependenciesTracker.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/layer/ChildLayerDependenciesTracker.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/layer/CompositingStrategy.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/layer/CompositingStrategy.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/painter/BitmapPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/painter/BitmapPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/painter/BrushPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/painter/BrushPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/painter/ColorPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/painter/ColorPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/painter/Painter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/painter/Painter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/Blur.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/Shadow.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/Shadow.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/ShadowContext.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/ShadowContext.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/shadow/ShadowRenderer.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/shadow/ShadowRenderer.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/vector/FastFloatParser.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/vector/FastFloatParser.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/vector/PathBuilder.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt -> src/vendor/common/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
|
||||
|
||||
nonJvmMain/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.native.kt
|
||||
|
||||
skikoMain/kotlin/ -> src/vendor/skikoRenderer/kotlin/
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaPathIterator.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaPathIterator.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaTileMode.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/shadow/Blur.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/shadow/Blur.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
|
||||
# B6.1: MANUAL-VENDORED (flattened toBitmap/putBytesInto expect-actuals — see the file).
|
||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
|
||||
|
||||
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||
# androidMain (28)
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidBlendMode.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidBlendMode.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidColor.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidColor.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidColorFilter.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidColorFilter.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidColorSpace.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidColorSpace.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidMatrixConversions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidMatrixConversions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathIterator.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidPathIterator.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidShader.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidShader.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidTileMode.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidTileMode.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/AndroidVertexMode.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/AndroidVertexMode.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/CanvasUtils.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/CanvasUtils.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/view/PlaceholderHardwareCanvas.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/view/PlaceholderHardwareCanvas.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/layer/view/ViewLayerContainer.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/shadow/AndroidShadowContext.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/shadow/AndroidShadowContext.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/graphics/shadow/Blur.android.kt
|
||||
# desktopMain (7)
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/graphics/awt/AwtColor.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/graphics/awt/AwtColor.desktop.kt
|
||||
# jvmAndAndroidMain (1)
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.jvmAndroidAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/graphics/internal/JvmDefaultWithCompatibility.jvmAndroidAndroid.kt
|
||||
# nativeMain (1)
|
||||
# | nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt
|
||||
# skikoExcludingWebMain (1)
|
||||
# | skikoExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt -> src/vendor/skikoExcludingWeb/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt
|
||||
# webMain (2)
|
||||
# | webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/graphics/Actuals.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt
|
||||
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||
+3
-3
@@ -19,9 +19,9 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
* Internal helper invoked by the project's `BackgroundNode.draw()` body
|
||||
* (Phase 8 chain-driven draw). Resolves the [Shape] into a project
|
||||
* [Outline] and dispatches to the appropriate `DrawScope.drawXxx`. The
|
||||
* receiver [DrawScope] is the renderer's wrap scope (Skia or SDL3) —
|
||||
* any of its `drawRect / drawRoundRect / drawPath` calls becomes a
|
||||
* concrete paint operation on the backend canvas.
|
||||
* receiver [DrawScope] is the Skia renderer's wrap scope — any of its
|
||||
* `drawRect / drawRoundRect / drawPath` calls becomes a concrete paint
|
||||
* operation on the backend canvas.
|
||||
*
|
||||
* Note `Shape.createOutline` is the upstream three-arg signature
|
||||
* (`size`, `layoutDirection`, `density`); project shapes implement it
|
||||
+5
-5
@@ -16,11 +16,11 @@ import androidx.compose.ui.graphics.TileMode
|
||||
// LinearGradient / RadialGradient / SweepGradient field accessors
|
||||
// `internal`, which keeps `androidx.compose.ui.graphics.LinearGradient.colors`
|
||||
// etc. out of the public ABI. Sealed-class scope forces the concrete
|
||||
// gradient classes to live in `androidx.compose.ui.graphics`, but our
|
||||
// renderer-skia / renderer-sdl3 modules can't read the resulting `internal`
|
||||
// fields directly. The extensions below sit in :core (same module as the
|
||||
// internal fields, so the bodies CAN read them) and are themselves public,
|
||||
// so :renderer-skia / :renderer-sdl3 import them and reach gradient state
|
||||
// gradient classes to live in `androidx.compose.ui.graphics`, but the Skia
|
||||
// renderer code in skikoRendererMain can't read the resulting `internal`
|
||||
// fields directly. The extensions below sit in this module's commonMain (same
|
||||
// module as the internal fields, so the bodies CAN read them) and are
|
||||
// themselves public, so the renderer imports them and reaches gradient state
|
||||
// through them.
|
||||
|
||||
// ============
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.compose.sdl.graphics
|
||||
|
||||
// ==================
|
||||
// MARK: NativeFinishableCanvas
|
||||
// ==================
|
||||
|
||||
/**
|
||||
A renderer canvas that must be flushed once drawing into it is done — some backends
|
||||
batch geometry and only submit on flush; an offscreen canvas must flush before its
|
||||
backing texture is read. Implemented by SkiaBackedCanvas (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()
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.compose.sdl.graphics
|
||||
|
||||
import com.compose.sdl.res.ResourceKind
|
||||
|
||||
// ==================
|
||||
// MARK: NativePainterCanvas bridge
|
||||
// ==================
|
||||
|
||||
/**
|
||||
Image drawing bridge. Renderer Canvas backends (SkiaBackedCanvas) implement this
|
||||
to paint a bundled resource by path + kind. [com.compose.sdl.res.ResourcePainter]
|
||||
(an upstream `Painter` subclass in the res pipeline) casts the current Canvas to
|
||||
this and calls drawNativePainter from inside its `Painter.onDraw()` — reached via
|
||||
upstream `Modifier.paint(...)` (PainterModifier), which pre-scales the
|
||||
destination size per ContentScale. So the painter always draws fill-bounds into
|
||||
the given rect, and this seam carries no ui-layout (ContentScale) dependency —
|
||||
keeping :ui-graphics free of :ui.
|
||||
*/
|
||||
interface NativePainterCanvas {
|
||||
fun drawNativePainter(
|
||||
inResourcePath: String,
|
||||
inKind: ResourceKind,
|
||||
inX: Float,
|
||||
inY: Float,
|
||||
inWidth: Float,
|
||||
inHeight: Float,
|
||||
inAlpha: Float,
|
||||
)
|
||||
}
|
||||
+2
-4
@@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.Outline
|
||||
// MARK: NativeShadowCanvas (render bridge)
|
||||
// ==================
|
||||
|
||||
/** Drop-shadow bridge, same shape as NativeTextCanvas / NativePainterCanvas:
|
||||
/** Drop-shadow bridge, same shape as NativePainterCanvas:
|
||||
the render nodes call it when a layer carries shadowElevation > 0
|
||||
(Modifier.shadow, m3 Surface tonal+shadow elevation). The outline is the
|
||||
layer's resolved shape in LAYER-LOCAL pixels — the canvas is already
|
||||
@@ -15,9 +15,7 @@ import androidx.compose.ui.graphics.Outline
|
||||
paint BEFORE the layer's clip (it lives outside the bounds).
|
||||
|
||||
Implementations:
|
||||
- SkiaBackedCanvas — a real Gaussian blur MaskFilter on the outline.
|
||||
- Sdl3Canvas — cached shadow tiles (Sdl3ShadowCache) blitted via 9-slice
|
||||
(the SDL 2D renderer has no shader/blur primitive). */
|
||||
- SkiaBackedCanvas — a real Gaussian blur MaskFilter on the outline. */
|
||||
interface NativeShadowCanvas {
|
||||
fun drawDropShadow(
|
||||
inOutline: Outline,
|
||||
+4
-4
@@ -11,10 +11,10 @@ import androidx.compose.ui.geometry.RoundRect
|
||||
a rounded-rect / circle OUTLINE (not just its bounding box).
|
||||
|
||||
The Skia backend does NOT implement this: Skia's `Canvas.clipPath` already
|
||||
clips to a rounded path natively. The SDL3 backend has no path-clip primitive
|
||||
(only a rectangular render clip), so `Sdl3Canvas` implements this by rendering
|
||||
the clipped subtree into an offscreen target and masking it to the rounded shape
|
||||
(lazily — see the pending-clip machinery there).
|
||||
clips to a rounded path natively. This side-channel exists for any backend
|
||||
that has no path-clip primitive (only a rectangular render clip) and must
|
||||
emulate it by rendering the clipped subtree into an offscreen target and
|
||||
masking it to the rounded shape.
|
||||
|
||||
The render nodes check `canvas is NativeShapeClipCanvas` when a layer has an
|
||||
`Outline.Rounded` clip and route here; otherwise they fall back to `clipPath`. The pushed clip is scoped to the enclosing `Canvas.save()` /
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.compose.sdl.res
|
||||
|
||||
// ==================
|
||||
// MARK: Platform-environment seams
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* System-environment seams the platform layer (the SDL module) installs at
|
||||
* startup, so `:foundation` (`isSystemInDarkTheme`) and `:components-resources`
|
||||
* (locale / theme resource qualifiers) can read them WITHOUT a dependency on the
|
||||
* sdl3 cinterop — the cinterop lives in the SDL platform module, but these
|
||||
* consumers sit below it. null until installed → callers fall back to the light
|
||||
* theme / empty locale (the sensible headless default).
|
||||
*/
|
||||
|
||||
/** True when the OS is in dark mode. Installed from SDL_GetSystemTheme. */
|
||||
var systemThemeIsDarkProvider: (() -> Boolean)? = null
|
||||
|
||||
/** The OS preferred locale as (language, region) — e.g. ("fr", "FR"); either may
|
||||
be empty. Installed from SDL_GetPreferredLocales. */
|
||||
var preferredLocaleProvider: (() -> Pair<String, String>)? = null
|
||||
|
||||
/** OS preferred-locale BCP47 tags (language[-REGION]), most-preferred first —
|
||||
backs androidx.compose.ui.text.intl Locale.current / LocaleList.current (in
|
||||
:ui-text). Installed from SDL_GetPreferredLocales. */
|
||||
var preferredLocaleTagsProvider: (() -> List<String>)? = null
|
||||
+9
-2
@@ -34,8 +34,8 @@ fun resourceKindForPath(path: String): ResourceKind {
|
||||
// ==================
|
||||
|
||||
/** Shared bridge the commonMain layout pass uses to size images and read raw
|
||||
bytes. The active RenderBackend installs an implementation at startup
|
||||
(parallel to currentTextMeasurer). Actual drawing is done by the renderer
|
||||
bytes. The active RenderBackend installs an implementation at startup.
|
||||
Actual drawing is done by the renderer
|
||||
directly off the LayoutNode painter leaf, reusing the backend's decode
|
||||
cache — so a decoded texture is created once and shared by measure + draw. */
|
||||
interface ImageLoader {
|
||||
@@ -56,6 +56,13 @@ private val kFallbackImageLoader = object : ImageLoader {
|
||||
/** Installed by ComposeWindow from renderBackend.imageLoader. */
|
||||
var currentImageLoader: ImageLoader = kFallbackImageLoader
|
||||
|
||||
/** Raw resource-bytes reader (reads an entry from the bundled data.kres). The
|
||||
platform layer installs the impl (SDL's ResourceIO via SDL_GetBasePath); the
|
||||
Skia font + image actuals read through this instead of the SDL loader directly,
|
||||
so ui-graphics / ui-text carry no dependency on the sdl3 cinterop. null until
|
||||
installed → callers treat that as "resource unavailable". */
|
||||
var composeResourceReader: ((String) -> ByteArray?)? = null
|
||||
|
||||
// ==================
|
||||
// MARK: painterResource
|
||||
// ==================
|
||||
+2
-3
@@ -23,7 +23,7 @@ import com.compose.sdl.graphics.NativePainterCanvas
|
||||
the renderer, so this reduced type carries just the resource pointer.
|
||||
|
||||
[onDraw] casts the current Canvas to [NativePainterCanvas] (implemented by
|
||||
Sdl3Canvas / SkiaCanvas) and delegates paint to the renderer's image cache.
|
||||
SkiaBackedCanvas) and delegates paint to the renderer's image cache.
|
||||
If the Canvas doesn't implement NativePainterCanvas (offscreen layer capture
|
||||
for graphics-layer effects) the paint is skipped.
|
||||
*/
|
||||
@@ -45,8 +45,7 @@ class ResourcePainter internal constructor(
|
||||
// translate + scaledSize, so we paint the resource FillBounds into the
|
||||
// full DrawScope size. Alpha is threaded via applyAlpha.
|
||||
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
|
||||
resourcePath, kind, 0f, 0f, size.width, size.height,
|
||||
androidx.compose.ui.layout.ContentScale.FillBounds, fAlpha,
|
||||
resourcePath, kind, 0f, 0f, size.width, size.height, fAlpha,
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -11,4 +11,4 @@ import androidx.compose.ui.graphics.GraphicsContext
|
||||
(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
|
||||
expect fun createGraphicsContext(): GraphicsContext
|
||||
+21
-4
@@ -1,5 +1,6 @@
|
||||
package com.compose.sdl.graphics
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
// ==================
|
||||
@@ -8,25 +9,41 @@ import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
/** Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
|
||||
drawable [ImageBitmap]. The active renderer registers its implementation at
|
||||
backend construction — the SDL backend goes through SDL3_image
|
||||
(Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
|
||||
backend construction — the Skia backend goes through Image.makeFromEncoded /
|
||||
SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
|
||||
hook under BOTH renderers (its skikoRendererMain reuses the SDL actuals).
|
||||
hook.
|
||||
|
||||
Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
|
||||
painterResource(Res.drawable.x) works without that module reaching into
|
||||
renderer internals. */
|
||||
interface EncodedImageDecoder {
|
||||
fun decode(inBytes: ByteArray): ImageBitmap?
|
||||
|
||||
/** Intrinsic (viewport) size of an SVG document, or null if it can't be
|
||||
parsed. Used by the resolution-independent SvgPainter for layout. */
|
||||
fun svgIntrinsicSize(inBytes: ByteArray): Size? = null
|
||||
|
||||
/** Rasterise an SVG at a specific PIXEL size (resolution-independent draw).
|
||||
Default falls back to the intrinsic-size [decode] for impls that don't
|
||||
support size-driven rendering. */
|
||||
fun decodeSvgAt(inBytes: ByteArray, inWidthPx: Int, inHeightPx: Int): ImageBitmap? = decode(inBytes)
|
||||
}
|
||||
|
||||
/** 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
|
||||
internal set
|
||||
|
||||
/** Decode via the active renderer's registered decoder — null when no renderer
|
||||
* has initialised yet or the bytes aren't a supported image. */
|
||||
fun decodeEncodedImageBitmap(inBytes: ByteArray): ImageBitmap? =
|
||||
encodedImageDecoder?.decode(inBytes)
|
||||
|
||||
/** Intrinsic size of an SVG document via the active decoder, or null. */
|
||||
fun svgIntrinsicSize(inBytes: ByteArray): Size? =
|
||||
encodedImageDecoder?.svgIntrinsicSize(inBytes)
|
||||
|
||||
/** Rasterise an SVG at [inWidthPx]×[inHeightPx] via the active decoder — the
|
||||
* resolution-independent draw path used by the resources SvgPainter. */
|
||||
fun decodeSvgAt(inBytes: ByteArray, inWidthPx: Int, inHeightPx: Int): ImageBitmap? =
|
||||
encodedImageDecoder?.decodeSvgAt(inBytes, inWidthPx, inHeightPx)
|
||||
+1
-1
@@ -16,7 +16,7 @@ import kotlin.math.abs
|
||||
|
||||
// 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(
|
||||
fun prepareLayerTransformationMatrix(
|
||||
matrix: Matrix,
|
||||
pivotX: Float,
|
||||
pivotY: Float,
|
||||
+5
-51
@@ -17,11 +17,11 @@
|
||||
// DERIVED (copy + edit) from compose-multiplatform-core -- MANUAL VENDOR, NON-IDEMPOTENT.
|
||||
// VENDOR-BASE: compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt @ v1.12.0-beta03+dev4483
|
||||
// ONLY change vs upstream: this class also implements the port draw contracts
|
||||
// (NativeTextCanvas / NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so
|
||||
// the port text engine (SkiaTextRenderer, P3.1 metrics) + resource-image cache + elevation
|
||||
// shadows keep working on the Skia leg, which now draws through THIS canvas instead of the
|
||||
// retired project SkiaCanvas. Bridged methods forward to `skiaLeafDrawer` using
|
||||
// `internalSkiaCanvas`. See RENDERER.md (B6.1, section 4).
|
||||
// (NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so the
|
||||
// resource-image cache + elevation shadows keep working on the Skia renderer,
|
||||
// which draws through THIS canvas instead of the retired project SkiaCanvas.
|
||||
// Bridged methods forward to `skiaLeafDrawer` using `internalSkiaCanvas`.
|
||||
// See RENDERER.md (B6.1, section 4).
|
||||
|
||||
package androidx.compose.ui.graphics
|
||||
|
||||
@@ -103,7 +103,6 @@ var Canvas.alphaMultiplier: Float
|
||||
internal class SkiaBackedCanvas(
|
||||
internal val internalSkiaCanvas: SkCanvas,
|
||||
) : Canvas,
|
||||
com.compose.sdl.text.NativeTextCanvas,
|
||||
com.compose.sdl.graphics.NativePainterCanvas,
|
||||
com.compose.sdl.graphics.NativeShadowCanvas,
|
||||
com.compose.sdl.graphics.NativeFinishableCanvas {
|
||||
@@ -497,49 +496,6 @@ internal class SkiaBackedCanvas(
|
||||
|
||||
override fun finish() { /* Skia save/restore is balanced per call */ }
|
||||
|
||||
override fun drawNativeText(
|
||||
inText: String,
|
||||
inSpans: List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>?,
|
||||
inX: Float,
|
||||
inY: Float,
|
||||
inBoxWidth: Float,
|
||||
inBoxHeight: Float,
|
||||
inColor: Color,
|
||||
inFontSizePx: Int,
|
||||
inTextAlign: androidx.compose.ui.text.style.TextAlign,
|
||||
inSoftWrap: Boolean,
|
||||
inFontFamily: String?,
|
||||
inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?,
|
||||
inBaseItalic: Boolean,
|
||||
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
|
||||
inLineHeightPx: Float,
|
||||
inTrimFirstLine: Boolean,
|
||||
) {
|
||||
val drawer = skiaLeafDrawer ?: return
|
||||
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
|
||||
val vLineThrough = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.LineThrough) == true
|
||||
drawer.textRenderer.drawText(
|
||||
inCanvas = internalSkiaCanvas,
|
||||
inText = inText,
|
||||
inX = inX,
|
||||
inY = inY,
|
||||
inBoxWidth = inBoxWidth.toInt(),
|
||||
inBoxHeight = inBoxHeight.toInt(),
|
||||
inColor = inColor,
|
||||
inFontSize = inFontSizePx,
|
||||
inAlign = inTextAlign,
|
||||
inSoftWrap = inSoftWrap,
|
||||
inFontFamily = inFontFamily,
|
||||
inFontVariations = inFontVariations,
|
||||
inSpans = inSpans,
|
||||
inBaseItalic = inBaseItalic,
|
||||
inBaseUnderline = vUnderline,
|
||||
inBaseLineThrough = vLineThrough,
|
||||
inLineHeightPx = inLineHeightPx,
|
||||
inTrimFirstLine = inTrimFirstLine,
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawNativePainter(
|
||||
inResourcePath: String,
|
||||
inKind: com.compose.sdl.res.ResourceKind,
|
||||
@@ -547,7 +503,6 @@ internal class SkiaBackedCanvas(
|
||||
inY: Float,
|
||||
inWidth: Float,
|
||||
inHeight: Float,
|
||||
inContentScale: androidx.compose.ui.layout.ContentScale,
|
||||
inAlpha: Float,
|
||||
) {
|
||||
val drawer = skiaLeafDrawer ?: return
|
||||
@@ -559,7 +514,6 @@ internal class SkiaBackedCanvas(
|
||||
inY = inY,
|
||||
inW = inWidth,
|
||||
inH = inHeight,
|
||||
inScale = inContentScale,
|
||||
inAlpha = inAlpha,
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -10,4 +10,4 @@ import androidx.compose.ui.graphics.SkiaGraphicsContext
|
||||
/** The Skia leg uses upstream's SkiaGraphicsContext, which owns a skiko
|
||||
RenderNodeContext and creates upstream GraphicsLayer(skiko.RenderNode) instances.
|
||||
See RENDERER.md (B6.2, section 4). */
|
||||
internal actual fun createGraphicsContext(): GraphicsContext = SkiaGraphicsContext()
|
||||
actual fun createGraphicsContext(): GraphicsContext = SkiaGraphicsContext()
|
||||
+1
-1
@@ -18,7 +18,7 @@ import org.jetbrains.skia.Surface
|
||||
Returns null if anything along the way fails: the Surface may not yet
|
||||
exist (first frame), the GPU readback may fail, or bitmap.readPixels()
|
||||
may not be exposed in this Skiko build. */
|
||||
internal fun readBackBgra(inSurface: Surface?, inWidth: Int, inHeight: Int): Triple<Int, Int, ByteArray>? {
|
||||
fun readBackBgra(inSurface: Surface?, inWidth: Int, inHeight: Int): Triple<Int, Int, ByteArray>? {
|
||||
val vSurface = inSurface ?: return null
|
||||
if (inWidth <= 0 || inHeight <= 0) return null
|
||||
val vBitmap = Bitmap()
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package com.compose.sdl.renderer.skia
|
||||
|
||||
import androidx.compose.ui.geometry.isSpecified
|
||||
import com.compose.sdl.res.AndroidVectorToSvg
|
||||
import com.compose.sdl.res.ResourceKind
|
||||
import com.compose.sdl.res.composeResourceReader
|
||||
import kotlin.math.roundToInt
|
||||
import org.jetbrains.skia.Canvas
|
||||
import org.jetbrains.skia.Color
|
||||
import org.jetbrains.skia.Data
|
||||
import org.jetbrains.skia.Image
|
||||
import org.jetbrains.skia.Paint
|
||||
import org.jetbrains.skia.Rect
|
||||
import org.jetbrains.skia.Surface
|
||||
import org.jetbrains.skia.svg.SVGDOM
|
||||
|
||||
// ==================
|
||||
// MARK: SkiaImageCache
|
||||
// ==================
|
||||
|
||||
/** Decodes bundled image resources to org.jetbrains.skia.Image and caches them
|
||||
by relative path; backs both the layout pass (intrinsicSize) and the
|
||||
renderer's draw. Raster formats go through Image.makeFromEncoded; SVG and
|
||||
the SVG produced from Android <vector> XML are rendered through SVGDOM into
|
||||
an offscreen surface and snapshotted to an Image, so the draw path is the
|
||||
same drawImageRect for every kind.
|
||||
|
||||
NOTE: this source set isn't compiled on the mingwX64 host, so it is built
|
||||
only on macOS / Linux — keep it to the Skiko APIs already used elsewhere in
|
||||
this module. */
|
||||
private const val MAX_CACHED_IMAGES = 256
|
||||
|
||||
class SkiaImageCache {
|
||||
|
||||
// Bounded access-order LRU. Value is null when a decode failed — cached to
|
||||
// avoid retrying each frame. Bounding + closing the least-recently-used image
|
||||
// stops long-running apps that show many distinct runtime images
|
||||
// (registerMemoryResource — e.g. downloaded PNGs) from growing image memory
|
||||
// without limit (issue #2). On-screen images stay hot; an evicted one just
|
||||
// re-decodes on next use.
|
||||
private val fCache = LinkedHashMap<String, Image?>()
|
||||
|
||||
// SVG / Android-vector kinds are RESOLUTION-INDEPENDENT: instead of caching one
|
||||
// intrinsic-size raster and letting drawImageRect upscale it (blurry when an icon
|
||||
// is drawn larger than its intrinsic box), we re-rasterise the vector at the
|
||||
// destination pixel size and cache that raster keyed by "path@WxH". Mirrors
|
||||
// upstream's size-driven DrawCache for SVGPainter. Intrinsic dims are cached
|
||||
// separately so the layout pass (intrinsicSize) doesn't force a raster.
|
||||
private val fSvgRasterCache = LinkedHashMap<String, Image?>()
|
||||
private val fSvgIntrinsic = HashMap<String, androidx.compose.ui.geometry.Size>()
|
||||
|
||||
fun intrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
||||
if (inKind == ResourceKind.Svg || inKind == ResourceKind.AndroidVector) {
|
||||
return svgIntrinsicSize(inPath, inKind)
|
||||
}
|
||||
val vImg = get(inPath, inKind) ?: return androidx.compose.ui.geometry.Size.Unspecified
|
||||
return androidx.compose.ui.geometry.Size(vImg.width.toFloat(), vImg.height.toFloat())
|
||||
}
|
||||
|
||||
private fun get(inPath: String, inKind: ResourceKind): Image? {
|
||||
if (fCache.containsKey(inPath)) {
|
||||
// Re-insert to mark most-recently-used (LinkedHashMap keeps first =
|
||||
// least-recently-used for eviction below).
|
||||
val vExisting = fCache.remove(inPath)
|
||||
fCache[inPath] = vExisting
|
||||
return vExisting
|
||||
}
|
||||
val vImage = decode(inPath, inKind)
|
||||
fCache[inPath] = vImage
|
||||
if (fCache.size > MAX_CACHED_IMAGES) {
|
||||
val vEldest = fCache.keys.firstOrNull()
|
||||
if (vEldest != null) fCache.remove(vEldest)?.close()
|
||||
}
|
||||
return vImage
|
||||
}
|
||||
|
||||
private fun decode(inPath: String, inKind: ResourceKind): Image? {
|
||||
val vBytes = composeResourceReader?.invoke(inPath) ?: return null
|
||||
return when (inKind) {
|
||||
ResourceKind.Raster -> runCatching { Image.makeFromEncoded(vBytes) }.getOrNull()
|
||||
ResourceKind.Svg -> rasterizeSvg(vBytes)
|
||||
ResourceKind.AndroidVector -> rasterizeSvg(AndroidVectorToSvg.convert(vBytes.decodeToString()).encodeToByteArray())
|
||||
ResourceKind.Raw -> null
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: Draw
|
||||
// ==================
|
||||
|
||||
/** Paints the resource into (inX, inY, inW, inH) applying contentScale +
|
||||
alpha. Alpha modulates via the paint colour's alpha channel (RGB is
|
||||
ignored for images without a colour filter). */
|
||||
fun draw(
|
||||
inCanvas: Canvas,
|
||||
inPath: String,
|
||||
inKind: ResourceKind,
|
||||
inX: Float,
|
||||
inY: Float,
|
||||
inW: Float,
|
||||
inH: Float,
|
||||
inAlpha: Float,
|
||||
) {
|
||||
if (inW <= 0f || inH <= 0f) return
|
||||
if (inKind == ResourceKind.Svg || inKind == ResourceKind.AndroidVector) {
|
||||
drawSvg(inCanvas, inPath, inKind, inX, inY, inW, inH, inAlpha)
|
||||
return
|
||||
}
|
||||
val vImg = get(inPath, inKind) ?: return
|
||||
val vIw = vImg.width.toFloat()
|
||||
val vIh = vImg.height.toFloat()
|
||||
if (vIw <= 0f || vIh <= 0f) return
|
||||
|
||||
val vPaint = Paint()
|
||||
vPaint.color = Color.makeARGB((inAlpha * 255f).toInt().coerceIn(0, 255), 255, 255, 255)
|
||||
|
||||
// Upstream PainterModifier pre-scales the destination per ContentScale, so
|
||||
// draw fill-bounds into the given rect.
|
||||
inCanvas.drawImageRect(vImg, Rect.makeWH(vIw, vIh), Rect.makeXYWH(inX, inY, inW, inH), vPaint)
|
||||
vPaint.close()
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: Size-driven SVG / Android-vector
|
||||
// ==================
|
||||
|
||||
/** Raw SVG bytes for the resource (Android-vector XML is converted first). */
|
||||
private fun svgBytes(inPath: String, inKind: ResourceKind): ByteArray? {
|
||||
val vRaw = composeResourceReader?.invoke(inPath) ?: return null
|
||||
return if (inKind == ResourceKind.AndroidVector) {
|
||||
runCatching { AndroidVectorToSvg.convert(vRaw.decodeToString()).encodeToByteArray() }.getOrNull()
|
||||
} else {
|
||||
vRaw
|
||||
}
|
||||
}
|
||||
|
||||
/** Intrinsic (viewport) size of the vector, parsed once and cached. */
|
||||
private fun svgIntrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
||||
fSvgIntrinsic[inPath]?.let { return it }
|
||||
val vBytes = svgBytes(inPath, inKind) ?: return androidx.compose.ui.geometry.Size.Unspecified
|
||||
val vSize = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(vBytes))
|
||||
val vRoot = vDom.root
|
||||
val vW = (vRoot?.width?.value ?: 0f).takeIf { it > 0f } ?: 100f
|
||||
val vH = (vRoot?.height?.value ?: 0f).takeIf { it > 0f } ?: 100f
|
||||
vDom.close()
|
||||
androidx.compose.ui.geometry.Size(vW, vH)
|
||||
}.getOrDefault(androidx.compose.ui.geometry.Size.Unspecified)
|
||||
fSvgIntrinsic[inPath] = vSize
|
||||
return vSize
|
||||
}
|
||||
|
||||
private fun drawSvg(
|
||||
inCanvas: Canvas, inPath: String, inKind: ResourceKind,
|
||||
inX: Float, inY: Float, inW: Float, inH: Float, inAlpha: Float,
|
||||
) {
|
||||
val vWpx = inW.roundToInt().coerceAtLeast(1)
|
||||
val vHpx = inH.roundToInt().coerceAtLeast(1)
|
||||
val vKey = "$inPath@${vWpx}x$vHpx"
|
||||
val vImg = if (fSvgRasterCache.containsKey(vKey)) {
|
||||
val vExisting = fSvgRasterCache.remove(vKey)
|
||||
fSvgRasterCache[vKey] = vExisting
|
||||
vExisting
|
||||
} else {
|
||||
val vIntrinsic = svgIntrinsicSize(inPath, inKind)
|
||||
val vBytes = svgBytes(inPath, inKind)
|
||||
val vRaster = if (vBytes != null && vIntrinsic.isSpecified) {
|
||||
rasterizeSvgAt(vBytes, vIntrinsic.width, vIntrinsic.height, vWpx, vHpx)
|
||||
} else null
|
||||
fSvgRasterCache[vKey] = vRaster
|
||||
if (fSvgRasterCache.size > MAX_CACHED_IMAGES) {
|
||||
fSvgRasterCache.keys.firstOrNull()?.let { fSvgRasterCache.remove(it)?.close() }
|
||||
}
|
||||
vRaster
|
||||
} ?: return
|
||||
|
||||
val vPaint = Paint()
|
||||
vPaint.color = Color.makeARGB((inAlpha * 255f).toInt().coerceIn(0, 255), 255, 255, 255)
|
||||
// Raster is already at destination pixel size → 1:1 blit (crisp, no upscale).
|
||||
inCanvas.drawImageRect(vImg, Rect.makeWH(vImg.width.toFloat(), vImg.height.toFloat()),
|
||||
Rect.makeXYWH(inX, inY, inW, inH), vPaint)
|
||||
vPaint.close()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
for (vImg in fCache.values) vImg?.close()
|
||||
fCache.clear()
|
||||
for (vImg in fSvgRasterCache.values) vImg?.close()
|
||||
fSvgRasterCache.clear()
|
||||
fSvgIntrinsic.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: SVG rasterisation (shared with SkiaEncodedImageDecoder)
|
||||
// ==================
|
||||
|
||||
/** SVGDOM → offscreen raster → Image. Falls back to a 100×100 canvas when
|
||||
the document declares no explicit width/height (the Android-vector path
|
||||
always supplies them). */
|
||||
/** Size-driven SVG raster: renders the vector into a [inWpx]×[inHpx] surface by
|
||||
scaling the intrinsic-sized document up to the target, so a vector drawn larger
|
||||
than its intrinsic box stays crisp (vector render at target resolution) instead
|
||||
of upscaling a small raster. Canvas-scale (rather than only setContainerSize)
|
||||
keeps it correct whether or not the document declares a viewBox. */
|
||||
internal fun rasterizeSvgAt(inBytes: ByteArray, inIntrinsicW: Float, inIntrinsicH: Float, inWpx: Int, inHpx: Int): Image? = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(inBytes))
|
||||
val vIw = inIntrinsicW.takeIf { it > 0f } ?: 100f
|
||||
val vIh = inIntrinsicH.takeIf { it > 0f } ?: 100f
|
||||
vDom.setContainerSize(vIw, vIh)
|
||||
val vSurface = Surface.makeRasterN32Premul(inWpx, inHpx)
|
||||
val vCanvas = vSurface.canvas
|
||||
vCanvas.scale(inWpx / vIw, inHpx / vIh)
|
||||
vDom.render(vCanvas)
|
||||
val vImage = vSurface.makeImageSnapshot()
|
||||
vDom.close()
|
||||
vSurface.close()
|
||||
vImage
|
||||
}.getOrNull()
|
||||
|
||||
internal fun rasterizeSvg(inBytes: ByteArray): Image? = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(inBytes))
|
||||
val vRoot = vDom.root
|
||||
var vW = vRoot?.width?.value ?: 0f
|
||||
var vH = vRoot?.height?.value ?: 0f
|
||||
if (vW <= 0f) vW = 100f
|
||||
if (vH <= 0f) vH = 100f
|
||||
vDom.setContainerSize(vW, vH)
|
||||
val vSurface = Surface.makeRasterN32Premul(vW.toInt().coerceAtLeast(1), vH.toInt().coerceAtLeast(1))
|
||||
vDom.render(vSurface.canvas)
|
||||
vSurface.makeImageSnapshot()
|
||||
}.getOrNull()
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.compose.sdl.renderer.skia
|
||||
|
||||
// ==================
|
||||
// MARK: SkiaLeafDrawer — port text/image renderers behind the upstream canvas (B6.1)
|
||||
// ==================
|
||||
|
||||
/** The Skia leg draws through upstream's `SkiaBackedCanvas` (real gradients/paint/
|
||||
shader). Text now goes through the skiko paragraph engine (Paragraph.paint), so
|
||||
this drawer only holds the resource-image cache (`SkiaImageCache`) that
|
||||
`SkiaBackedCanvas`'s `NativePainterCanvas` contract forwards to. Set once by
|
||||
`SkiaRenderBackend`; any `SkiaBackedCanvas` (frame, offscreen, or a GraphicsLayer
|
||||
recording — B6.2) can then draw images onto its own `internalSkiaCanvas`. */
|
||||
var skiaLeafDrawer: SkiaLeafDrawer? = null
|
||||
|
||||
class SkiaLeafDrawer(
|
||||
val imageCache: SkiaImageCache,
|
||||
)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.compose.sdl.renderer.skia
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import com.compose.sdl.graphics.EncodedImageDecoder
|
||||
import org.jetbrains.skia.Data
|
||||
import org.jetbrains.skia.Image
|
||||
import org.jetbrains.skia.svg.SVGDOM
|
||||
|
||||
// ==================
|
||||
// MARK: SkiaEncodedImageDecoder
|
||||
// ==================
|
||||
|
||||
/** Encoded-image decode hook impl for the Skia renderer, consumed by
|
||||
:components-resources' actuals (painterResource / SVG). Raster formats go
|
||||
through Image.makeFromEncoded; when that fails the bytes are retried as SVG
|
||||
through the same SVGDOM rasterisation SkiaImageCache uses. Pure CPU raster
|
||||
(no GrContext), so it is safe on the resources pipeline's Dispatchers.Default
|
||||
workers.
|
||||
|
||||
B6.1: produces an UPSTREAM ImageBitmap (Image.toComposeImageBitmap) — the Skia
|
||||
renderer uses upstream's SkiaBackedCanvas/SkiaImageAsset, so the offscreen +
|
||||
ImageBitmap-backed paths go through upstream's own actuals (the project
|
||||
SkiaImageBitmap / SkiaOffscreenRenderer are retired). */
|
||||
class SkiaEncodedImageDecoder : EncodedImageDecoder {
|
||||
|
||||
override fun decode(inBytes: ByteArray): ImageBitmap? {
|
||||
if (inBytes.isEmpty()) return null
|
||||
val vImg = runCatching { Image.makeFromEncoded(inBytes) }.getOrNull()
|
||||
?: rasterizeSvg(inBytes)
|
||||
?: return null
|
||||
val vBmp = vImg.toComposeImageBitmap()
|
||||
vImg.close()
|
||||
return vBmp
|
||||
}
|
||||
|
||||
override fun svgIntrinsicSize(inBytes: ByteArray): Size? = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(inBytes))
|
||||
val vRoot = vDom.root
|
||||
val vW = (vRoot?.width?.value ?: 0f).takeIf { it > 0f } ?: 100f
|
||||
val vH = (vRoot?.height?.value ?: 0f).takeIf { it > 0f } ?: 100f
|
||||
vDom.close()
|
||||
Size(vW, vH)
|
||||
}.getOrNull()
|
||||
|
||||
override fun decodeSvgAt(inBytes: ByteArray, inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
||||
if (inBytes.isEmpty() || inWidthPx <= 0 || inHeightPx <= 0) return null
|
||||
val vSize = svgIntrinsicSize(inBytes) ?: return null
|
||||
val vImg = rasterizeSvgAt(inBytes, vSize.width, vSize.height, inWidthPx, inHeightPx) ?: return null
|
||||
val vBmp = vImg.toComposeImageBitmap()
|
||||
vImg.close()
|
||||
return vBmp
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
// :ui-text — androidx.compose.ui.text.* split out of :ui (upstream layout).
|
||||
// AnnotatedString / TextStyle / Paragraph / FontFamily + the B6.3 skiko text
|
||||
// engine (SkiaParagraph / SkiaParagraphOps / SkiaFonts, IconFont). SDL-free —
|
||||
// depends on :ui-graphics (Canvas), skiko, and the low ui primitives.
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
}
|
||||
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64()
|
||||
linuxX64()
|
||||
macosArm64()
|
||||
if (vHostSupportsMingw) mingwX64()
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
kotlin.srcDir("src/vendor/common/kotlin")
|
||||
dependencies {
|
||||
api(project(":ui-graphics"))
|
||||
api(project(":ui-unit"))
|
||||
api(project(":ui-util"))
|
||||
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||
api("org.jetbrains.compose.runtime:runtime-saveable:${libs.versions.composeRuntime.get()}")
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
}
|
||||
nativeMain {
|
||||
kotlin.srcDir("src/vendor/native/kotlin")
|
||||
}
|
||||
|
||||
val skikoRendererMain = create("skikoRendererMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
|
||||
dependencies {
|
||||
implementation(libs.skiko)
|
||||
}
|
||||
}
|
||||
val skikoRendererMacosMain = create("skikoRendererMacosMain") { dependsOn(skikoRendererMain) }
|
||||
val skikoRendererLinuxMain = create("skikoRendererLinuxMain") { dependsOn(skikoRendererMain) }
|
||||
get("macosArm64Main").dependsOn(skikoRendererMacosMain)
|
||||
get("linuxX64Main").dependsOn(skikoRendererLinuxMain)
|
||||
get("linuxArm64Main").dependsOn(skikoRendererLinuxMain)
|
||||
|
||||
if (vHostSupportsMingw) {
|
||||
val skikoRendererMingwSharedMain = create("skikoRendererMingwSharedMain") {
|
||||
dependsOn(nativeMain.get())
|
||||
kotlin.srcDir("src/skikoRendererMain/kotlin")
|
||||
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
|
||||
dependencies {
|
||||
implementation("com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse("0.150.1-mingw.1")}")
|
||||
}
|
||||
}
|
||||
val skikoRendererMingwMain = create("skikoRendererMingwMain") { dependsOn(skikoRendererMingwSharedMain) }
|
||||
get("mingwX64Main").dependsOn(skikoRendererMingwMain)
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xcollection-literals",
|
||||
"-Xexpect-actual-classes",
|
||||
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
|
||||
"-opt-in=androidx.compose.ui.InternalComposeUiApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
# ACCEPTED DEVIATION — do NOT try to "fill" the skikoMain text-engine gaps below.
|
||||
# The upstream skikoMain text stack (ParagraphBuilder / ParagraphLayouter /
|
||||
# PlatformFont / FontCache / SkiaParagraph.skiko + siblings, ~17 files) is
|
||||
# intentionally UNSELECTED (it shows up in the DIAGNOSTIC GAPS tail). The port
|
||||
# instead ships a reduced local engine: a skiko-free SkiaParagraph.native.kt over
|
||||
# SkiaParagraphEngine.kt (skikoRendererMain), which keeps the project's own
|
||||
# SkiaFonts (name->bytes + variable-axis) font model instead of upstream's
|
||||
# PlatformFont/FontCache/FontCollection. Reason: the flat nativeMain source-set
|
||||
# layout can't host upstream's skiko/nonJvm-split actuals as-is (see the memory
|
||||
# note "flat nativeMain vs upstream skiko+nonJvm hierarchy"). This is the single
|
||||
# biggest "not upstream" surface and the root of the RTL / stroke-DrawStyle /
|
||||
# grapheme reductions — those are tracked in PLAN.md §2, not fixed by vendoring
|
||||
# these files. If you select them, expect the source-set hierarchy to fight you.
|
||||
|
||||
SET_REPO=https://github.com/JetBrains/compose-multiplatform-core@<COMPOSE_CORE_REF>
|
||||
SET_FOLDER=compose/ui/ui-text/src
|
||||
|
||||
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/AnnotatedString.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/AtomicReference.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/AtomicReference.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/Bullet.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/Bullet.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/CharHelpers.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/CharHelpers.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/InternalTextApi.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/MultiParagraph.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/Paragraph.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/ParagraphIntrinsics.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/ParagraphIntrinsics.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/Placeholder.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/PlatformTextStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/PlatformTextStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/Savers.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/Savers.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/SpanStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/SpanStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/String.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/String.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/StringAnnotation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/StringAnnotation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextGranularity.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextGranularity.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextInclusionStrategy.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextInclusionStrategy.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextLayoutResult.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextLayoutResult.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextLinkStyles.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextLinkStyles.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextMeasurer.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextPainter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextRange.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextRange.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/TtsAnnotation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TtsAnnotation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/UrlAnnotation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/UrlAnnotation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/Font.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/Font.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontFamily.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontFamilyTypefaceAdapter.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontFamilyTypefaceAdapter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontMatcher.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontMatcher.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontVariation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontVariation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/FontWeight.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/FontWeight.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/font/Typeface.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/font/Typeface.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/EditCommand.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/EditProcessor.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/EditProcessor.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/GapBuffer.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/GapBuffer.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/ImeAction.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/ImeAction.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/ImeOptions.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/ImeOptions.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/InputEventCallback.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/InputEventCallback.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/KeyboardCapitalization.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/KeyboardCapitalization.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/KeyboardType.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/KeyboardType.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/MathUtils.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/OffsetMapping.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/OffsetMapping.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/TextFieldValue.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/TextFieldValue.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/TextInputService.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/input/VisualTransformation.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/input/VisualTransformation.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/internal/InlineClassHelper.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/internal/InlineClassHelper.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/intl/Locale.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/intl/Locale.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/platform/Dispatcher.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/platform/Dispatcher.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/platform/PlatformMultiParagraphDraw.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/platform/PlatformMultiParagraphDraw.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/platform/PlatformString.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/platform/PlatformString.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/platform/Synchronization.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/platform/Synchronization.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/BaselineShift.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/BaselineShift.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/Hyphens.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/Hyphens.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/LineBreak.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/ResolvedTextDirection.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/ResolvedTextDirection.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextAlign.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextAlign.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextDecoration.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextDecoration.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextDirection.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextDirection.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextGeometricTransform.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextGeometricTransform.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextIndent.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextIndent.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextMotion.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextMotion.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
|
||||
!commonMain/kotlin/androidx/compose/ui/text/platform/Synchronization.kt
|
||||
|
||||
nativeMain/kotlin/androidx/compose/ui/text/WeakReference.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/WeakReference.native.kt
|
||||
nativeMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/font/FontSynthesis.native.kt
|
||||
|
||||
nonJvmMain/kotlin/ -> src/vendor/native/kotlin/
|
||||
# | nonJvmMain/kotlin/androidx/compose/ui/text/AnnotatedString.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/AnnotatedString.nonJvm.kt
|
||||
# | nonJvmMain/kotlin/androidx/compose/ui/text/AtomicReference.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/AtomicReference.nonJvm.kt
|
||||
# | nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt
|
||||
# | nonJvmMain/kotlin/androidx/compose/ui/text/input/GapBuffer.nonJvmMain.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/input/GapBuffer.nonJvmMain.kt
|
||||
# | nonJvmMain/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.nonJvmNative.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.nonJvmNative.kt
|
||||
!nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt
|
||||
|
||||
skikoMain/kotlin/androidx/compose/ui/text/Paragraph.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/Paragraph.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/Savers.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/Savers.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/TextStyle.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/TextStyle.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/WeakReference.skiko.kt -> src/vendor/common/kotlin/androidx/compose/ui/text/WeakReference.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/style/LineBreak.native.kt
|
||||
skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/style/TextMotion.native.kt
|
||||
|
||||
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||
# androidMain (77)
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/AndroidComposeUiTextFlags.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/AndroidComposeUiTextFlags.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/CharHelpers.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/CharHelpers.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/EmojiSupportMatch.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/EmojiSupportMatch.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/Html.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/Html.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/Paragraph.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/Paragraph.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/ParagraphIntrinsics.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/ParagraphIntrinsics.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/Savers.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/Savers.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/CharSequenceCharacterIterator.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/CharSequenceCharacterIterator.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/InlineClassUtils.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/InlineClassUtils.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/InternalPlatformTextApi.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/InternalPlatformTextApi.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/LayoutCompat.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/LayoutCompat.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/LayoutHelper.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/LayoutHelper.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/LayoutIntrinsics.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/LayoutIntrinsics.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/PaintExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/PaintExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/SpannedExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/SpannedExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/TextAndroidCanvas.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/TextAndroidCanvas.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/TextLayoutGetRangeForRectExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/TextLayoutGetRangeForRectExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/selection/SegmentFinder.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/selection/SegmentFinder.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/selection/WordBoundary.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/selection/WordBoundary.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/BaselineShiftSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/BaselineShiftSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/FontFeatureSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/FontFeatureSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/IndentationFixSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/IndentationFixSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/LineHeightSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/ShadowSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/ShadowSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/SkewXSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/SkewXSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/TextDecorationSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/TextDecorationSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/android/style/TypefaceSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/android/style/TypefaceSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidFont.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidFontLoader.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidFontLoader.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidFontResolveInterceptor.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidFontResolveInterceptor.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidFontUtils.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidFontUtils.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/AndroidTypeface.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/AndroidTypeface.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/FontSynthesis.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/PlatformFontVariationSettings.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/PlatformFontVariationSettings.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/font/PlatformTypefaces.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/font/PlatformTypefaces.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/intl/AndroidPlatformLocale.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/intl/AndroidPlatformLocale.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidDefaultTypeface.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidDefaultTypeface.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidGenericFontFamilyTypeface.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidGenericFontFamilyTypeface.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidMultiParagraphDraw.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidMultiParagraphDraw.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidStringDelegate.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidStringDelegate.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTypeface.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidTypeface.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceWrapper.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/AndroidTypefaceWrapper.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/Synchronization.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/Synchronization.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/extensions/LocaleExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/extensions/LocaleExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/extensions/PlaceholderExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/extensions/PlaceholderExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TtsAnnotationExtensions.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/extensions/TtsAnnotationExtensions.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/style/BulletSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/style/BulletSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/style/DrawStyleSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/style/DrawStyleSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/platform/style/ShaderBrushSpan.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/platform/style/ShaderBrushSpan.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
|
||||
# | androidMain/kotlin/androidx/compose/ui/text/style/TextMotion.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/text/style/TextMotion.android.kt
|
||||
# darwinMain (2)
|
||||
# | darwinMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt
|
||||
# | darwinMain/kotlin/androidx/compose/ui/text/platform/NativeStringDelegate.darwin.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/platform/NativeStringDelegate.darwin.kt
|
||||
# desktopMain (15)
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/Cache.jvm.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/Cache.jvm.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/WeakReference.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/WeakReference.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/font/FontSynthesis.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopStringDelegate.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/DesktopStringDelegate.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt
|
||||
# | desktopMain/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt
|
||||
# iosMain (2)
|
||||
# | iosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.ios.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.ios.kt
|
||||
# | iosMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.ios.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.ios.kt
|
||||
# jsMain (1)
|
||||
# | jsMain/kotlin/androidx/compose/ui/text/intl/Actuals.js.kt -> src/vendor/js/kotlin/androidx/compose/ui/text/intl/Actuals.js.kt
|
||||
# jvmAndAndroidMain (5)
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/text/ActualAtomicReferenceJvm.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/text/ActualAtomicReferenceJvm.jvmAndAndroid.kt
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvmAndAndroid.kt
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/text/input/GapBuffer.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/text/input/GapBuffer.jvmAndAndroid.kt
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/text/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/text/intl/Locale.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/text/intl/Locale.jvmAndAndroid.kt
|
||||
# macosMain (1)
|
||||
# | macosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.macos.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.macos.kt
|
||||
# nativeMain (4)
|
||||
# | nativeMain/kotlin/androidx/compose/ui/text/Cache.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/Cache.native.kt
|
||||
# | nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt
|
||||
# | nativeMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt
|
||||
# | nativeMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt -> src/vendor/native/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt
|
||||
# skikoMain (17)
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/ActualParagraph.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/ActualParagraph.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/Cache.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/Cache.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/CharHelpers.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/CharHelpers.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/ParagraphIntrisics.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/intl/PlatformLocale.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphBuilder.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/ParagraphBuilder.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsics.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsics.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaTextPaint.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/SkiaTextPaint.skiko.kt
|
||||
# | skikoMain/kotlin/androidx/compose/ui/text/platform/Synchronization.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/text/platform/Synchronization.skiko.kt
|
||||
# wasmJsMain (1)
|
||||
# | wasmJsMain/kotlin/androidx/compose/ui/text/intl/Actuals.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/text/intl/Actuals.wasm.kt
|
||||
# webMain (10)
|
||||
# | webMain/kotlin/androidx/compose/ui/text/Cache.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/Cache.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/WeakReference.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/WeakReference.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/font/FontSynthesis.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/intl/PreferredLanguages.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/intl/PreferredLanguages.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/platform/WebFont.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/platform/WebFont.kt
|
||||
# | webMain/kotlin/androidx/compose/ui/text/platform/WebStringDelegate.kt -> src/vendor/web/kotlin/androidx/compose/ui/text/platform/WebStringDelegate.kt
|
||||
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||
+6
-6
@@ -20,9 +20,9 @@ object IconFont {
|
||||
|
||||
private val fFonts = mutableMapOf<String, ByteArray>()
|
||||
// Families that render as single variable-axis glyphs (Material Symbols),
|
||||
// vs ordinary text fonts. The SDL3 renderer routes only these through its
|
||||
// single-glyph FreeType icon path; text families (e.g. a bundled
|
||||
// monospace) must go through normal full-string rendering.
|
||||
// vs ordinary text fonts — icon families are drawn one codepoint at a time,
|
||||
// while text families (e.g. a bundled monospace) go through normal
|
||||
// full-string rendering.
|
||||
private val fIconFamilies = mutableSetOf<String>()
|
||||
|
||||
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
|
||||
@@ -32,9 +32,9 @@ object IconFont {
|
||||
fFonts[inFamily] = inBytes
|
||||
}
|
||||
|
||||
/** Register an ICON font family (Material Symbols et al.) — variable-axis,
|
||||
drawn one glyph at a time on the SDL3 backend. Same byte store as
|
||||
register(), but also flags the family as an icon font. */
|
||||
/** Register an ICON font family (Material Symbols et al.) — variable-axis.
|
||||
Same byte store as register(), but also flags the family as an icon
|
||||
font. */
|
||||
fun registerIcon(inFamily: String, inBytes: ByteArray) {
|
||||
fFonts[inFamily] = inBytes
|
||||
fIconFamilies += inFamily
|
||||
+19
-2
@@ -30,16 +30,26 @@ class NamedFont(
|
||||
override val weight: FontWeight = FontWeight.Normal,
|
||||
override val style: FontStyle = FontStyle.Normal,
|
||||
val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
|
||||
// Raw variable-font axis settings (Material Symbols FILL/wght/GRAD/opsz),
|
||||
// threaded through the family so the skiko text engine can apply them via
|
||||
// FontFamily.projectFontVariations() — icons render on the standard text path.
|
||||
val axes: List<FontVariation.Setting>? = null,
|
||||
) : Font {
|
||||
override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is NamedFont && other.name == name && other.weight == weight && other.style == style
|
||||
other is NamedFont && other.name == name && other.weight == weight && other.style == style &&
|
||||
other.axes == axes && other.variationSettings == variationSettings
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var h = name.hashCode()
|
||||
h = 31 * h + weight.hashCode()
|
||||
h = 31 * h + style.hashCode()
|
||||
// Axis settings (Material Symbols FILL/wght/GRAD/opsz) are part of identity —
|
||||
// without them two axis-differing icon fonts compare equal and collide in the
|
||||
// FontFamily → typeface cache, rendering one icon's axes for the other.
|
||||
h = 31 * h + (axes?.hashCode() ?: 0)
|
||||
h = 31 * h + variationSettings.hashCode()
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -52,7 +62,8 @@ fun namedFontFamily(
|
||||
name: String,
|
||||
weight: FontWeight = FontWeight.Normal,
|
||||
style: FontStyle = FontStyle.Normal,
|
||||
): FontFamily = FontFamily(NamedFont(name, weight, style))
|
||||
axes: List<FontVariation.Setting>? = null,
|
||||
): FontFamily = FontFamily(NamedFont(name, weight, style, axes = axes))
|
||||
|
||||
/**
|
||||
* Extract the renderer font-registry name for a `FontFamily`:
|
||||
@@ -69,3 +80,9 @@ fun FontFamily?.projectFontName(): String? = when (this) {
|
||||
if (name == "sans-serif") null else "generic:$name"
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** Variable-font axis settings carried by a [namedFontFamily]'s [NamedFont]
|
||||
* (Material Symbols FILL/wght/GRAD/opsz); null for every other family. The
|
||||
* skiko text engine applies these on top of / instead of the paragraph weight. */
|
||||
fun FontFamily?.projectFontVariations(): List<FontVariation.Setting>? =
|
||||
((this as? FontListFontFamily)?.firstOrNull() as? NamedFont)?.axes
|
||||
+2
-3
@@ -10,9 +10,8 @@ package com.compose.sdl.text
|
||||
something the renderer can't deliver. Today the only consumer is the
|
||||
Material Symbols install() path, which emits a one-shot warning when
|
||||
variable-font axes won't be honoured. The Skia renderer sets
|
||||
supportsFontVariations = true via Typeface.makeClone; the SDL3 renderer
|
||||
leaves it false because SDL3_ttf 3.2 has no axis-set API (filename /
|
||||
iostream / size / face / dpi only).
|
||||
supportsFontVariations = true via Typeface.makeClone; a renderer that
|
||||
couldn't honour variable-font axes would leave it false.
|
||||
|
||||
`supportsFontVariations` is null until a renderer initialises — code
|
||||
that warns on install() ignores the null case so it doesn't fire when
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.compose.sdl.text
|
||||
|
||||
// ==================
|
||||
// MARK: Viewport size (project-only)
|
||||
// ==================
|
||||
|
||||
/** Logical size of the window, set by the render loop each frame. Lets commonMain
|
||||
composables (selection highlights cull per-line work, DropdownMenu flips/clamps
|
||||
itself) read the viewport without a hard dependency on the window layer.
|
||||
0 until first set — callers treat that as "viewport unknown". */
|
||||
var currentViewportHeight: Int = 0
|
||||
var currentViewportWidth: Int = 0
|
||||
+1
-1
@@ -20,7 +20,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
SDL_TEXT_INPUT_EVENT), replace this with a real implementation.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
internal object NoOpPlatformTextInputService : PlatformTextInputService {
|
||||
object NoOpPlatformTextInputService : PlatformTextInputService {
|
||||
override fun startInput(
|
||||
value: TextFieldValue,
|
||||
imeOptions: ImeOptions,
|
||||
+17
-9
@@ -1,18 +1,25 @@
|
||||
package androidx.compose.ui.text
|
||||
|
||||
import kotlin.experimental.ExperimentalNativeApi
|
||||
|
||||
// VENDOR-BASE: compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt @ v1.12.0-beta03+dev4483
|
||||
|
||||
// ==================
|
||||
// MARK: FontRasterizationSettings native actual (Skia-free)
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* Hand-written port of upstream `FontRasterizationSettings.skiko.kt` minus
|
||||
* the Skia-only `toSkFontEdging` / `toSkFontHinting` extensions and the
|
||||
* `currentPlatform()` lookup. Vendored TextStyle.native.kt references
|
||||
* `FontRasterizationSettings.PlatformDefault` so we need the class +
|
||||
* companion to exist; the renderer doesn't read these values yet so a
|
||||
* single shared "PlatformDefault" instance (AntiAlias / Normal /
|
||||
* subpixel-on / autohint-off — matches upstream's macOS/Linux default)
|
||||
* is fine.
|
||||
* Rule-3 manual vendor of upstream `FontRasterizationSettings.skiko.kt` minus the
|
||||
* Skia-only `toSkFontEdging` / `toSkFontHinting` extensions (the skiko renderer
|
||||
* maps `PlatformDefault` to skiko enums itself, in SkiaParagraphEngine). It lives
|
||||
* in the skiko-FREE `nativeMain` because vendored `TextStyle.native.kt` references
|
||||
* `FontRasterizationSettings.PlatformDefault`, and `nativeMain` is the shared parent
|
||||
* of the official-skiko (mac/linux) and fork-skiko (mingw) legs — so it can't carry
|
||||
* skiko. `PlatformDefault` reproduces upstream's per-OS defaults VERBATIM (verified
|
||||
* against the pin): anti-aliased + subpixel everywhere, hinting Slight on Linux and
|
||||
* Normal on Windows/macOS (macOS ignores hinting). This split — and the hand port —
|
||||
* goes away if the port hosts the `Paragraph` actual in the skiko source set and
|
||||
* vendors upstream's skiko text files verbatim (PLAN.md §6).
|
||||
*/
|
||||
|
||||
@ExperimentalTextApi
|
||||
@@ -38,9 +45,10 @@ class FontRasterizationSettings(
|
||||
val autoHintingForced: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
@OptIn(ExperimentalNativeApi::class)
|
||||
val PlatformDefault: FontRasterizationSettings = FontRasterizationSettings(
|
||||
smoothing = FontSmoothing.AntiAlias,
|
||||
hinting = FontHinting.Normal,
|
||||
hinting = if (Platform.osFamily == OsFamily.LINUX) FontHinting.Slight else FontHinting.Normal,
|
||||
subpixelPositioning = true,
|
||||
autoHintingForced = false,
|
||||
)
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package androidx.compose.ui.text
|
||||
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
|
||||
// ==================
|
||||
// MARK: ParagraphIntrinsics + Paragraph factory actuals (skiko engine)
|
||||
// ==================
|
||||
//
|
||||
// The `Paragraph()` / `ParagraphIntrinsics()` factories (expects in vendored
|
||||
// commonMain) resolve here to the skiko-backed engine. Because `:ui`'s native
|
||||
// hierarchy is nativeMain -> skiko (inverted vs upstream skiko -> native), the
|
||||
// real SkiaParagraph lives in the child skiko source set; these parent-level
|
||||
// actuals bridge to it through `makeSkiaParagraph` / `paragraphIntrinsicWidths`,
|
||||
// whose actuals sit in skikoRendererMain (compiled into both the official-skiko
|
||||
// and mingw-fork siblings). Same pattern as createRenderBackend.
|
||||
|
||||
/** Construct the nativeMain [SkiaParagraph] over a skiko-backed ops seam. */
|
||||
private fun makeSkiaParagraph(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
width: Float,
|
||||
maxLines: Int,
|
||||
ellipsize: Boolean,
|
||||
density: Float,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
): Paragraph = SkiaParagraph(
|
||||
text, style, width,
|
||||
buildParagraphOps(text, style, width, maxLines, ellipsize, density, spanStyles, placeholders),
|
||||
)
|
||||
|
||||
/** Carries text+style for the intrinsics-based Paragraph factories; intrinsic
|
||||
widths come from an unbounded skiko layout. `density` is the LocalDensity
|
||||
scalar (dpr on Retina): it converts sp → pixels so intrinsic widths land in
|
||||
the same physical-pixel space the layout tree measures in. */
|
||||
internal class NativeParagraphIntrinsics(
|
||||
val paragraphText: String,
|
||||
val paragraphStyle: TextStyle,
|
||||
val density: Float,
|
||||
val spanStyles: List<AnnotatedString.Range<SpanStyle>> = emptyList(),
|
||||
val placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList(),
|
||||
) : ParagraphIntrinsics {
|
||||
// Shape ONCE here (unbounded, no line cap). The shaped paragraph is RETAINED
|
||||
// and reused by Paragraph(this, …) for the final layout — skiko re-breaks at
|
||||
// the final width without re-shaping — so measured text shapes once, not twice
|
||||
// (the P1.2 double-shape: cold text frames were dominated by shaping).
|
||||
val ops: NativeParagraphOps = buildParagraphOps(
|
||||
paragraphText, paragraphStyle, Float.POSITIVE_INFINITY, Int.MAX_VALUE, false, density, spanStyles, placeholders,
|
||||
)
|
||||
override val minIntrinsicWidth: Float = ops.minIntrinsicWidth
|
||||
override val maxIntrinsicWidth: Float = ops.maxIntrinsicWidth
|
||||
override val hasStaleResolvedFonts: Boolean = false
|
||||
}
|
||||
|
||||
/** Build the final Paragraph from a pre-shaped intrinsics. Reuse the intrinsics'
|
||||
shaped paragraph (just re-break at [width]) when there's no line cap / ellipsis
|
||||
— those are baked into the paragraph at build time and need a fresh build. */
|
||||
private fun paragraphFromIntrinsics(
|
||||
i: NativeParagraphIntrinsics, width: Float, maxLines: Int, ellipsize: Boolean,
|
||||
): Paragraph {
|
||||
if (maxLines == Int.MAX_VALUE && !ellipsize) {
|
||||
i.ops.relayout(width)
|
||||
return SkiaParagraph(i.paragraphText, i.paragraphStyle, width, i.ops)
|
||||
}
|
||||
return makeSkiaParagraph(
|
||||
i.paragraphText, i.paragraphStyle, width, maxLines, ellipsize, i.density, i.spanStyles, i.placeholders,
|
||||
)
|
||||
}
|
||||
|
||||
private fun widthFrom(constraints: Constraints): Float =
|
||||
if (constraints.hasBoundedWidth) constraints.maxWidth.toFloat() else Float.POSITIVE_INFINITY
|
||||
|
||||
// ---- ParagraphIntrinsics factories ----
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
actual fun ParagraphIntrinsics(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
density: Density,
|
||||
resourceLoader: Font.ResourceLoader,
|
||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles, placeholders)
|
||||
|
||||
actual fun ParagraphIntrinsics(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles, placeholders)
|
||||
|
||||
actual fun ParagraphIntrinsics(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
annotations: List<AnnotatedString.Range<out AnnotatedString.Annotation>>,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles(), placeholders)
|
||||
|
||||
actual fun ParagraphIntrinsics(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
annotations: List<AnnotatedString.Range<out AnnotatedString.Annotation>>,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
softWrap: Boolean,
|
||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles(), placeholders)
|
||||
|
||||
// AnnotatedString.Annotation is a sealed interface (SpanStyle, ParagraphStyle,
|
||||
// LinkAnnotation, StringAnnotation, TtsAnnotation, …). Only SpanStyle affects
|
||||
// glyph layout/painting.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun List<AnnotatedString.Range<out AnnotatedString.Annotation>>.filterSpanStyles():
|
||||
List<AnnotatedString.Range<SpanStyle>> =
|
||||
filter { it.item is SpanStyle } as List<AnnotatedString.Range<SpanStyle>>
|
||||
|
||||
// ---- Paragraph factories ----
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
actual fun Paragraph(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
maxLines: Int,
|
||||
ellipsis: Boolean,
|
||||
width: Float,
|
||||
density: Density,
|
||||
resourceLoader: Font.ResourceLoader,
|
||||
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||
|
||||
actual fun Paragraph(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
width: Float,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
maxLines: Int,
|
||||
ellipsis: Boolean,
|
||||
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||
|
||||
actual fun Paragraph(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
constraints: Constraints,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
maxLines: Int,
|
||||
ellipsis: Boolean,
|
||||
): Paragraph = makeSkiaParagraph(text, style, widthFrom(constraints), maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||
|
||||
actual fun Paragraph(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
constraints: Constraints,
|
||||
density: Density,
|
||||
fontFamilyResolver: FontFamily.Resolver,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
maxLines: Int,
|
||||
overflow: TextOverflow,
|
||||
): Paragraph = makeSkiaParagraph(
|
||||
text, style, widthFrom(constraints), maxLines, overflow == TextOverflow.Ellipsis, density.density, spanStyles, placeholders,
|
||||
)
|
||||
|
||||
actual fun Paragraph(
|
||||
paragraphIntrinsics: ParagraphIntrinsics,
|
||||
maxLines: Int,
|
||||
ellipsis: Boolean,
|
||||
width: Float,
|
||||
): Paragraph =
|
||||
paragraphFromIntrinsics(paragraphIntrinsics as NativeParagraphIntrinsics, width, maxLines, ellipsis)
|
||||
|
||||
actual fun Paragraph(
|
||||
paragraphIntrinsics: ParagraphIntrinsics,
|
||||
constraints: Constraints,
|
||||
maxLines: Int,
|
||||
ellipsis: Boolean,
|
||||
): Paragraph =
|
||||
paragraphFromIntrinsics(paragraphIntrinsics as NativeParagraphIntrinsics, widthFrom(constraints), maxLines, ellipsis)
|
||||
|
||||
actual fun Paragraph(
|
||||
paragraphIntrinsics: ParagraphIntrinsics,
|
||||
constraints: Constraints,
|
||||
maxLines: Int,
|
||||
overflow: TextOverflow,
|
||||
): Paragraph =
|
||||
paragraphFromIntrinsics(
|
||||
paragraphIntrinsics as NativeParagraphIntrinsics, widthFrom(constraints), maxLines,
|
||||
overflow == TextOverflow.Ellipsis,
|
||||
)
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package androidx.compose.ui.text
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.text.style.ResolvedTextDirection
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import kotlin.math.floor
|
||||
|
||||
// ==================
|
||||
// MARK: SkiaParagraph — nativeMain Paragraph over a skiko-backed ops seam
|
||||
// ==================
|
||||
//
|
||||
// `Paragraph` is a sealed interface whose actual is declared in nativeMain, so
|
||||
// its implementers must live here (skiko-tied code in the child skikoRenderer
|
||||
// source set counts as a "different module" for the sealed check). This class
|
||||
// therefore holds no skiko types: it drives the real skiko skparagraph through
|
||||
// [NativeParagraphOps] (impl in skikoRendererMain), exchanging plain data
|
||||
// ([TextBoxData] / [LineMetricData] / [RectData]). Cursor/selection/line-metric
|
||||
// math is adapted from upstream `SkiaParagraph.skiko.kt` (@ CMP core v1.12.x).
|
||||
|
||||
/** Plain mirror of a skiko `TextBox` (a glyph-run rect + its direction). */
|
||||
internal class TextBoxData(
|
||||
val left: Float, val top: Float, val right: Float, val bottom: Float, val isRtl: Boolean,
|
||||
)
|
||||
|
||||
/** Plain mirror of a skiko paragraph `LineMetrics`. */
|
||||
internal class LineMetricData(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
val endExcludingWhitespaces: Int,
|
||||
val endIncludingNewline: Int,
|
||||
val isHardBreak: Boolean,
|
||||
val ascent: Double,
|
||||
val descent: Double,
|
||||
val baseline: Double,
|
||||
val left: Double,
|
||||
val right: Double,
|
||||
val width: Double,
|
||||
val height: Double,
|
||||
val lineNumber: Int,
|
||||
)
|
||||
|
||||
/** Plain mirror of a Compose/skiko rect. */
|
||||
internal class RectData(val left: Float, val top: Float, val right: Float, val bottom: Float) {
|
||||
fun toComposeRect() = Rect(left, top, right, bottom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skiko-free view of one laid-out skiko paragraph. Implemented in
|
||||
* skikoRendererMain (SkiaParagraphOps) where skiko is on the classpath; all
|
||||
* exchange types are plain so this interface can live in nativeMain beside the
|
||||
* sealed [Paragraph]. [rebuildAndPaint] re-lays-out with paint-time overrides
|
||||
* (Compose paints text with the resolved colour/shadow/decoration) then draws.
|
||||
*/
|
||||
internal interface NativeParagraphOps {
|
||||
val height: Float
|
||||
val lineNumber: Int
|
||||
val minIntrinsicWidth: Float
|
||||
val maxIntrinsicWidth: Float
|
||||
val didExceedMaxLines: Boolean
|
||||
val alphabeticBaseline: Float
|
||||
val defaultAscentPx: Float // positive: -fontMetrics.ascent
|
||||
val defaultDescentPx: Float
|
||||
|
||||
fun lineMetrics(): List<LineMetricData>
|
||||
fun placeholderRects(): List<RectData?>
|
||||
fun getRectsForRange(start: Int, end: Int, useMaxHeight: Boolean): List<TextBoxData>
|
||||
fun glyphPositionAtCoordinate(x: Float, y: Float): Int
|
||||
fun wordBoundary(offset: Int): IntArray // [start, end]
|
||||
|
||||
fun rebuildAndPaint(canvas: Canvas, color: Color, shadow: Shadow?, decoration: TextDecoration?)
|
||||
|
||||
/** Re-break the ALREADY-SHAPED paragraph at a new width (skiko reuses the
|
||||
cached shaped runs — no HarfBuzz re-shape). Used to reuse the intrinsics
|
||||
pass's shaped paragraph for the final layout instead of shaping twice. */
|
||||
fun relayout(width: Float)
|
||||
|
||||
/** Explicitly free the underlying native paragraph. Call when the ops is
|
||||
discarded (e.g. an intrinsics-only throwaway) so the native memory is
|
||||
released deterministically instead of waiting for a GC-driven Cleaner. */
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
/** Bridge to the skiko ops impl (actual in skikoRendererMain). */
|
||||
internal expect fun buildParagraphOps(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
width: Float,
|
||||
maxLines: Int,
|
||||
ellipsize: Boolean,
|
||||
density: Float,
|
||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||
): NativeParagraphOps
|
||||
|
||||
internal class SkiaParagraph(
|
||||
private val text: String,
|
||||
private val style: TextStyle,
|
||||
private val widthConstraint: Float,
|
||||
private val ops: NativeParagraphOps,
|
||||
) : Paragraph {
|
||||
|
||||
private val textDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr
|
||||
|
||||
override val width: Float
|
||||
get() = if (widthConstraint.isFinite()) widthConstraint else ops.maxIntrinsicWidth
|
||||
override val height: Float get() = ops.height
|
||||
override val minIntrinsicWidth: Float get() = ops.minIntrinsicWidth
|
||||
override val maxIntrinsicWidth: Float get() = ops.maxIntrinsicWidth
|
||||
override val firstBaseline: Float get() = lineMetrics.firstOrNull()?.baseline?.toFloat() ?: 0f
|
||||
override val lastBaseline: Float get() = lineMetrics.lastOrNull()?.baseline?.toFloat() ?: 0f
|
||||
override val didExceedMaxLines: Boolean get() = ops.didExceedMaxLines
|
||||
|
||||
override val lineCount: Int
|
||||
get() = if (text == "" || ops.lineNumber < 1) 1 else ops.lineNumber
|
||||
|
||||
override val placeholderRects: List<Rect?>
|
||||
get() = ops.placeholderRects().map { it?.toComposeRect() }
|
||||
|
||||
override fun getPathForRange(start: Int, end: Int): Path {
|
||||
val path = Path()
|
||||
for (b in ops.getRectsForRange(start, end, useMaxHeight = true)) {
|
||||
path.addRect(Rect(b.left, b.top, b.right, b.bottom))
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun getCursorRect(offset: Int): Rect {
|
||||
val horizontal = getHorizontalPosition(offset, true)
|
||||
val line = lineMetricsForOffset(offset) ?: return Rect(horizontal, 0f, horizontal, ops.defaultDescentPx)
|
||||
val isNewEmptyLine = offset - 1 == line.startIndex && offset == text.length
|
||||
val asc = line.ascent.let { if (isNewEmptyLine) it.coerceAtMost(ops.defaultAscentPx.toDouble()) else it }
|
||||
val desc = line.descent.let { if (isNewEmptyLine) it.coerceAtMost(ops.defaultDescentPx.toDouble()) else it }
|
||||
return Rect(horizontal, (line.baseline - asc).toFloat(), horizontal, (line.baseline + desc).toFloat())
|
||||
}
|
||||
|
||||
override fun getLineLeft(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.left?.toFloat() ?: 0f
|
||||
override fun getLineRight(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.right?.toFloat() ?: 0f
|
||||
override fun getLineTop(lineIndex: Int): Float =
|
||||
lineMetrics.getOrNull(lineIndex)?.let { floor((it.baseline - it.ascent).toFloat()) } ?: 0f
|
||||
override fun getLineBottom(lineIndex: Int): Float =
|
||||
lineMetrics.getOrNull(lineIndex)?.let { floor((it.baseline + it.descent).toFloat()) } ?: 0f
|
||||
override fun getLineBaseline(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.baseline?.toFloat() ?: 0f
|
||||
override fun getLineHeight(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.height?.toFloat() ?: 0f
|
||||
override fun getLineWidth(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.width?.toFloat() ?: 0f
|
||||
override fun getLineStart(lineIndex: Int): Int = lineMetrics.getOrNull(lineIndex)?.startIndex ?: 0
|
||||
|
||||
override fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int {
|
||||
val metrics = lineMetrics.getOrNull(lineIndex) ?: return 0
|
||||
return if (visibleEnd) {
|
||||
if (lineIndex > 0 && metrics.startIndex < lineMetrics[lineIndex - 1].endIndex) metrics.endIndex
|
||||
else if (metrics.startIndex < text.length && text[metrics.startIndex] == '\n') metrics.startIndex
|
||||
else metrics.endExcludingWhitespaces
|
||||
} else metrics.endIndex
|
||||
}
|
||||
|
||||
override fun isLineEllipsized(lineIndex: Int): Boolean = false
|
||||
|
||||
override fun getLineForOffset(offset: Int): Int = when {
|
||||
offset < 0 -> 0
|
||||
offset > text.length -> lineCount - 1
|
||||
else -> lineMetricsForOffset(offset)?.lineNumber ?: 0
|
||||
}
|
||||
|
||||
override fun getLineForVerticalPosition(vertical: Float): Int =
|
||||
lineMetricsForVerticalPosition(vertical)?.lineNumber ?: 0
|
||||
|
||||
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float {
|
||||
val prevBox = getBoxBackwardByOffset(offset)
|
||||
val nextBox = getBoxForwardByOffset(offset)
|
||||
val isRtl = textDirection == ResolvedTextDirection.Rtl
|
||||
return when {
|
||||
prevBox == null && nextBox == null -> getAlignedStartingPosition(isRtl)
|
||||
prevBox == null -> nextBox!!.cursorHorizontalPosition(true)
|
||||
nextBox == null -> prevBox.cursorHorizontalPosition()
|
||||
nextBox.isRtl == prevBox.isRtl -> nextBox.cursorHorizontalPosition(true)
|
||||
!isRtl && !prevBox.isRtl -> nextBox.cursorHorizontalPosition(opposite = true)
|
||||
isRtl && prevBox.isRtl -> nextBox.cursorHorizontalPosition(opposite = true)
|
||||
usePrimaryDirection -> prevBox.cursorHorizontalPosition()
|
||||
else -> nextBox.cursorHorizontalPosition(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAlignedStartingPosition(isRtl: Boolean): Float = when (style.textAlign) {
|
||||
TextAlign.Left -> 0f
|
||||
TextAlign.Right -> width
|
||||
TextAlign.Center -> width / 2
|
||||
TextAlign.Start -> if (isRtl) width else 0f
|
||||
TextAlign.End -> if (isRtl) 0f else width
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
override fun getParagraphDirection(offset: Int): ResolvedTextDirection = textDirection
|
||||
|
||||
override fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
|
||||
if (getBoxForwardByOffset(offset)?.isRtl == true) ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
|
||||
|
||||
override fun getOffsetForPosition(position: Offset): Int {
|
||||
val glyphPosition = ops.glyphPositionAtCoordinate(position.x, position.y)
|
||||
val expectedLine = lineMetricsForVerticalPosition(position.y) ?: return glyphPosition
|
||||
if (position.x > expectedLine.left && position.x < expectedLine.right) return glyphPosition
|
||||
val isNotEmptyLine = expectedLine.startIndex < expectedLine.endIndex
|
||||
val rects = if (isNotEmptyLine) ops.getRectsForRange(
|
||||
expectedLine.startIndex,
|
||||
if (expectedLine.isHardBreak) expectedLine.endIndex else expectedLine.endIndex - 1,
|
||||
useMaxHeight = false,
|
||||
) else null
|
||||
val leftX = rects?.firstOrNull()?.left ?: expectedLine.left.toFloat()
|
||||
val rightX = rects?.lastOrNull()?.right ?: expectedLine.right.toFloat()
|
||||
if (leftX == rightX) return glyphPosition
|
||||
return when {
|
||||
position.x <= leftX -> ops.glyphPositionAtCoordinate(leftX + 1f, position.y)
|
||||
position.x >= rightX -> ops.glyphPositionAtCoordinate(rightX - 1f, position.y)
|
||||
else -> glyphPosition
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRangeForRect(rect: Rect, granularity: TextGranularity, inclusionStrategy: TextInclusionStrategy): TextRange =
|
||||
TextRange.Zero
|
||||
|
||||
override fun getBoundingBox(offset: Int): Rect {
|
||||
val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)
|
||||
return box?.let { Rect(it.left, it.top, it.right, it.bottom) } ?: Rect(0f, 0f, 0f, 0f)
|
||||
}
|
||||
|
||||
override fun fillBoundingBoxes(range: TextRange, array: FloatArray, arrayStart: Int) {
|
||||
// TODO(CMP-720): not supported by skiko paragraph yet.
|
||||
}
|
||||
|
||||
override fun getWordBoundary(offset: Int): TextRange {
|
||||
if (offset < text.length && text[offset].isWhitespace() || offset == text.length) {
|
||||
return if (offset > 0 && !text[offset - 1].isWhitespace()) {
|
||||
val b = ops.wordBoundary(offset - 1); TextRange(b[0], b[1])
|
||||
} else TextRange(offset, offset)
|
||||
}
|
||||
val b = ops.wordBoundary(offset)
|
||||
return TextRange(b[0], b[1])
|
||||
}
|
||||
|
||||
override fun paint(canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?) {
|
||||
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
|
||||
}
|
||||
|
||||
override fun paint(
|
||||
canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?,
|
||||
drawStyle: DrawStyle?, blendMode: BlendMode,
|
||||
) {
|
||||
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
|
||||
}
|
||||
|
||||
override fun paint(
|
||||
canvas: Canvas, brush: Brush, alpha: Float, shadow: Shadow?, textDecoration: TextDecoration?,
|
||||
drawStyle: DrawStyle?, blendMode: BlendMode,
|
||||
) {
|
||||
val color = (brush as? SolidColor)?.value?.let {
|
||||
if (alpha.isFinite()) it.copy(alpha = it.alpha * alpha) else it
|
||||
} ?: style.color
|
||||
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
|
||||
}
|
||||
|
||||
// ============
|
||||
// Line-metric helpers
|
||||
|
||||
private val lineMetrics: List<LineMetricData> by lazy { ops.lineMetrics() }
|
||||
|
||||
private fun lineMetricsForOffset(offset: Int): LineMetricData? =
|
||||
if (offset in 0..text.length) lineMetrics.binarySearchFirstMatchingOrLast { offset < it.endIncludingNewline } else null
|
||||
|
||||
private fun lineMetricsForVerticalPosition(vertical: Float): LineMetricData? =
|
||||
lineMetrics.binarySearchFirstMatchingOrLast { vertical < it.baseline + it.descent }
|
||||
|
||||
private fun getBoxForwardByOffset(offset: Int): TextBoxData? {
|
||||
if (offset !in 0..text.length) return null
|
||||
var to = offset + 1
|
||||
while (to <= text.length) {
|
||||
ops.getRectsForRange(offset, to, useMaxHeight = false).firstOrNull()?.let { return it }
|
||||
to += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getBoxBackwardByOffset(offset: Int, end: Int = offset): TextBoxData? {
|
||||
if (offset !in 0..text.length) return null
|
||||
var from = offset - 1
|
||||
while (from >= 0) {
|
||||
val box = ops.getRectsForRange(from, end, useMaxHeight = false).firstOrNull()
|
||||
when {
|
||||
box == null -> from -= 1
|
||||
text[from] == '\n' -> {
|
||||
val bottom = box.bottom + box.bottom - box.top
|
||||
return TextBoxData(0f, box.bottom, 0f, bottom, box.isRtl)
|
||||
}
|
||||
else -> return box
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextBoxData.cursorHorizontalPosition(opposite: Boolean = false): Float =
|
||||
if (isRtl) (if (opposite) right else left) else (if (opposite) left else right)
|
||||
|
||||
private inline fun <T> List<T>.binarySearchFirstMatchingOrLast(crossinline predicate: (T) -> Boolean): T? {
|
||||
if (isEmpty()) return null
|
||||
val index = binarySearch { if (predicate(it)) 1 else -1 }
|
||||
return this[(-index - 1).coerceAtMost(lastIndex)]
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ package androidx.compose.ui.text.font
|
||||
* that-isn't-a-FontListFontFamily" adapter. Upstream's skiko actual routes
|
||||
* `Default`, `SansSerif`, `Serif`, `Monospace`, `Cursive`, and `LoadedFontFamily`
|
||||
* through `SkiaFontLoader` to produce a real `SkTypeface`. Our text renderer
|
||||
* (SdlParagraph / SkiaParagraph) reads `TextStyle.fontFamily` and
|
||||
* (SkiaParagraph) reads `TextStyle.fontFamily` and
|
||||
* [com.compose.sdl.text.NamedFont] directly to look the family up
|
||||
* against its registered font table, so the resolver's typeface is never
|
||||
* consulted. A no-op adapter (returns an [TypefaceResult.Immutable] wrapping
|
||||
+1
-1
@@ -64,4 +64,4 @@ actual class Locale actual constructor(languageTag: String) {
|
||||
// The OS's most-preferred locale (SDL); en-US before SDL init or on a host that
|
||||
// reports none. Cheap: the tag list is cached after the first non-empty read.
|
||||
private fun systemCurrentLocale(): Locale =
|
||||
com.compose.sdl.text.systemPreferredLocaleTags().firstOrNull()?.let(::Locale) ?: Locale("en-US")
|
||||
com.compose.sdl.res.preferredLocaleTagsProvider?.invoke()?.firstOrNull()?.let(::Locale) ?: Locale("en-US")
|
||||
+1
-1
@@ -17,7 +17,7 @@ internal actual fun createPlatformLocaleDelegate(): PlatformLocaleDelegate =
|
||||
object : PlatformLocaleDelegate {
|
||||
override val current: LocaleList
|
||||
get() {
|
||||
val tags = com.compose.sdl.text.systemPreferredLocaleTags()
|
||||
val tags = com.compose.sdl.res.preferredLocaleTagsProvider?.invoke().orEmpty()
|
||||
return if (tags.isEmpty()) LocaleList(listOf(Locale("en-US")))
|
||||
else LocaleList(tags.map(::Locale))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user