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 |
@@ -7,6 +7,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- '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.
|
# Never overlap two publish runs for the same tag.
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -103,16 +106,13 @@ jobs:
|
|||||||
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
|
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
|
||||||
# link time via --defsym in sdl3.def.
|
# link time via --defsym in sdl3.def.
|
||||||
|
|
||||||
# Build the static native deps from scripts/build-sdl/build-sdl.properties.
|
# Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
|
||||||
# Only Windows ships the SDL3 renderer (no Skiko on mingw), so only it needs
|
# target renders through Skia now, so all platforms need only base SDL3 for
|
||||||
# SDL3_ttf / SDL3_image / FreeType (SDL3_ttf text, SDL3_image decode, FreeType
|
# the main loop (windowing / input / audio / filesystem) — the from-scratch
|
||||||
# icon-axis rasterisation). macOS / Linux publish the Skia renderer — Skiko
|
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
|
||||||
# handles text / images / icons — so they only need base SDL3 for the main
|
# Cached against the manifest + scripts so version bumps invalidate cleanly;
|
||||||
# loop (windowing / input / audio / filesystem). Matches the cinterop guard
|
# bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N
|
||||||
# in compose/ui/ui/build.gradle.kts, which skips those three cinterops on the
|
# sysroot) changes without a scripts/build-sdl/ edit.
|
||||||
# Skia targets. Cached against the manifest + scripts so version bumps
|
|
||||||
# invalidate cleanly; bump the `v3` key when the toolchain (runner OS,
|
|
||||||
# compiler flavour, K/N sysroot) changes without a scripts/build-sdl/ edit.
|
|
||||||
- name: Cache libs/
|
- name: Cache libs/
|
||||||
id: cache-libs
|
id: cache-libs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -120,16 +120,10 @@ jobs:
|
|||||||
path: libs
|
path: libs
|
||||||
key: sdl-libs-v3-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
|
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
|
- name: Build static native deps
|
||||||
if: steps.cache-libs.outputs.cache-hit != 'true'
|
if: steps.cache-libs.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: python3 scripts/build-sdl/build-all.py
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
|
||||||
python3 scripts/build-sdl/build-all.py
|
|
||||||
else
|
|
||||||
python3 scripts/build-sdl/build-all.py sdl3
|
|
||||||
fi
|
|
||||||
|
|
||||||
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
|
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
|
||||||
# androidx.compose.* files. Sync copies them in from the upstream refs
|
# androidx.compose.* files. Sync copies them in from the upstream refs
|
||||||
@@ -160,16 +154,35 @@ jobs:
|
|||||||
VER="${PUBLISH_VERSION#v}"
|
VER="${PUBLISH_VERSION#v}"
|
||||||
OWNER="${GITHUB_REPOSITORY%%/*}"
|
OWNER="${GITHUB_REPOSITORY%%/*}"
|
||||||
# Keep in sync with the module list in settings.gradle.kts.
|
# 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
|
case "${{ matrix.host }}" in
|
||||||
macos) SUFFIXES="-macosarm64" ;;
|
macos) SUFFIXES="-macosarm64" ;;
|
||||||
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
||||||
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
||||||
esac
|
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() {
|
host_packages() {
|
||||||
for m in $MODULES; do
|
for m in $MODULES; do
|
||||||
|
g=$(group_of "$m")
|
||||||
for s in $SUFFIXES; do
|
for s in $SUFFIXES; do
|
||||||
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
|
if [ "$s" = "_ROOT_" ]; then echo "$g.$m"; else echo "$g.$m$s"; fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
@@ -183,7 +196,7 @@ jobs:
|
|||||||
host_packages | while read -r pkg; do
|
host_packages | while read -r pkg; do
|
||||||
vid=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
vid=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions?per_page=100" \
|
"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
|
[ -z "$vid" ] && continue
|
||||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
@@ -214,6 +227,33 @@ jobs:
|
|||||||
echo "::error::Publish failed after 3 attempts"
|
echo "::error::Publish failed after 3 attempts"
|
||||||
exit 1
|
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
|
# Release binaries for the host target — the binary plus the data.kres
|
||||||
# (bundled composeResources + fonts + subset icon-font). Each app's
|
# (bundled composeResources + fonts + subset icon-font). Each app's
|
||||||
# linkReleaseExecutable<Target> auto-runs its data.kres zip task —
|
# linkReleaseExecutable<Target> auto-runs its data.kres zip task —
|
||||||
@@ -242,8 +282,21 @@ jobs:
|
|||||||
for app in demo apidemo; do
|
for app in demo apidemo; do
|
||||||
src="$app/build/bin/$target_dir/releaseExecutable"
|
src="$app/build/bin/$target_dir/releaseExecutable"
|
||||||
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
|
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
|
||||||
|
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()' \
|
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
|
done
|
||||||
ls -la dist/
|
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,
|
- [README.md](README.md) — public overview + quickstart (bridge plugin,
|
||||||
`nativeComposeWindow`, sample apps).
|
`nativeComposeWindow`, sample apps).
|
||||||
- [RENDERER.md](RENDERER.md) — the rendering layer: architecture, the retained-
|
- Renderer — one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows /
|
||||||
layer engine, convergence decisions, learnings, and open work. Read it before
|
CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine.
|
||||||
touching any renderer, graphics-actual, or layer-engine code.
|
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
|
- [TOOLING.md](TOOLING.md) — build/vendor/verify scripts and workflows
|
||||||
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
|
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
|
||||||
verify-mac) + the version map and the ref-bump / release runbooks.
|
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
|
- [PLAN.md](PLAN.md) §2 — audited list of no-ops, stubs, and hardcodes left in the
|
||||||
port, with severity, plus what's missing for a stable 1.12 release.
|
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
|
- This file — architecture, module layout, vendoring rules, source-set
|
||||||
hierarchy, density flow, conventions, and common pitfalls.
|
hierarchy, density flow, conventions, and common pitfalls.
|
||||||
|
|
||||||
@@ -31,9 +39,9 @@ Rendering is **Skia everywhere** behind one `RenderBackend` — Metal / OpenGL
|
|||||||
- **Windows (mingwX64)** links the bitsycore skiko FORK — skiko+Skia compiled
|
- **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
|
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
|
an embedded GNU import lib, published to GitHub Packages as
|
||||||
`org.jetbrains.skiko:skiko:0.150.1-mingw.1` (override with
|
`com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override with
|
||||||
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
|
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
|
||||||
the bridge plugin (`installWindowsSkiaDll`). See SKIKO-MINGW-FEASIBILITY.md.
|
the bridge plugin (`installWindowsSkiaDll`).
|
||||||
|
|
||||||
Windowing, input, audio, filesystem access, and the OS-integration surface
|
Windowing, input, audio, filesystem access, and the OS-integration surface
|
||||||
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
|
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
|
||||||
@@ -46,8 +54,11 @@ re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
|
|||||||
## Module layout
|
## Module layout
|
||||||
|
|
||||||
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
|
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
|
||||||
Only `:window` is the SDL integration layer ("Compose SDL" — SDL3 main loop)
|
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
|
||||||
and lives under `compose/sdl/`.
|
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
|
One Gradle module per upstream artifact; the directory mirrors the upstream
|
||||||
`compose/` path, the gradle path is kept short (redirected via `projectDir`).
|
`compose/` path, the gradle path is kept short (redirected via `projectDir`).
|
||||||
@@ -55,10 +66,18 @@ One Gradle module per upstream artifact; the directory mirrors the upstream
|
|||||||
```
|
```
|
||||||
compose/
|
compose/
|
||||||
├── ui/
|
├── ui/
|
||||||
│ ├── ui/ → :ui — androidx.compose.ui.* (ui + ui-graphics + ui-text) +
|
│ ├── ui/ → :ui — androidx.compose.ui.* CORE (Modifier, LayoutNode,
|
||||||
│ │ com.compose.sdl.* — the sdl3 cinterop + the Skia renderer
|
│ │ composition, semantics, input, focus) + com.compose.sdl.* —
|
||||||
│ │ pipeline live here. ui-graphics/ui-text can't split off: their
|
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
|
||||||
│ │ Canvas / Paragraph `expect`s are the renderer's `actual`s (same-module).
|
│ │ 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-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
||||||
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
||||||
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
||||||
@@ -79,9 +98,15 @@ compose/
|
|||||||
│ └── material3/ → :material3 — androidx.compose.material3.*
|
│ └── material3/ → :material3 — androidx.compose.material3.*
|
||||||
├── material/
|
├── material/
|
||||||
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.*
|
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.*
|
||||||
└── sdl/ ("Compose SDL" — project modules, not upstream CMP artifacts)
|
└── desktop/native/window/ → :desktop-native-window — nativeComposeApp { Window(...) {} }
|
||||||
└── window/ → :window — nativeComposeApp { Window(...) {} } multi-window
|
multi-window shell + SDL3 main loop; nativeComposeWindow()
|
||||||
shell + SDL3 main loop; nativeComposeWindow() wrapper
|
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/
|
utils/
|
||||||
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
||||||
@@ -181,7 +206,7 @@ libs/ → gitignored per-host static SDL3 output of
|
|||||||
scripts/build-sdl/build-all.py on Windows
|
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
|
`settings.gradle.kts` redirects `projectDir` for each so build files across
|
||||||
the repo stay terse. `androidx.collection` is a plain Maven dependency
|
the repo stay terse. `androidx.collection` is a plain Maven dependency
|
||||||
(`androidx.collection:collection`), not a module — same as other simple
|
(`androidx.collection:collection`), not a module — same as other simple
|
||||||
@@ -193,23 +218,27 @@ androidx KMP libs.
|
|||||||
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
|
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
|
||||||
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
|
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
|
||||||
:material-ripple ←────────────┘──────────────┘
|
: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
|
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
|
||||||
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
|
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-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-layout → :ui`; `:animation → :animation-core, :foundation-layout`;
|
||||||
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
|
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
|
||||||
`:material-ripple → :foundation, :animation-core`;
|
`:material-ripple → :foundation, :animation-core`;
|
||||||
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
|
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
|
||||||
|
|
||||||
`:ui` is the renderer/cinterop hub (the ui-graphics + ui-text `expect`s resolve to
|
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
|
||||||
its Skia renderer `actual`s). The pure lower artifacts (`:ui-util`,
|
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
|
||||||
`:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are split out below it. Everything
|
`actual`s, SDL-free) and the naked `:sdl-core` (sdl3 cinterop). The pure lower
|
||||||
above `:ui` can only touch renderer / cinterop internals via its public surface.
|
artifacts (`:ui-util`, `:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are below.
|
||||||
`:window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
|
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).
|
install the popup / scaffold layer at the composition root).
|
||||||
|
|
||||||
## Vendoring philosophy — read this before writing any `androidx.compose.*` code
|
## Vendoring philosophy — read this before writing any `androidx.compose.*` code
|
||||||
@@ -292,11 +321,12 @@ Two categories of code live in each module:
|
|||||||
reconciled by hand. This is fine; do it when the edit is small and
|
reconciled by hand. This is fine; do it when the edit is small and
|
||||||
the file is unlikely to churn upstream.
|
the file is unlikely to churn upstream.
|
||||||
|
|
||||||
4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`.** When upstream
|
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
|
||||||
ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …, the
|
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
|
||||||
`.skiko.kt` variant is fine to vendor into `skikoRendererMain` (the Skia
|
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` — graphics
|
||||||
drawing pipeline — `SkiaCanvas.kt`, `SkiaTextRenderer.kt`,
|
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
|
||||||
`SkiaImageCache.kt`, …). All native targets attach this source set;
|
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`.
|
mingwX64 layers its fork actuals on top under `skikoRendererMingwMain`.
|
||||||
|
|
||||||
5. **Multi-OS project code goes through SDL3, not hand-rolled per-target
|
5. **Multi-OS project code goes through SDL3, not hand-rolled per-target
|
||||||
@@ -315,7 +345,9 @@ call sites don't care.
|
|||||||
|
|
||||||
## Source-set hierarchy (:ui only)
|
## Source-set hierarchy (:ui only)
|
||||||
|
|
||||||
`:ui` owns the `sdl3` cinterop + the Skia renderer pipeline. 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
|
commonMain
|
||||||
@@ -339,12 +371,15 @@ fails to come up.
|
|||||||
|
|
||||||
### Cinterop
|
### Cinterop
|
||||||
|
|
||||||
`:ui` owns ONE cinterop in `src/nativeInterop/cinterop/`: `sdl3` (SDL_Window /
|
The `sdl3` cinterop lives in the NAKED `:sdl-core` module
|
||||||
SDL_Event / SDL_GetBasePath / clipboard / dialogs / GL+Metal context /
|
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
|
||||||
SDL_Renderer-for-CPU-raster). It has no siblings, so there's no
|
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
|
||||||
`depends = <sibling>` propagation to worry about — cinterop `.def` files list
|
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
|
||||||
each other under `depends =` and Gradle does NOT auto-add a sibling cinterop's
|
bridges — and `:desktop-native-window` downstream — see `sdl3.*` and inherit SDL3's static-lib +
|
||||||
klib to the `-library` list, but with a single cinterop that gotcha is moot.
|
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)
|
## Density flow (Option B — layout in physical pixels)
|
||||||
|
|
||||||
@@ -379,7 +414,7 @@ Passing raw `px.dp` will double-scale on Retina.
|
|||||||
./gradlew :apidemo: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 (org.jetbrains.skiko:skiko:
|
# Links the bitsycore skiko FORK from GitHub Packages (com.bitsycore.skiko:skiko:
|
||||||
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops
|
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops
|
||||||
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
|
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
|
||||||
gradlew.bat :demo:runDebugExecutableMingwX64
|
gradlew.bat :demo:runDebugExecutableMingwX64
|
||||||
@@ -446,7 +481,7 @@ drift / vendor-clean guardrails are in
|
|||||||
## Key files by area — start here when you need to find something
|
## Key files by area — start here when you need to find something
|
||||||
|
|
||||||
### Renderer + main loop
|
### 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.
|
recomposer lifecycle, SDL event dispatch, composition-local seeding.
|
||||||
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface.
|
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface.
|
||||||
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker
|
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker
|
||||||
@@ -464,14 +499,15 @@ drift / vendor-clean guardrails are in
|
|||||||
alpha bridge).
|
alpha bridge).
|
||||||
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
|
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
|
||||||
|
|
||||||
### Text
|
### Text (all in `:ui-text` now)
|
||||||
- `compose/ui/ui/src/nativeMain/…/ui/text/SdlParagraph.native.kt` — the
|
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` — the
|
||||||
bridged `Paragraph` implementation (measurement, hit-test, line metrics,
|
`Paragraph` actual (skiko-free; the 30 interface methods over plain data),
|
||||||
span painting).
|
driving the skiko engine through the `NativeParagraphOps` seam.
|
||||||
- `compose/ui/ui/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
|
- `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.
|
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
|
||||||
- `compose/ui/ui/src/commonMain/…/text/TextMeasurer.kt` — `NativeTextMeasurer`
|
|
||||||
interface (implemented by `SkiaTextRenderer`).
|
|
||||||
|
|
||||||
### Icons
|
### Icons
|
||||||
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` —
|
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` —
|
||||||
@@ -507,7 +543,7 @@ profiler. Quick rules of thumb:
|
|||||||
- Slow frame → **profiler** first (`CDN_PROFILE=1`), optimize second.
|
- Slow frame → **profiler** first (`CDN_PROFILE=1`), optimize second.
|
||||||
- Any renderer change → the **`verify-mac`** runbook gates it 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
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -72,7 +72,7 @@ saved state behave as they do on Android.
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
commonMain.dependencies {
|
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(":material3")) // Material 3 widgets
|
||||||
implementation(project(":material-symbols")) // icon-font composables (optional)
|
implementation(project(":material-symbols")) // icon-font composables (optional)
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,10 @@ commonMain.dependencies {
|
|||||||
|
|
||||||
### Building from your own project: the bridge plugin
|
### 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
|
the bridge Gradle plugin once, declare the **official** Compose Multiplatform
|
||||||
coordinates, and the plugin swaps in the port's klibs on native desktop targets
|
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.
|
while android, jvm, iOS, and wasm keep resolving the official artifacts.
|
||||||
@@ -103,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).
|
[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
|
For a complete project that applies the bridge and builds one shared UI for
|
||||||
@@ -142,8 +153,8 @@ client certificates.
|
|||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
One Gradle module per upstream Compose artifact, mirroring the upstream
|
One Gradle module per upstream Compose artifact, mirroring the upstream
|
||||||
`compose/` tree. The renderer and cinterops live in `:ui`; `:window` is the SDL
|
`compose/` tree. The renderer lives in `:ui` and the `sdl3` cinterop in
|
||||||
integration layer. `:material-symbols`, `:components-resources`, and
|
`: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
|
`:navigation3-ui` are project or vendored modules that the official Maven
|
||||||
artifacts do not cover for Kotlin/Native desktop.
|
artifacts do not cover for Kotlin/Native desktop.
|
||||||
|
|
||||||
|
|||||||
-219
@@ -1,219 +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 renderer
|
|
||||||
|
|
||||||
> **This branch is Skia-only.** The from-scratch SDL renderer leg
|
|
||||||
> (`sdlRendererMain`: `Sdl3Canvas` / `Sdl3TextRenderer` / `FreeTypeIcons` / the
|
|
||||||
> SDL render nodes, ~3,800 lines) and its `SDL3_ttf` / `SDL3_image` / FreeType
|
|
||||||
> cinterops were removed once Skiko-on-mingwX64 landed (Route 1a — see
|
|
||||||
> [SKIKO-MINGW-FEASIBILITY.md](SKIKO-MINGW-FEASIBILITY.md)). SDL3 remains as the
|
|
||||||
> windowing / input / platform layer only. Below, references to "the SDL leg", a
|
|
||||||
> second `RenderBackend` actual, `-Prenderer=sdl3`, and the SDL-leg fidelity work
|
|
||||||
> are **historical** — kept because the convergence decisions still explain the
|
|
||||||
> Skia leg's shape.
|
|
||||||
|
|
||||||
Rendering goes through one `RenderBackend`, implemented once: the **Skia leg**
|
|
||||||
(`skikoRendererMain`). It 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` — the layer and draw engine internals are literally
|
|
||||||
upstream. macOS/Linux link the official Skiko klibs; mingwX64 links the
|
|
||||||
bitsycore skiko **fork** (`skikoRendererMingwMain`, Route 1a).
|
|
||||||
|
|
||||||
The text and image pipeline (`SdlParagraph`, `NativeTextMeasurer`, `IconFont`)
|
|
||||||
is the port's own engine; glyph rasterization is Skia. GPU path per platform:
|
|
||||||
Metal (macOS), OpenGL (Linux + Windows), with a CPU-raster `Software` fallback.
|
|
||||||
|
|
||||||
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 Skia leg 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`) 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.
|
|
||||||
- **Parity is a golden-master against JVM** (about 2% median, dominated by the
|
|
||||||
shared text engine's small metric delta). 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. All platforms now render through
|
|
||||||
real Skia — mingwX64 via the bitsycore skiko fork (Route 1a; see
|
|
||||||
[SKIKO-MINGW-FEASIBILITY.md](SKIKO-MINGW-FEASIBILITY.md)) — so there is no
|
|
||||||
fidelity tier below the JVM cross-check. The JVM Compose Desktop target
|
|
||||||
(`:demo:run`) remains the fidelity/parity reference on any host.
|
|
||||||
- **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 (e.g. no windowing/input toolkit in K/N ->
|
|
||||||
SDL3). "It was easier" is not valid.
|
|
||||||
- **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 becomes
|
|
||||||
a hard requirement.
|
|
||||||
- **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).
|
|
||||||
- **Real Skia on Windows K/N shipped (Route 1a).** mingwX64 links the bitsycore
|
|
||||||
skiko fork against `skiko-windows-x64.dll` (with an embedded GNU import lib),
|
|
||||||
published to GitHub Packages and auto-provisioned by the bridge plugin. This
|
|
||||||
replaced the SDL renderer as the Windows path. See
|
|
||||||
[SKIKO-MINGW-FEASIBILITY.md](SKIKO-MINGW-FEASIBILITY.md) for the ABI details
|
|
||||||
and the route trade-offs.
|
|
||||||
|
|
||||||
## 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: text metrics parity (17% median -> 2%) | Done |
|
|
||||||
| Module split | Shelved (infeasible as specified) |
|
|
||||||
| Track A: real Skia on Windows (Route 1a) | Done |
|
|
||||||
|
|
||||||
## 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 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
|
|
||||||
matrix fn usable from the owner layer).
|
|
||||||
- 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.
|
|
||||||
- **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.
|
|
||||||
- **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/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,397 +0,0 @@
|
|||||||
# Skiko on mingwX64 — Feasibility & Effort Assessment
|
|
||||||
|
|
||||||
Investigation of the effort to run the real Skia leg (`org.jetbrains.skiko`)
|
|
||||||
on the Kotlin/Native **mingwX64** target, under the relaxed constraint that a
|
|
||||||
**DLL is acceptable** (the static / no-DLL invariant may be dropped). Companion
|
|
||||||
to [RENDERER.md](RENDERER.md) §4 "Track A" — which this assessment **partly
|
|
||||||
supersedes** (see [Corrections](#corrections-to-renderermd-4) below).
|
|
||||||
|
|
||||||
Pins at time of writing: skiko `0.150.1` (Skia m150), kotlin `2.4.0`,
|
|
||||||
compose `1.12.0-beta02`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bottom line
|
|
||||||
|
|
||||||
Feasible — and **more tractable than RENDERER.md §4 claims**. Two of that
|
|
||||||
section's load-bearing premises are now factually stale:
|
|
||||||
|
|
||||||
1. **"Building C++20 Skia to a GNU/mingw archive is an open-ended fork (only
|
|
||||||
abandoned Mozilla-era precedent)."** — **False.** MSYS2 maintains
|
|
||||||
`mingw-w64-skia` at **Skia milestone 143 (C++20)** for `mingw64` (GCC),
|
|
||||||
`ucrt64`, and `clang64` (clang → `x86_64-w64-windows-gnu`), producing
|
|
||||||
`libskia.dll` + a GNU import lib `libskia.dll.a`. Last rebuilt **2026-07-24**.
|
|
||||||
The GNU-ABI recipe is ~8 small GN patches (the key one adds
|
|
||||||
`is_mingw = is_win && (cxx=="g++"||cxx=="clang++")`).
|
|
||||||
2. **"The only working route is a runtime DLL, which breaks the no-DLL
|
|
||||||
invariant."** — The DLL route is real, but the reason it works is **not**
|
|
||||||
what the section implies. See the crux below.
|
|
||||||
|
|
||||||
**Effort:** ~**1–2 weeks** to a proven spike + a CPU-raster Skia-on-Windows
|
|
||||||
prototype; ~**1–2 months** to a tested, GPU-accelerated Windows Skia leg with
|
|
||||||
published fork klibs and repo wiring — **plus** permanent
|
|
||||||
`bitsycore/skiko`-fork maintenance (re-sync at every skiko bump). Difficulty:
|
|
||||||
**research-grade / high**, because no working prior art exists (the two GitHub
|
|
||||||
`skiko-mingw64` forks are vaporware — 0 commits ahead of upstream, no mingw
|
|
||||||
source set). Confidence: **medium** (high on mechanism, medium on timeline).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Spike result — EXECUTED, GREEN ✅ (2026-07-24)
|
|
||||||
|
|
||||||
The core de-risking experiment has been **run on this machine** and passed. The
|
|
||||||
whole Route 1a link mechanism works on the exact target toolchain (Kotlin
|
|
||||||
**2.4.0**, mingwX64, LLVM **21** `ld.lld`, `x86_64-pc-windows-gnu`).
|
|
||||||
|
|
||||||
**What was built** (`spike-skiko-mingw/` in this repo):
|
|
||||||
- A tiny DLL reproducing skiko's binding shape — an **opaque handle over a real
|
|
||||||
C++/STL object** (`std::string`), **alloc+free paired inside the DLL**, flat
|
|
||||||
`extern "C"` surface. Compiled **MSVC-ABI via `clang-cl /LD /MT /EHsc`**
|
|
||||||
(static CRT). Exports verified undecorated: `spike_make/size/free`.
|
|
||||||
- A **GNU import library** (`libspike.dll.a`) generated with `dlltool`.
|
|
||||||
- A **Kotlin/Native mingwX64 executable** binding it via cinterop (plain C
|
|
||||||
header) + `linkerOpts("-L… -lspike")`.
|
|
||||||
|
|
||||||
**Results:**
|
|
||||||
- `linkReleaseExecutableMingwX64` **linked clean** — **KT-65671 did NOT bite**
|
|
||||||
(the DLL path puts zero C++ objects in the K/N link graph, so the
|
|
||||||
`--allow-multiple-definition` / `.drectve __clang_call_terminate` failures
|
|
||||||
never arise). LLVM 21 `ld.lld` is also far newer than the 1.9.x-era reports.
|
|
||||||
- Runtime: `SPIKE-RESULT size=7 expected=7 ok=true`, exit 0 — the C++
|
|
||||||
`std::string(7,'x').size()` round-trips through the opaque handle correctly.
|
|
||||||
- `objdump -p` confirms the exe imports `spike.dll`, and `spike.dll` depends on
|
|
||||||
**`KERNEL32.dll` only** → **self-contained, no VC++ redist needed**. The exe
|
|
||||||
carries K/N's `msvcrt.dll` while the DLL carries its own static MSVC CRT —
|
|
||||||
**two CRTs coexist in one process** with no issue (ownership paired inside the
|
|
||||||
DLL), empirically confirming the CRT-boundary discipline.
|
|
||||||
|
|
||||||
**Implication:** the one load-bearing unknown (does the DLL route link on
|
|
||||||
2.4.0?) is resolved **in favour**. Route 1a is de-risked at the mechanism level;
|
|
||||||
remaining effort is engineering the real `skiko.dll` (compile skiko's shim +
|
|
||||||
skia-pack MSVC Skia into one DLL) and repo wiring — not proving feasibility.
|
|
||||||
Feasibility confidence is now **high**; residual medium confidence is on
|
|
||||||
timeline only.
|
|
||||||
|
|
||||||
Reproduce: `./gradlew -p spike-skiko-mingw linkReleaseExecutableMingwX64`, then
|
|
||||||
run the exe with `spike-skiko-mingw/native` on `PATH`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The crux — and why "just use a DLL" is only half the story
|
|
||||||
|
|
||||||
**The blocker is a C++ ABI mismatch at the binding boundary, and it is
|
|
||||||
orthogonal to static-vs-dynamic linkage.**
|
|
||||||
|
|
||||||
- **K/N mingwX64 is GNU/Itanium ABI.** `konan.properties`:
|
|
||||||
`targetTriple.mingw_x64 = x86_64-pc-windows-gnu`,
|
|
||||||
`linker.mingw_x64 = ld.lld.exe`,
|
|
||||||
`linkerKonanFlags.mingw_x64 = -static-libgcc -static-libstdc++ … -lwinpthread`
|
|
||||||
— GCC libstdc++/libgcc, Itanium mangling (`_Z3fooi`).
|
|
||||||
- **skia-pack's only Windows Skia is MSVC-ABI** — built with `clang-cl`,
|
|
||||||
`is_trivial_abi=false`, shipped as MSVC `.lib`; C++ exports are MSVC-mangled
|
|
||||||
(`?foo@@YAHH@Z`).
|
|
||||||
|
|
||||||
LLVM issue #60847 ("LLD cannot link against MSVC libraries in MinGW toolchain",
|
|
||||||
closed *not planned*) confirms the two mangling schemes never reconcile.
|
|
||||||
**Wrapping the same MSVC C++ Skia in a DLL does not change this** — a DLL's C++
|
|
||||||
exports are still MSVC-mangled. So dynamic-vs-static is a red herring; **the C++
|
|
||||||
ABI at whatever boundary the mangled C++ crosses is the wall.**
|
|
||||||
|
|
||||||
What makes it tractable is *where that boundary sits in skiko*:
|
|
||||||
|
|
||||||
- **skiko's Kotlin↔native boundary is already a flat, unmangled C symbol
|
|
||||||
boundary.** Kotlin declares
|
|
||||||
`@ExternalSymbolName("org_jetbrains_skia_Data__1nSize") external fun _nSize(…)`;
|
|
||||||
`ExternalSymbolName` is a typealias for `kotlin.native.SymbolName`, which
|
|
||||||
resolves to a *plain named symbol* at link. The generated `.def` is
|
|
||||||
header-less (only `linkerOpts`). **No C++ type ever crosses into Kotlin.**
|
|
||||||
- **The C++ ABI coupling lives one layer down**, at the *shim↔Skia* link:
|
|
||||||
~78 `extern "C"` (`SKIKO_EXPORT`) `.cc` files under `src/nativeJsMain/cpp/`
|
|
||||||
that `reinterpret_cast<SkData*>(ptr)->size()` etc. This shim **must share one
|
|
||||||
C++ ABI with Skia** — today they're one GNU-ABI island fused via
|
|
||||||
`-include-binary`.
|
|
||||||
|
|
||||||
**Consequence:** a DLL is viable **iff** the *entire* C++ island (shim + Skia)
|
|
||||||
is one consistent ABI internally, and only the **flat extern-C export surface**
|
|
||||||
crosses to K/N via a generated import library. On x86_64 there is a single
|
|
||||||
Windows calling convention and extern-C exports are undecorated on both MSVC and
|
|
||||||
mingw, so `ld.lld` links such a DLL via `gendef`+`dlltool` (or directly, LLD
|
|
||||||
13+). **The repo already does exactly this for system DLLs** (`sdl3.def`:
|
|
||||||
`linkerOpts.mingw_x64 = -lkernel32 -luser32 -lgdi32 …`).
|
|
||||||
|
|
||||||
Crucially the extern-C façade **already exists** (the `nativeJsMain/cpp` shim
|
|
||||||
exports precisely the `org_jetbrains_skia_*` symbols the K/N side binds) — it is
|
|
||||||
**reused, not authored anew**. The catch: no ownership / exceptions / STL may
|
|
||||||
cross the C line — skiko is already handle/RefCnt-based, so this holds if
|
|
||||||
authored carefully. (The JVM `jvmMain/cpp` bridge exports `Java_…` JNI symbols
|
|
||||||
and is **not** reusable — the *native* shim is the right one.)
|
|
||||||
|
|
||||||
The non-negotiable that survives all of this: **upstream skiko ships no
|
|
||||||
mingwX64 K/N target at all** (`throw GradleException("$os not yet supported")`
|
|
||||||
for non-{Mac,iOS,tvOS,Linux}; "Windows" in skiko means the JVM/AWT clang-cl
|
|
||||||
path). So this is a **from-scratch skiko build-system port**, not a flag flip.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The dominant risk: KT-65671
|
|
||||||
|
|
||||||
`KT-65671` ("Kotlin/Native: Failed linking cinterop static library for
|
|
||||||
mingwX64", **Open**, filed against 1.9.x, status on 2.4.0 unverified): K/N's
|
|
||||||
mingw link step chokes when **fusing an external C++ static archive** —
|
|
||||||
`ld.lld` rejects `--allow-multiple-definition`, and
|
|
||||||
`-exclude-symbols:__clang_call_terminate` in the objects is rejected in
|
|
||||||
`.drectve`. This is skiko's **exact static-fusion pattern**.
|
|
||||||
|
|
||||||
Key implication for route choice: a **pure extern-C DLL boundary sidesteps this
|
|
||||||
bug class entirely** — no C++ objects enter the K/N link graph; only a flat
|
|
||||||
import surface crosses. So, counter-intuitively, the **DLL route is the
|
|
||||||
risk-*reducing* route** w.r.t. KT-65671, and it is exactly what the user is
|
|
||||||
willing to accept.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
### ★ Recommended — DLL with a flat extern-C export, K/N binds via import lib
|
|
||||||
|
|
||||||
Put **all** the C++ (skiko's existing shim + Skia) inside one DLL; export only
|
|
||||||
the flat `org_jetbrains_skia_*` symbols; bind from a new mingwX64 skiko target
|
|
||||||
via a generated import lib. K/N resolves at link; the Windows loader binds
|
|
||||||
`skiko.dll` at process start — **no dlopen/`staticLoad` code needed** (the
|
|
||||||
shipped `staticLoad()` no-op is fine for the import-lib path).
|
|
||||||
|
|
||||||
Two sub-choices for the DLL internals (K/N neither knows nor cares which):
|
|
||||||
|
|
||||||
- **1a — MSVC internals (minimal fork).** Recompile skiko's `nativeJsMain/cpp`
|
|
||||||
shim with `clang-cl` against **skia-pack's existing MSVC Windows `.lib`**
|
|
||||||
(`windows-x64` archives are already published). **Zero Skia fork.** Net-new:
|
|
||||||
add `__declspec(dllexport)` to `SKIKO_EXPORT` on this build, a mingw/DLL
|
|
||||||
branch in skiko's build logic, import-lib generation. CRT discipline (MSVC CRT
|
|
||||||
in the DLL vs K/N's static libgcc) is a correctness constraint on the C API
|
|
||||||
(handles + primitives only), not a linkability blocker.
|
|
||||||
- **1b — GNU internals.** Build the shim + Skia GNU-ABI (reuse MSYS2's
|
|
||||||
`mingw-w64-skia` patch set) into the DLL. Slightly more Skia-side work, but a
|
|
||||||
single toolchain end-to-end.
|
|
||||||
|
|
||||||
**Effort:** ~2–4 weeks once the spike is green. **Risk: medium** (sidesteps
|
|
||||||
KT-65671). **"Just works" preserved:** none — ships `skiko.dll` (tens of MB)
|
|
||||||
next to `data.kres` (accepted). **Sub-choice 1a is the least-fork option.**
|
|
||||||
|
|
||||||
### Route 2 — GNU-ABI static Skia + real mingwX64 skiko target (purist)
|
|
||||||
|
|
||||||
Fork skia-pack (or reuse MSYS2's patches) to emit **GNU-ABI static** Skia;
|
|
||||||
fork skiko to add a mingwX64 target that recompiles the existing extern-C shim
|
|
||||||
GNU-ABI and **static-fuses** exactly as macOS/Linux do today.
|
|
||||||
|
|
||||||
**Effort:** Skia build itself is now **days** (MSYS2 proves it). skiko port +
|
|
||||||
link reconciliation ~3–6 weeks. **Risk: medium-high** — this route hits
|
|
||||||
**KT-65671 head-on** (static C++ archive fusion). **Preserves the static /
|
|
||||||
no-DLL invariant** — the only reason to prefer it, and the user said they don't
|
|
||||||
need it.
|
|
||||||
|
|
||||||
### Route 3 — CPU-raster-only (a scope reduction, not an ABI shortcut)
|
|
||||||
|
|
||||||
The repo's `SkiaSurfaceBridge` (`Surface.makeRasterDirect` → `SDL_UpdateTexture`
|
|
||||||
→ `SDL_RenderTexture`) needs **no GPU context** and compiles on mingwX64 the
|
|
||||||
moment a skiko klib exists. Use it to **skip authoring the Windows GPU bridge**
|
|
||||||
for a first cut. Still requires solving Route 1/2's binding problem first.
|
|
||||||
**Saves ~1–2 weeks** of GPU-bridge work → the natural **milestone 1** of
|
|
||||||
whichever route you pick.
|
|
||||||
|
|
||||||
### Baseline — do nothing (already shipping)
|
|
||||||
|
|
||||||
Windows renders via the from-scratch **SDL leg** (`SDL_RenderGeometry` +
|
|
||||||
SDL3_ttf + FreeType); the **JVM parity target** is the full-fidelity Skia
|
|
||||||
reference. The bar any Skia-on-Windows work must clear is "materially better
|
|
||||||
than the SDL leg's fidelity, worth the DLL + maintenance cost."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommended work breakdown (Route 1a, CPU-raster first)
|
|
||||||
|
|
||||||
**A. Skia — none (1a) / days (1b).**
|
|
||||||
Route 1a reuses skia-pack's published `windows-x64` MSVC `.lib`. (1b: reuse the
|
|
||||||
MSYS2 `mingw-w64-skia` PKGBUILD + patches at skiko's pinned milestone; backends
|
|
||||||
CPU-raster + `skia_use_gl`, `skia_use_freetype`.)
|
|
||||||
|
|
||||||
**B. `bitsycore/skiko` fork — the bulk, medium risk.**
|
|
||||||
1. Add `mingwX64()` + a `configureNativeTarget(OS.Windows, Arch.X64, …)` branch;
|
|
||||||
replace the `throw GradleException("$os not yet supported")` / empty
|
|
||||||
`linkerFlags` fall-throughs in `NativeTasksConfiguration.kt` with a Windows
|
|
||||||
branch (PE/COFF, `llvm-ar`).
|
|
||||||
2. Compile the **existing** `nativeJsMain/cpp` shim (not the JVM JNI bridge)
|
|
||||||
into `skiko.dll`; make `SKIKO_EXPORT` export the flat symbols
|
|
||||||
(`__declspec(dllexport)` or `--export-all-symbols`/def file).
|
|
||||||
3. Generate the GNU import lib (`gendef` + `dlltool` → `libskiko.dll.a`).
|
|
||||||
4. cinterop `.def` stays header-less (`linkerOpts`/`libraryPaths`), matching the
|
|
||||||
repo's own idiom. Publish mingwX64 klibs + the DLL.
|
|
||||||
|
|
||||||
**C. Windows context actual — deferred behind milestone 1.**
|
|
||||||
5. **Milestone 1:** wire only the CPU-raster `SkiaSurfaceBridge` (already
|
|
||||||
Windows-ready).
|
|
||||||
6. **Milestone 2:** author `SkiaD3D11Bridge` / `SkiaVulkanBridge` — extract HWND
|
|
||||||
via `SDL_GetWindowProperties`, wrap `DirectContext.makeD3D11`/`makeVulkan`,
|
|
||||||
mirror the Metal/GL bridges' per-frame acquire/present.
|
|
||||||
|
|
||||||
**D. This repo's wiring — small, low risk (integration points confirmed).**
|
|
||||||
7. `compose/ui/ui/build.gradle.kts` — flip `isSkiaTarget("mingwX64")` to `true`
|
|
||||||
(currently hardcoded `false`, ~line 58); create a `skikoRendererMingwMain`
|
|
||||||
and `mingwX64Main.dependsOn(...)` it (today mingw attaches only to
|
|
||||||
`sdlRendererMingwMain`, lines ~203–206; the skiko tree, lines ~220–235, is
|
|
||||||
created only under `!useSdl3Everywhere` and never for mingw). Add the fork's
|
|
||||||
coords to that source set.
|
|
||||||
8. **No new expect/actual needed:** `createRenderBackend` /
|
|
||||||
`rendererPreferredGpuMode` are `expect`s in `nativeMain` with `actual`s in
|
|
||||||
`skikoRendererMain` and `sdlRendererMain`; attaching mingwX64 to the skiko
|
|
||||||
source set makes the Skia actual resolve for Windows (one renderer source set
|
|
||||||
per target — the existing invariant).
|
|
||||||
9. Add a Windows GPU-bridge branch to `rendererPreferredGpuMode()`; verify with
|
|
||||||
the demo/apidemo (`--gpu`, `--screenshot`) and the parity harness.
|
|
||||||
|
|
||||||
Repo surface to cover is narrow: **~67 `org.jetbrains.skia` classes + 5
|
|
||||||
`org.jetbrains.skiko.node` APIs** (`RenderNode`, `RenderNodeContext`,
|
|
||||||
`SkikoRenderDelegate`, `SystemTheme`, `currentSystemTheme`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The one spike to run first (~1–2 days)
|
|
||||||
|
|
||||||
Before any fork work, **empirically test the K/N-mingw link path**: build a
|
|
||||||
trivial `extern "C"` C++ DLL (one function returning a handle + one that uses
|
|
||||||
it) — or consume MSYS2's `mingw-w64-x86_64-skia` DLL + `libskia.dll.a`
|
|
||||||
directly — from a throwaway mingwX64 K/N project via cinterop + import lib, and
|
|
||||||
**run it**. This answers, in a day instead of a month:
|
|
||||||
|
|
||||||
- Does K/N mingw link + call a GNU-ABI DLL via an import lib at all?
|
|
||||||
- Does **KT-65671** reproduce on Kotlin 2.4.0 (import-lib vs static fusion)?
|
|
||||||
- DLL vs static-fusion behaviour on this toolchain.
|
|
||||||
|
|
||||||
If green, Routes 1/2 are de-risked. (Caveat: MSYS2 Skia exports Skia's *mangled
|
|
||||||
C++* symbols, so a direct cinterop of it tests only the *link mechanism*, not
|
|
||||||
the skiko API — for the API you still need the extern-C shim. The trivial-C-DLL
|
|
||||||
variant is the cleaner mechanism test.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Biggest risks / unknowns
|
|
||||||
|
|
||||||
1. **KT-65671** — whether K/N's mingw link works on 2.4.0 for external C++;
|
|
||||||
the DLL route sidesteps it, the static route hits it. *The single fact that
|
|
||||||
swings the effort.*
|
|
||||||
2. **Toolchain matching** — Konan bundles LLVM 16 clang + `ld.lld`; the C++20
|
|
||||||
Skia + shim must be built by a toolchain whose objects/imports LLD accepts.
|
|
||||||
3. **CRT / ownership discipline** across the C boundary (only matters for 1a).
|
|
||||||
4. **Maintenance** — SKIKO-446 / SKIKO-611 are Open, unassigned → almost
|
|
||||||
certainly a **permanent `bitsycore/skiko` fork** with per-bump re-sync tax.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
Pursue **Route 1a** (skiko fork producing a DLL with a flat extern-C export over
|
|
||||||
skia-pack's existing MSVC Windows Skia), **scoped CPU-raster first** (Route 3
|
|
||||||
milestone), GPU bridge second. It matches the user's accepted DLL tolerance,
|
|
||||||
requires **zero Skia fork**, and **dodges KT-65671**. Keep Route 2 (static,
|
|
||||||
GNU-ABI) in reserve only if reclaiming the no-DLL invariant later becomes
|
|
||||||
valuable. **Run the MSYS2/trivial-DLL spike before committing to any fork.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation status — WORKING END-TO-END ✅ (2026-07-24)
|
|
||||||
|
|
||||||
**`demo.exe` (mingwX64) renders the Material 3 Buttons screen through real Skia**
|
|
||||||
and wrote a 1000×700 screenshot ("settled at frame 3"), using the CPU-raster
|
|
||||||
`SkiaSurfaceBridge` backed by the forked `skiko-windows-x64.dll`. Route 1a is
|
|
||||||
complete. Build/run:
|
|
||||||
|
|
||||||
```
|
|
||||||
# fork (C:/Dev/skiko), one-time env: SKIKO_VSBT_PATH=<VS BuildTools dir>
|
|
||||||
./gradlew :skiko:publishKotlinMultiplatformPublicationToMavenLocal \
|
|
||||||
:skiko:publishMingwX64PublicationToMavenLocal -Pskiko.native.windows.enabled=true
|
|
||||||
# compose repo
|
|
||||||
./gradlew :demo:linkDebugExecutableMingwX64 -PwindowsSkia=true
|
|
||||||
cp <fork>/skiko/build/out/link/Release-windows-native-x64/skiko-windows-x64.dll <demo exe dir>/
|
|
||||||
<demo exe dir>/demo.exe --screenshot=out.bmp --screen=Buttons
|
|
||||||
```
|
|
||||||
|
|
||||||
Fork fixes needed beyond the DLL build (skiko uses a CUSTOM hierarchy template,
|
|
||||||
`applyDefaultHierarchyTemplate=false`):
|
|
||||||
1. `sourceHierarchy.kt` — add `group("windows"){ withMingwX64() }` under `native`
|
|
||||||
(else the mingwX64 klib compiles EMPTY — missing all of `org.jetbrains.skia`).
|
|
||||||
2. `SkikoProjectContext` — add `supportNativeWindows` into `supportAnyNative` so
|
|
||||||
the `@SymbolName` opt-in reaches the mingw compile.
|
|
||||||
3. `Resources.native.kt` — `ftell` is 32-bit `Int` on Windows (LLP64); `==` fix.
|
|
||||||
4. `windowsMain` actuals for `SkiaLayer` / `currentSystemTheme` (linux is a stub).
|
|
||||||
5. Force-export the 2 ICU symbols (`uloc_getDefault_skiko`,
|
|
||||||
`uloc_toLanguageTag_skiko`) that Kotlin binds via `@SymbolName`.
|
|
||||||
|
|
||||||
Compose-repo wiring (all behind `-PwindowsSkia=true`): `mavenLocal()` in
|
|
||||||
settings, `isSkiaTarget("mingwX64")`, a separate `skikoRendererMingwMain` tree
|
|
||||||
on the fork's **root** coord (`org.jetbrains.skiko:skiko:0.0.0-SNAPSHOT` — the
|
|
||||||
platform artifact alone doesn't expose api-elements), `PlatformGpu` Windows →
|
|
||||||
`Software`, and a one-line `PlatformGpu.mingw.kt`.
|
|
||||||
|
|
||||||
## Earlier milestone — fork BUILDS ✅ (2026-07-24)
|
|
||||||
|
|
||||||
Route 1a is not just designed — the `bitsycore/skiko` fork now **builds the
|
|
||||||
native Windows Skia DLL + a mingwX64 klib** on this machine (`C:/Dev/skiko`,
|
|
||||||
skiko `v0.150.1`).
|
|
||||||
|
|
||||||
Edits made (all in the fork):
|
|
||||||
- `src/nativeJsMain/cpp/common.h` — `SKIKO_EXPORT` gains `__declspec(dllexport)`
|
|
||||||
under a `SKIKO_WINDOWS_DLL` guard.
|
|
||||||
- `skiko/build.gradle.kts` — `configureNativeTarget(OS.Windows, Arch.X64,
|
|
||||||
mingwX64())`, gated behind `-Pskiko.native.windows.enabled=true`.
|
|
||||||
- `buildSrc/.../NativeTasksConfiguration.kt` — a Windows compile branch
|
|
||||||
(clang-cl + `-DSKIKO_WINDOWS_DLL`) and a new `configureWindowsNativeTarget`
|
|
||||||
that links `skiko-windows-x64.dll` via `lld-link` (reusing the JVM template +
|
|
||||||
`resolveBinaryInputs(…, TargetEnv.JVM, …)`), auto-generates the GNU import lib
|
|
||||||
(`dumpbin`→`.def`→`dlltool`), and wires the K/N cinterop `linkerOpts`.
|
|
||||||
|
|
||||||
Results:
|
|
||||||
- The native bridge (`nativeJsMain/cpp`) **compiled on Windows with clang-cl,
|
|
||||||
zero source changes.** The only link fix was adding `d3d12.lib
|
|
||||||
d3dcompiler.lib dxgi.lib` for Skia's `SK_DIRECT3D` backend.
|
|
||||||
- Produced **`skiko-windows-x64.dll` (14.2 MB, 1005 exported
|
|
||||||
`org_jetbrains_skia_*` symbols)**, `libskiko-windows-x64.dll.a` (GNU import
|
|
||||||
lib), and the **mingwX64 klib** (`build/classes/kotlin/mingwX64/main/klib`).
|
|
||||||
|
|
||||||
Prerequisite for any skiko Windows build on this box:
|
|
||||||
`SKIKO_VSBT_PATH='C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools'`
|
|
||||||
(Gradle's VS locator doesn't auto-find VS 18 BuildTools).
|
|
||||||
|
|
||||||
Remaining: publish the fork to `mavenLocal` → a tiny K/N mingw consumer that
|
|
||||||
calls a real Skia function (links the import lib, runs with the DLL on `PATH`)
|
|
||||||
for the definitive end-to-end proof → then wire the compose repo (flip
|
|
||||||
`isSkiaTarget("mingwX64")`, add `skikoRendererMingwMain`, ship the DLL next to
|
|
||||||
the exe, CPU-raster `SkiaSurfaceBridge` first). One known TODO: the klib's
|
|
||||||
`linkerOpts` use an absolute import-lib path (fine same-machine; make relative /
|
|
||||||
bundled for distribution).
|
|
||||||
|
|
||||||
## Corrections to RENDERER.md §4
|
|
||||||
|
|
||||||
The shelving note should be updated — its two central technical claims are stale
|
|
||||||
as of 2026-07:
|
|
||||||
|
|
||||||
- "GNU-ABI Skia is an open-ended fork / only abandoned Mozilla-era precedent" —
|
|
||||||
**false**: MSYS2 maintains a modern (m143, C++20) GNU-ABI Skia, rebuilt
|
|
||||||
routinely.
|
|
||||||
- The accurate residual blocker is **the K/N-side link/binding port**
|
|
||||||
(KT-65671 + authoring the mingwX64 skiko target), **not** building Skia, and
|
|
||||||
the DLL route is *risk-reducing*, not merely "the only working route."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources (primary, load-bearing)
|
|
||||||
|
|
||||||
- skiko v0.150.1: `skiko/build.gradle.kts`, `buildSrc/.../NativeTasksConfiguration.kt`,
|
|
||||||
`src/commonMain/kotlin/org/jetbrains/skia/Data.kt`,
|
|
||||||
`src/nativeMain/kotlin/org/jetbrains/skia/Actuals.native.kt`,
|
|
||||||
`src/nativeJsMain/cpp/common.h`, `.../impl/Library.native.kt`
|
|
||||||
- skia-pack `script/build.py`; skia.org build docs
|
|
||||||
- Kotlin `konan.properties` (v2.2.0); kotlinlang native-c-interop / target-support
|
|
||||||
- LLVM issue #60847; **KT-65671**; **SKIKO-446**, **SKIKO-611**
|
|
||||||
- MSYS2 `mingw-w64-skia` (packages.msys2.org; MINGW-packages PKGBUILD + patches)
|
|
||||||
- Vaporware forks: github.com/Cdm2883/skiko-mingw64, crowforkotlin/skiko-mingw64
|
|
||||||
- Repo: `compose/ui/ui/build.gradle.kts`, the `SkiaMetalBridge` / `SkiaGLBridge`
|
|
||||||
/ `SkiaSurfaceBridge` / `RenderBackendFactory` seam
|
|
||||||
@@ -1,166 +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` / `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)). There is now ONE renderer — **Skia**
|
|
||||||
(macOS/Linux official Skiko; mingwX64 the bitsycore skiko fork), so Windows
|
|
||||||
renders through real Skia. The priorities below are the cross-cutting platform
|
|
||||||
gaps that affect every OS. JVM Compose Desktop remains a parity cross-check.
|
|
||||||
|
|
||||||
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** — `ByteArray.decodeToImageBitmap()` /
|
|
||||||
`createImageBitmap(bytes)` decodes encoded image bytes; was
|
|
||||||
`UnsupportedOperationException`. Verified `demo --imagebytestest`.
|
|
||||||
- **`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`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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, 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),
|
|
||||||
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. Skia renderer anomalies
|
|
||||||
|
|
||||||
Skia is the renderer everywhere, so its bugs are rarely tracked here. This one
|
|
||||||
surfaced while adding BlendMode coverage (`demo --blendtest`):
|
|
||||||
|
|
||||||
- `BlendMode.Multiply` renders wrong. 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.
|
|
||||||
`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.**
|
|
||||||
|
|
||||||
## J. Needs further review (2026-07 bug audit)
|
|
||||||
|
|
||||||
An audit (parallel passes over the renderer and the text/input/event actuals)
|
|
||||||
surfaced these. The clear, contained bugs were fixed in the same pass; the items
|
|
||||||
here are left open because they are either invasive, risky, or a judgement call.
|
|
||||||
(The renderer-side fixes from that pass predate the single-Skia consolidation and
|
|
||||||
are no longer tracked here.)
|
|
||||||
|
|
||||||
Fixed in the audit (for reference, not open work): `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).**
|
|
||||||
+39
-6
@@ -21,6 +21,15 @@ python3 scripts/build-sdl/build-all.py sdl3 # rebuild the step
|
|||||||
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
|
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
|
||||||
absent). Run it once per machine, or after bumping the pinned version.
|
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
|
## Vendoring upstream Compose
|
||||||
|
|
||||||
Most `androidx.compose.*` code is copied byte for byte from
|
Most `androidx.compose.*` code is copied byte for byte from
|
||||||
@@ -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
|
Note: `present` is vsync-blocking, so profile on the target refresh rate before
|
||||||
drawing conclusions about a frame-rate gap.
|
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 probes
|
||||||
|
|
||||||
`:demo` ships headless regression probes driven by CLI flags, used by the
|
`:demo` ships headless regression probes driven by CLI flags, used by the
|
||||||
@@ -136,7 +160,7 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|||||||
```
|
```
|
||||||
|
|
||||||
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
||||||
GitHub Packages as `org.jetbrains.skiko:skiko:0.150.1-mingw.1` (macOS/Linux use
|
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
|
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
|
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
|
published by a separate GitHub Actions workflow in the fork repo, out of band
|
||||||
@@ -158,10 +182,10 @@ single file to edit; know which axis you are changing.
|
|||||||
|
|
||||||
| Version | Where | Notes |
|
| 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. |
|
| 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. |
|
| 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`. | macOS/Linux use official Skiko; mingwX64 uses the bitsycore fork (`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. Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
|
|
||||||
@@ -187,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
|
1. Complete the ref-bump flow above and confirm it is green. For a STABLE
|
||||||
release, `vComposeJvmVersion` should match the vendored ref (no skew).
|
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
|
Only Windows compiles the full common-metadata variant table, and the root
|
||||||
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
||||||
roots without mingwX64 variants (this bit v0.1.15).
|
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:
|
What the publish job does, so you can read a failure:
|
||||||
|
|
||||||
- Each host publishes only its own target's publications to GitHub Packages
|
- 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
|
KotlinMultiplatform metadata, the `jvm` publication, and the bridge plugin
|
||||||
(only Windows declares every target).
|
(only Windows declares every target).
|
||||||
- The `MODULES` list in the workflow must stay in sync with the modules in
|
- The `MODULES` list in the workflow must stay in sync with the modules in
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":window"))
|
implementation(project(":desktop-native-window"))
|
||||||
implementation(libs.ktor.client.curl)
|
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),
|
/** 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)
|
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). */
|
every one of N lines on each width change (the other half of the freeze). */
|
||||||
private fun lineStartOffsets(inText: String): IntArray {
|
private fun lineStartOffsets(inText: String): IntArray {
|
||||||
val vStarts = ArrayList<Int>(64)
|
val vStarts = ArrayList<Int>(64)
|
||||||
@@ -963,10 +963,10 @@ internal fun BodyView(
|
|||||||
val vGutterWidth = (vDigits * 7 + 4).dp
|
val vGutterWidth = (vDigits * 7 + 4).dp
|
||||||
// Body wrap width (physical px, from onSizeChanged), reported by the body
|
// 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
|
// 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
|
// 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
|
// scale 12.sp through the current density here, matching what the paragraph
|
||||||
// does when it builds the body's real paragraph.
|
// engine does when it builds the body's real paragraph.
|
||||||
var vBodyWidthPx by remember { mutableStateOf(0) }
|
var vBodyWidthPx by remember { mutableStateOf(0) }
|
||||||
val vFontPx = with(LocalDensity.current) { 12.sp.toPx().toInt() }
|
val vFontPx = with(LocalDensity.current) { 12.sp.toPx().toInt() }
|
||||||
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
|
// 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.revealInFileManager
|
||||||
import com.compose.sdl.showOpenFileDialog
|
import com.compose.sdl.showOpenFileDialog
|
||||||
import com.compose.sdl.showSaveFileDialog
|
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.*
|
||||||
import io.ktor.client.engine.curl.*
|
import io.ktor.client.engine.curl.*
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
@@ -49,8 +55,31 @@ actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
|
|||||||
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
||||||
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
||||||
|
|
||||||
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
|
// Cached resolver for wrappedRowCount — the skiko engine resolves fonts by name
|
||||||
currentTextMeasurer.wrap(inText, inFontPx, inMaxWidthPx, inFamilyName).lines.size
|
// (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
|
actual var editorTabWidth: Int
|
||||||
get() = TextLayoutConfig.tabWidth
|
get() = TextLayoutConfig.tabWidth
|
||||||
|
|||||||
+55
-10
@@ -12,14 +12,49 @@ plugins {
|
|||||||
apiValidation {
|
apiValidation {
|
||||||
@OptIn(ExperimentalBCVApi::class)
|
@OptIn(ExperimentalBCVApi::class)
|
||||||
klib { enabled = true }
|
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
|
// 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.
|
// 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")
|
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 {
|
allprojects {
|
||||||
group = "com.bitsycore.compose.sdl"
|
group = groupFor(path)
|
||||||
version = vPublishVersion
|
version = vPublishVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,12 +72,13 @@ allprojects {
|
|||||||
|
|
||||||
val kAppModules = setOf(":demo", ":apidemo")
|
val kAppModules = setOf(":demo", ":apidemo")
|
||||||
val kPublishedLibs = setOf(
|
val kPublishedLibs = setOf(
|
||||||
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
|
":sdl-core",
|
||||||
":ui-tooling-preview",
|
":ui", ":ui-util", ":ui-geometry", ":ui-graphics", ":ui-text",
|
||||||
|
":ui-unit", ":ui-backhandler", ":ui-tooling-preview",
|
||||||
":animation-core", ":animation", ":animation-graphics",
|
":animation-core", ":animation", ":animation-graphics",
|
||||||
":foundation", ":foundation-layout",
|
":foundation", ":foundation-layout",
|
||||||
":material3", ":material-ripple",
|
":material3", ":material-ripple",
|
||||||
":window", ":material-symbols",
|
":desktop-native-window", ":material-symbols",
|
||||||
":navigation3-ui", ":components-resources",
|
":navigation3-ui", ":components-resources",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,7 +112,7 @@ subprojects {
|
|||||||
kPublishedLibs.forEach { modulePath ->
|
kPublishedLibs.forEach { modulePath ->
|
||||||
val vArtifactId = modulePath.removePrefix(":")
|
val vArtifactId = modulePath.removePrefix(":")
|
||||||
substitute(project(modulePath))
|
substitute(project(modulePath))
|
||||||
.using(module("com.bitsycore.compose.sdl:$vArtifactId:$kConsumeVersion"))
|
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
|
||||||
.because("-PuseGithubPackages=true")
|
.because("-PuseGithubPackages=true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +141,14 @@ subprojects {
|
|||||||
password = System.getenv("GITHUB_TOKEN")
|
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 {
|
publications.withType<MavenPublication>().configureEach {
|
||||||
pom {
|
pom {
|
||||||
@@ -135,10 +179,11 @@ allprojects {
|
|||||||
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
||||||
resolutionStrategy.dependencySubstitution {
|
resolutionStrategy.dependencySubstitution {
|
||||||
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
|
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
|
||||||
// ui's split artifacts: ui-graphics + ui-text are merged INTO
|
// ui-graphics / ui-text are their own modules (split out of :ui,
|
||||||
// :ui in the port; ui-unit / ui-geometry have their own modules.
|
// upstream layout). Declare each DIRECTLY so the app commonMain sees
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui"))
|
// them under the granular-metadata visibility rule.
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui"))
|
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-unit")).using(project(":ui-unit"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
|
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
|
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
|
||||||
|
|||||||
@@ -49,9 +49,11 @@ class ComposeFontBundlingConfig {
|
|||||||
var bundleMaterialSymbols: Boolean = false
|
var bundleMaterialSymbols: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hb-subset each bundled Material Symbols font to the glyphs the sources reference
|
* 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
|
* (scripts/subset-material-symbols.py picks the codepoints; the subset task uses
|
||||||
* absent). Needs [bundleMaterialSymbols]; still gated by -PsubsetIcons.
|
* 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
|
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
|
* 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.
|
* 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(
|
private fun Project.registerSubsetTask(
|
||||||
inStyle: String,
|
inStyle: String,
|
||||||
@@ -279,30 +283,67 @@ private fun Project.registerSubsetTask(
|
|||||||
vOut.parentFile.mkdirs()
|
vOut.parentFile.mkdirs()
|
||||||
val vInputFile = vInputProvider.get().asFile
|
val vInputFile = vInputProvider.get().asFile
|
||||||
val vBefore = vInputFile.length()
|
val vBefore = vInputFile.length()
|
||||||
// ProcessBuilder: project.exec isn't usable inside doLast on Gradle 9.
|
// project.exec isn't usable inside doLast on Gradle 9, so shell out via
|
||||||
// hb-subset is optional : without it, bundle the full font.
|
// ProcessBuilder. Returns (exitedZero, combinedOutput); a missing binary
|
||||||
val vProc = try {
|
// (IOException) yields (false, message) so callers can fall through.
|
||||||
ProcessBuilder(
|
fun runCmd(vararg inCmd: String): Pair<Boolean, String> =
|
||||||
"hb-subset",
|
try {
|
||||||
vInputFile.absolutePath,
|
val vProc = ProcessBuilder(*inCmd).redirectErrorStream(true).start()
|
||||||
"-o", vOut.absolutePath,
|
val vText = vProc.inputStream.bufferedReader().readText()
|
||||||
"--unicodes=$vUnicodes",
|
(vProc.waitFor() == 0) to vText
|
||||||
).redirectErrorStream(true).start()
|
} catch (e: java.io.IOException) {
|
||||||
} catch (_: java.io.IOException) {
|
false to (e.message ?: "not found")
|
||||||
vInputFile.copyTo(vOut, overwrite = true)
|
}
|
||||||
logger.warn(
|
|
||||||
"[subset $inStyle] hb-subset not found on PATH : bundling the full font " +
|
fun logShrink(inTool: String) {
|
||||||
"(${vBefore / 1024}KB). Install harfbuzz to shrink it (brew install harfbuzz / " +
|
val vAfter = vOut.length()
|
||||||
"pacman -S mingw-w64-x86_64-harfbuzz / apt install harfbuzz-utils)."
|
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]"
|
||||||
)
|
)
|
||||||
return@doLast
|
|
||||||
}
|
}
|
||||||
val vOutput = vProc.inputStream.bufferedReader().readText()
|
|
||||||
val vCode = vProc.waitFor()
|
// 1. hb-subset (harfbuzz) — fastest path when it is on PATH.
|
||||||
if (vCode != 0) throw GradleException("hb-subset failed (exit $vCode):\n$vOutput")
|
if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first
|
||||||
val vAfter = vOut.length()
|
&& vOut.length() > 0L
|
||||||
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")
|
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] 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."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
constructor <init>() // org.jetbrains.compose.resources/ExperimentalResourceApi.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ kotlin {
|
|||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
kotlin.srcDir("src/vendor/native/kotlin")
|
kotlin.srcDir("src/vendor/native/kotlin")
|
||||||
// The Font / Image resource actuals — pure project code that delegates
|
// src/nativeMain/kotlin also holds the Font / Image resource actuals —
|
||||||
// to :ui's IconFont / NamedFont / decodeEncodedImageBitmap (the Skia
|
// pure project code that delegates to the project font registry
|
||||||
// decoder). Renderer-agnostic and skiko-free, so a single native set
|
// (IconFont / NamedFont) and the :ui-graphics Skia decoder
|
||||||
// covers every target. (Dir name is historical — .sdl.kt.)
|
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
|
||||||
kotlin.srcDir("src/sdlRendererMain/kotlin")
|
// every target.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
# mingwX64/linux variants, so the runtime library is vendored and the platform
|
# mingwX64/linux variants, so the runtime library is vendored and the platform
|
||||||
# actuals are project code:
|
# actuals are project code:
|
||||||
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL),
|
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL),
|
||||||
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser)
|
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
|
||||||
# src/sdlRendererMain — the Font/Image resource actuals: renderer-agnostic
|
# and the Font/Image resource actuals: renderer-agnostic
|
||||||
# project code (image decode via :ui's Skia decoder, Font
|
# project code (image decode via the :ui-graphics Skia
|
||||||
# via the project font registry). Dir name is historical;
|
# decoder, Font via the project font registry)
|
||||||
# the build attaches it to nativeMain.
|
|
||||||
# Re-sync: python scripts/compose-fork/sync.py components/resources/library
|
# Re-sync: python scripts/compose-fork/sync.py components/resources/library
|
||||||
|
|
||||||
SET_REPO=https://github.com/JetBrains/compose-multiplatform@<COMPOSE_REF>
|
SET_REPO=https://github.com/JetBrains/compose-multiplatform@<COMPOSE_REF>
|
||||||
@@ -55,9 +54,9 @@ skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/
|
|||||||
|
|
||||||
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia
|
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia
|
||||||
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
|
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
|
||||||
# doesn't expose. The module's own Font/Image actuals (src/sdlRendererMain/kotlin/,
|
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
|
||||||
# attached to nativeMain) use pure project APIs (IconFont / NamedFont /
|
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
|
||||||
# decodeEncodedImageBitmap) instead, so opt the upstream Skia-bound files out.
|
# instead, so opt the upstream Skia-bound files out.
|
||||||
!skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt
|
!skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt
|
||||||
!skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
|
!skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
|
||||||
!skikoMain/kotlin/org/jetbrains/compose/resources/SvgPainter.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
|
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
|
registry (IconFont.registerIcon — the general byte-font table, not just
|
||||||
icons). A font resource therefore loads its bytes through the resource
|
icons). A font resource therefore loads its bytes through the resource
|
||||||
reader, registers them under a per-resource family name, and returns a
|
reader, registers them under a per-resource family name, and returns a
|
||||||
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the
|
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the
|
||||||
standard project pipeline. Weight/style are carried on the NamedFont;
|
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>")
|
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.InternalResourceApi
|
||||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||||
|
import com.compose.sdl.res.preferredLocaleProvider
|
||||||
import kotlinx.cinterop.alloc
|
import com.compose.sdl.res.systemThemeIsDarkProvider
|
||||||
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
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: ResourceEnvironment — SDL actual
|
// MARK: ResourceEnvironment — platform-env-seam actual
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Non-composable system environment for qualifier resolution (values-fr,
|
/** Non-composable system environment for qualifier resolution (values-fr,
|
||||||
drawable-dark, …): locale from SDL_GetPreferredLocales, theme from
|
drawable-dark, …): locale + theme come from the platform-env seams installed
|
||||||
SDL_GetSystemTheme. Density is reported as 1f — under this port's Option-B
|
by the SDL layer, so :components-resources carries no dependency on the sdl3
|
||||||
density flow layout runs in physical pixels and drawables are bundled at a
|
cinterop. Density is reported as 1f — under this port's Option-B density flow
|
||||||
single density, so the mdpi bucket is always the right one. The COMPOSABLE
|
layout runs in physical pixels and drawables are bundled at a single density,
|
||||||
path (rememberResourceEnvironment) doesn't use this: it reads
|
so the mdpi bucket is always the right one. The COMPOSABLE path
|
||||||
LocalDensity / isSystemInDarkTheme from the composition. */
|
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
|
||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
isSystemInDarkTheme from the composition. */
|
||||||
internal actual fun getSystemEnvironment(): ResourceEnvironment {
|
internal actual fun getSystemEnvironment(): ResourceEnvironment {
|
||||||
var vLanguage = ""
|
val (vLanguage, vRegion) = preferredLocaleProvider?.invoke() ?: ("" to "")
|
||||||
var vRegion = ""
|
val vDark = systemThemeIsDarkProvider?.invoke() ?: false
|
||||||
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
|
|
||||||
return ResourceEnvironment(
|
return ResourceEnvironment(
|
||||||
language = LanguageQualifier(vLanguage),
|
language = LanguageQualifier(vLanguage),
|
||||||
// SDL locales carry language+country only — no script subtag.
|
// 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 manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
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
|
// :animation-core — androidx.compose.animation.core.* vendored VERBATIM from
|
||||||
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
|
// 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.
|
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and
|
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and
|
||||||
@@ -16,8 +16,7 @@ plugins {
|
|||||||
alias(libs.plugins.compose.multiplatform)
|
alias(libs.plugins.compose.multiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
constructor <init>() // androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.plugin.compose)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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]
|
open annotation class androidx.compose.animation/ExperimentalAnimationApi : kotlin/Annotation { // androidx.compose.animation/ExperimentalAnimationApi|null[0]
|
||||||
constructor <init>() // androidx.compose.animation/ExperimentalAnimationApi.<init>|<init>(){}[0]
|
constructor <init>() // androidx.compose.animation/ExperimentalAnimationApi.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.plugin.compose)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
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).
|
// recomposer lifecycle, event dispatch, Snapshot apply notifications).
|
||||||
// Renderer selection happens entirely inside :core via source-set wiring
|
// Renderer selection happens entirely inside :ui via source-set wiring
|
||||||
// (skikoRendererMain vs sdlRendererMain) — this module just calls
|
// (skikoRendererMain — Skia-only) — this module just calls
|
||||||
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :core and
|
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
|
||||||
// the right symbol resolves per target.
|
// the right symbol resolves per target.
|
||||||
// Publication artifactId (when set up): compose-desktop-native.
|
// Publication artifactId (when set up): compose-desktop-native.
|
||||||
|
|
||||||
@@ -23,15 +23,15 @@ kotlin {
|
|||||||
|
|
||||||
applyDefaultHierarchyTemplate()
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
// sdl3.* types are api-exposed via :ui's cinterop klib — no separate
|
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib — no
|
||||||
// sdl3 cinterop here.
|
// separate sdl3 cinterop here.
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// api so apps depending on :window also get the compose re-impl,
|
// api so apps depending on :desktop-native-window also get the compose re-impl,
|
||||||
// Res/resources, GpuMode, and the renderer pipeline from :core, plus
|
// Res/resources, GpuMode, and the renderer pipeline from :ui, plus
|
||||||
// the foundation / animation-core / animation modules that were split
|
// 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
|
// Material widgets used to be re-exported from :material here; the
|
||||||
// module was retired when :apidemo and :demo migrated to :material3.
|
// module was retired when :apidemo and :demo migrated to :material3.
|
||||||
// Apps that want Material 3 widgets pull `implementation(project(":material3"))`
|
// Apps that want Material 3 widgets pull `implementation(project(":material3"))`
|
||||||
+56
-198
@@ -22,7 +22,6 @@ import androidx.compose.ui.input.key.key
|
|||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.input.pointer.PointerButton
|
import androidx.compose.ui.input.pointer.PointerButton
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import androidx.lifecycle.enableSavedStateHandles
|
|
||||||
import com.compose.sdl.node.ComposeRootHost
|
import com.compose.sdl.node.ComposeRootHost
|
||||||
import com.compose.sdl.res.currentImageLoader
|
import com.compose.sdl.res.currentImageLoader
|
||||||
import com.compose.sdl.window.LocalPopupHost
|
import com.compose.sdl.window.LocalPopupHost
|
||||||
@@ -35,7 +34,11 @@ import kotlinx.cinterop.toKString
|
|||||||
import kotlinx.cinterop.reinterpret
|
import kotlinx.cinterop.reinterpret
|
||||||
import kotlinx.cinterop.alloc
|
import kotlinx.cinterop.alloc
|
||||||
import kotlinx.cinterop.ptr
|
import kotlinx.cinterop.ptr
|
||||||
|
import kotlinx.cinterop.COpaquePointer
|
||||||
|
import kotlinx.cinterop.pointed
|
||||||
import sdl3.SDL_Delay
|
import sdl3.SDL_Delay
|
||||||
|
import sdl3.SDL_GetCurrentDisplayMode
|
||||||
|
import sdl3.SDL_GetDisplayForWindow
|
||||||
import sdl3.SDL_GetPerformanceCounter
|
import sdl3.SDL_GetPerformanceCounter
|
||||||
import sdl3.SDL_GetPerformanceFrequency
|
import sdl3.SDL_GetPerformanceFrequency
|
||||||
import sdl3.SDL_GetTicks
|
import sdl3.SDL_GetTicks
|
||||||
@@ -64,7 +67,7 @@ import sdl3.SDL_WaitEventTimeout
|
|||||||
routes events per SDL window id.
|
routes events per SDL window id.
|
||||||
|
|
||||||
Per-window pumping order matters: the render-bridge globals
|
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.
|
loop installs a window's globals before recomposing / laying out / drawing it.
|
||||||
|
|
||||||
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
|
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
|
||||||
@@ -142,8 +145,12 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
val appScope = ApplicationScopeImpl(runtime)
|
val appScope = ApplicationScopeImpl(runtime)
|
||||||
|
|
||||||
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
|
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()
|
runtime.markAllNeedFrame()
|
||||||
Snapshot.sendApplyNotifications()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appComposition.setContent { appScope.content() }
|
appComposition.setContent { appScope.content() }
|
||||||
@@ -216,6 +223,10 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
// Per-window pump + render.
|
// Per-window pump + render.
|
||||||
var vAnyRendered = false
|
var vAnyRendered = false
|
||||||
var vAllVsync = true
|
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
|
val vAppPending = appRecomposer.hasPendingWork
|
||||||
for (vW in runtime.windows.toList()) {
|
for (vW in runtime.windows.toList()) {
|
||||||
vW.installGlobals()
|
vW.installGlobals()
|
||||||
@@ -228,7 +239,10 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
if (vW.shouldRender()) {
|
if (vW.shouldRender()) {
|
||||||
vW.renderFrame()
|
vW.renderFrame()
|
||||||
vAnyRendered = true
|
vAnyRendered = true
|
||||||
if (!vW.backend.vsyncEnabled) vAllVsync = false
|
if (!vW.backend.vsyncEnabled) {
|
||||||
|
vAllVsync = false
|
||||||
|
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FrameProfiler.phase("render")
|
FrameProfiler.phase("render")
|
||||||
}
|
}
|
||||||
@@ -246,12 +260,19 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
// ============
|
// ============
|
||||||
// Pace / idle-skip.
|
// Pace / idle-skip.
|
||||||
if (vAnyRendered) {
|
if (vAnyRendered) {
|
||||||
SDL_Delay(if (vAllVsync) 1u else 16u)
|
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
|
||||||
} else if (!vAppPending) {
|
} else if (!vAppPending) {
|
||||||
// Nothing invalidated anywhere: block for events instead of
|
// Nothing invalidated anywhere: block for events instead of
|
||||||
// spinning. Keep FPS windows anchored to active periods.
|
// spinning. A real input/redraw event wakes this immediately; the
|
||||||
SDL_WaitEventTimeout(null, 10)
|
// 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()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
@@ -290,79 +311,6 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
SDL_Quit()
|
SDL_Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CDN_PROFILE=1 — per-phase timings, printed every ~2s of rendered frames.
|
|
||||||
A named-phase SINGLETON so both the main loop (events / app / pump / render)
|
|
||||||
AND renderFrame's sub-steps (render.layout / render.draw / render.present)
|
|
||||||
report into one line. `mark()` resets the stopwatch; `phase(name)` charges
|
|
||||||
the elapsed since the last mark/phase to that name. Measure first, optimize
|
|
||||||
second — see ROADMAP.md. */
|
|
||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
|
||||||
internal object FrameProfiler {
|
|
||||||
// null until first checked; then true/false for the run's lifetime.
|
|
||||||
private var fEnabled: Boolean? = null
|
|
||||||
// Output file — resolved once from CDN_PROFILE. Writing to a file (not
|
|
||||||
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
|
|
||||||
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
|
|
||||||
// the cwd; CDN_PROFILE=<path> → that path.
|
|
||||||
private var fPath: String = "cdn_profile.log"
|
|
||||||
val enabled: Boolean
|
|
||||||
get() = fEnabled ?: run {
|
|
||||||
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
|
||||||
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv
|
|
||||||
(vEnv != null).also { fEnabled = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
|
|
||||||
// Insertion-ordered so the printed line follows the call order.
|
|
||||||
private val fSum = LinkedHashMap<String, Double>()
|
|
||||||
private val fMax = LinkedHashMap<String, Double>()
|
|
||||||
private var fFrames = 0
|
|
||||||
private var fLastPrintMs = SDL_GetTicks()
|
|
||||||
private var fMark = 0uL
|
|
||||||
|
|
||||||
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() }
|
|
||||||
|
|
||||||
fun phase(inName: String) {
|
|
||||||
if (!enabled) return
|
|
||||||
val vNow = SDL_GetPerformanceCounter()
|
|
||||||
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
|
|
||||||
fSum[inName] = (fSum[inName] ?: 0.0) + vMs
|
|
||||||
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs
|
|
||||||
fMark = vNow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun frameDone(inRendered: Boolean) {
|
|
||||||
if (!enabled) return
|
|
||||||
if (inRendered) fFrames++
|
|
||||||
val vNowMs = SDL_GetTicks()
|
|
||||||
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
|
|
||||||
val vParts = fSum.keys.map { vName ->
|
|
||||||
val vAvg = (fSum[vName] ?: 0.0) / fFrames
|
|
||||||
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
|
||||||
}
|
|
||||||
// Per-frame draw-work averages (see DrawStats): what's inside `draw`.
|
|
||||||
val vStats = com.compose.sdl.graphics.DrawStats
|
|
||||||
val vDraw = "geo=${vStats.geometrySubmits / fFrames} verts=${vStats.vertices / fFrames} " +
|
|
||||||
"masks=${vStats.maskRealizations / fFrames} text=${vStats.textDraws / fFrames} img=${vStats.imageBlits / fFrames}"
|
|
||||||
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + " | per-frame " + vDraw + "\n"
|
|
||||||
val vFile = platform.posix.fopen(fPath, "a")
|
|
||||||
if (vFile != null) {
|
|
||||||
platform.posix.fputs(vLine, vFile)
|
|
||||||
platform.posix.fclose(vFile)
|
|
||||||
}
|
|
||||||
vStats.reset()
|
|
||||||
fSum.clear(); fMax.clear()
|
|
||||||
fFrames = 0
|
|
||||||
fLastPrintMs = vNowMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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)
|
// MARK: Probe / screenshot support (render-to-quiescence)
|
||||||
// ==================
|
// ==================
|
||||||
@@ -419,6 +367,18 @@ fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: f
|
|||||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||||
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
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.
|
/** Single-window compatibility wrapper — the pre-multi-window entry point.
|
||||||
Closing the window exits the app, exactly as before. */
|
Closing the window exits the app, exactly as before. */
|
||||||
fun nativeComposeWindow(
|
fun nativeComposeWindow(
|
||||||
@@ -776,6 +736,12 @@ internal class WindowInstance(
|
|||||||
// Register bundled generic fonts (FontFamily.Monospace → NotoSansMono) once
|
// Register bundled generic fonts (FontFamily.Monospace → NotoSansMono) once
|
||||||
// data.kres is loadable; idempotent, no-op if the font isn't bundled.
|
// data.kres is loadable; idempotent, no-op if the font isn't bundled.
|
||||||
com.compose.sdl.text.registerGenericFonts()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
@@ -797,8 +763,8 @@ internal class WindowInstance(
|
|||||||
// SDL3 delivers mouse coords in logical points on HiDPI — multiply by
|
// SDL3 delivers mouse coords in logical points on HiDPI — multiply by
|
||||||
// DPR so hit-testing lands in the pixel space layout uses.
|
// DPR so hit-testing lands in the pixel space layout uses.
|
||||||
val vDpr = backend.pixelDensity
|
val vDpr = backend.pixelDensity
|
||||||
val vPx = inEvent.event.x.toFloat() * vDpr
|
val vPx = inEvent.event.x * vDpr
|
||||||
val vPy = inEvent.event.y.toFloat() * vDpr
|
val vPy = inEvent.event.y * vDpr
|
||||||
if (inEvent.event.type == PointerEventType.Press) {
|
if (inEvent.event.type == PointerEventType.Press) {
|
||||||
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
||||||
}
|
}
|
||||||
@@ -823,7 +789,7 @@ internal class WindowInstance(
|
|||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
val vDpr = backend.pixelDensity
|
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) {
|
fun onKeyEvent(inEvent: AppEvent.Key) {
|
||||||
@@ -936,6 +902,11 @@ internal class WindowInstance(
|
|||||||
|
|
||||||
backend.updateWindowSize()
|
backend.updateWindowSize()
|
||||||
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
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)
|
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
||||||
// Deliver any state written by this iteration's frame-clock continuations
|
// Deliver any state written by this iteration's frame-clock continuations
|
||||||
// (withFrameNanos animations — notably smooth wheel scrolling) BEFORE we lay
|
// (withFrameNanos animations — notably smooth wheel scrolling) BEFORE we lay
|
||||||
@@ -1014,118 +985,5 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// WindowArchitectureOwner -> WindowArchitectureOwner.kt
|
||||||
// MARK: WindowArchitectureOwner
|
// BackNavigationInput / dispatchTypedText -> WindowInputHelpers.kt
|
||||||
// ==================
|
|
||||||
|
|
||||||
/** 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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
constructor <init>() // androidx.compose.foundation.layout/ExperimentalFlexBoxApi.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.plugin.compose)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
constructor <init>() // androidx.compose.foundation.lazy.grid/LazyGridScopeMarker.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||||
|
|
||||||
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.*
|
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.*
|
||||||
// (non-core) vendored VERBATIM from upstream. Sits on :core (which still owns
|
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
|
||||||
// androidx.compose.ui.*, both renderers, cinterops) + :animation-core.
|
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
|
||||||
//
|
//
|
||||||
// Why the two upstream modules (`:animation` and `:foundation`) are combined
|
// Why the two upstream modules (`:animation` and `:foundation`) are combined
|
||||||
// here: upstream splits `foundation-layout` off from `foundation`, then has
|
// 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()`,
|
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
|
||||||
// and its actual lives here as a plain nativeMain kt file (see
|
// and its actual lives here as a plain nativeMain kt file (see
|
||||||
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single
|
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single
|
||||||
// stub returning false. The upstream skiko/sdl per-renderer actuals were dropped
|
// stub returning false. The upstream skiko per-renderer actuals were dropped
|
||||||
// when :foundation was split from :core: expect/actual must live in the same
|
// when :foundation was split from :ui: expect/actual must live in the same
|
||||||
// module, and duplicating the skikoRenderer/sdlRenderer source-set hierarchy on
|
// module, and duplicating the skikoRenderer source-set hierarchy on
|
||||||
// :foundation just to read `LocalSystemTheme` on the Skia path was more build
|
// :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.
|
// wiring than is worth it for one function no in-repo app calls.
|
||||||
//
|
//
|
||||||
@@ -34,8 +34,7 @@ plugins {
|
|||||||
alias(libs.plugins.compose.multiplatform)
|
alias(libs.plugins.compose.multiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|||||||
@@ -517,6 +517,11 @@ skikoMain/kotlin/ -> src/vendor/native/kotlin/
|
|||||||
!skikoMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.skiko.kt
|
!skikoMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/foundation/Clickable.skiko.kt
|
!skikoMain/kotlin/androidx/compose/foundation/Clickable.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/foundation/DarkTheme.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/Scrollbar.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.skiko.kt
|
!skikoMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/foundation/text/CupertinoTextFieldPointerModifier.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)
|
|
||||||
+4
-3
@@ -2,9 +2,10 @@ package androidx.compose.foundation
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import sdl3.SDL_GetSystemTheme
|
import com.compose.sdl.res.systemThemeIsDarkProvider
|
||||||
import sdl3.SDL_SystemTheme
|
|
||||||
|
|
||||||
|
// System dark-theme flag via the platform-env seam (installed by the SDL layer),
|
||||||
|
// so :foundation carries no dependency on the sdl3 cinterop.
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@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
|
Byte-close mirror of upstream macosMain — desktop tap / selection gestures
|
||||||
delegate to the default helpers baked into TextFieldSelectionState.
|
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
|
ClipboardPasteState uses our project Clipboard's plaintext presence — no
|
||||||
NSPasteboard access on native.
|
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. */
|
/** No selection magnifier on desktop — mobile-only affordance. */
|
||||||
internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier = this
|
internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier = this
|
||||||
|
|
||||||
/** Desktop text-context-menu doesn't add SelectionContainer-specific
|
/** NO-OP by design: this seam belongs to the NEW text-context-menu API
|
||||||
items yet. TODO(CMP-7819): wire the paste-into-selection / clear
|
(ComposeFoundationFlags.isNewContextMenuEnabled), which is false on native, so
|
||||||
/ select-all extras through here when the toolbar lands. */
|
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(
|
internal actual fun Modifier.addSelectionContainerTextContextMenuComponents(
|
||||||
selectionManager: SelectionManager
|
selectionManager: SelectionManager
|
||||||
): Modifier = this
|
): Modifier = this
|
||||||
|
|||||||
+5
-1
@@ -20,7 +20,11 @@ internal actual fun TextFieldSelectionManager.isSelectionHandleInVisibleBound(
|
|||||||
isStartHandle: Boolean,
|
isStartHandle: Boolean,
|
||||||
): Boolean = isSelectionHandleInVisibleBoundDefault(isStartHandle)
|
): 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(
|
internal actual fun Modifier.addBasicTextFieldTextContextMenuComponents(
|
||||||
manager: TextFieldSelectionManager,
|
manager: TextFieldSelectionManager,
|
||||||
coroutineScope: CoroutineScope,
|
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 /
|
The four Material Symbols variable-font axes (fill / weight / grade /
|
||||||
opticalSize) come in via `fontVariationSettings`; use `MaterialIconAxes(…)`
|
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
|
@Composable
|
||||||
fun IconFontIcon(
|
fun IconFontIcon(
|
||||||
codepoint: Int,
|
codepoint: Int,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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 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 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]
|
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)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Show manifest properties: true
|
// - Show manifest properties: true
|
||||||
// - Show declarations: 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]
|
open annotation class androidx.compose.material3/ExperimentalMaterial3Api : kotlin/Annotation { // androidx.compose.material3/ExperimentalMaterial3Api|null[0]
|
||||||
constructor <init>() // androidx.compose.material3/ExperimentalMaterial3Api.<init>|<init>(){}[0]
|
constructor <init>() // androidx.compose.material3/ExperimentalMaterial3Api.<init>|<init>(){}[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ kotlin {
|
|||||||
api(project(":animation-core"))
|
api(project(":animation-core"))
|
||||||
api(project(":material-ripple"))
|
api(project(":material-ripple"))
|
||||||
// androidx.collection is used by material3 internals (MutableIntObjectMap, …).
|
// 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")
|
implementation("androidx.collection:collection:1.5.0")
|
||||||
// androidx.graphics.shapes.* — MaterialShapes (RoundedPolygon /
|
// androidx.graphics.shapes.* — MaterialShapes (RoundedPolygon /
|
||||||
// Morph / CornerRounding) that WavyProgress / LoadingIndicator use.
|
// 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
|
Upstream ships this via NSDateFormatter (darwinMain) or java.text.DateFormat
|
||||||
(desktopMain). Neither is available for K/N on Linux/Windows, so we ship a
|
(desktopMain). Neither is available for K/N on Linux/Windows, so this actual is
|
||||||
minimal English-only stub that unblocks DatePicker + TimePicker compilation.
|
a cross-platform kotlinx-datetime formatter that HONOURS the requested
|
||||||
Localization is TODO; format strings are matched via a small dispatcher on the
|
CLDR-style pattern / skeleton (so DatePicker/TimePicker headlines read
|
||||||
subset of patterns material3 actually asks for.
|
"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)
|
@OptIn(ExperimentalTime::class)
|
||||||
internal actual class PlatformDateFormat actual constructor(
|
internal actual class PlatformDateFormat actual constructor(
|
||||||
@@ -33,13 +35,13 @@ internal actual class PlatformDateFormat actual constructor(
|
|||||||
utcTimeMillis: Long,
|
utcTimeMillis: Long,
|
||||||
pattern: String,
|
pattern: String,
|
||||||
cache: MutableMap<String, Any>,
|
cache: MutableMap<String, Any>,
|
||||||
): String = defaultFormat(utcTimeMillis)
|
): String = formatPattern(utcTimeMillis, pattern)
|
||||||
|
|
||||||
actual fun formatWithSkeleton(
|
actual fun formatWithSkeleton(
|
||||||
utcTimeMillis: Long,
|
utcTimeMillis: Long,
|
||||||
skeleton: String,
|
skeleton: String,
|
||||||
cache: MutableMap<String, Any>,
|
cache: MutableMap<String, Any>,
|
||||||
): String = defaultFormat(utcTimeMillis)
|
): String = formatPattern(utcTimeMillis, skeletonToPattern(skeleton))
|
||||||
|
|
||||||
actual fun parse(
|
actual fun parse(
|
||||||
date: String,
|
date: String,
|
||||||
@@ -64,11 +66,91 @@ internal actual class PlatformDateFormat actual constructor(
|
|||||||
|
|
||||||
actual fun is24HourFormat(): Boolean = true
|
actual fun is24HourFormat(): Boolean = true
|
||||||
|
|
||||||
private fun defaultFormat(utcTimeMillis: Long): String {
|
// ============
|
||||||
val vInstant = Instant.fromEpochMilliseconds(utcTimeMillis)
|
// Pattern / skeleton formatting
|
||||||
val vDt = vInstant.toLocalDateTime(TimeZone.UTC)
|
|
||||||
return "${vDt.year}-${vDt.monthNumber.pad2()}-${vDt.dayOfMonth.pad2()}"
|
/** 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 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 manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
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 manifest properties: true
|
||||||
// - Show declarations: 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]
|
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]
|
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
|
* Internal helper invoked by the project's `BackgroundNode.draw()` body
|
||||||
* (Phase 8 chain-driven draw). Resolves the [Shape] into a project
|
* (Phase 8 chain-driven draw). Resolves the [Shape] into a project
|
||||||
* [Outline] and dispatches to the appropriate `DrawScope.drawXxx`. The
|
* [Outline] and dispatches to the appropriate `DrawScope.drawXxx`. The
|
||||||
* receiver [DrawScope] is the renderer's wrap scope (Skia or SDL3) —
|
* receiver [DrawScope] is the Skia renderer's wrap scope — any of its
|
||||||
* any of its `drawRect / drawRoundRect / drawPath` calls becomes a
|
* `drawRect / drawRoundRect / drawPath` calls becomes a concrete paint
|
||||||
* concrete paint operation on the backend canvas.
|
* operation on the backend canvas.
|
||||||
*
|
*
|
||||||
* Note `Shape.createOutline` is the upstream three-arg signature
|
* Note `Shape.createOutline` is the upstream three-arg signature
|
||||||
* (`size`, `layoutDirection`, `density`); project shapes implement it
|
* (`size`, `layoutDirection`, `density`); project shapes implement it
|
||||||
+5
-5
@@ -16,11 +16,11 @@ import androidx.compose.ui.graphics.TileMode
|
|||||||
// LinearGradient / RadialGradient / SweepGradient field accessors
|
// LinearGradient / RadialGradient / SweepGradient field accessors
|
||||||
// `internal`, which keeps `androidx.compose.ui.graphics.LinearGradient.colors`
|
// `internal`, which keeps `androidx.compose.ui.graphics.LinearGradient.colors`
|
||||||
// etc. out of the public ABI. Sealed-class scope forces the concrete
|
// etc. out of the public ABI. Sealed-class scope forces the concrete
|
||||||
// gradient classes to live in `androidx.compose.ui.graphics`, but our
|
// gradient classes to live in `androidx.compose.ui.graphics`, but the Skia
|
||||||
// renderer-skia / renderer-sdl3 modules can't read the resulting `internal`
|
// renderer code in skikoRendererMain can't read the resulting `internal`
|
||||||
// fields directly. The extensions below sit in :core (same module as the
|
// fields directly. The extensions below sit in this module's commonMain (same
|
||||||
// internal fields, so the bodies CAN read them) and are themselves public,
|
// module as the internal fields, so the bodies CAN read them) and are
|
||||||
// so :renderer-skia / :renderer-sdl3 import them and reach gradient state
|
// themselves public, so the renderer imports them and reaches gradient state
|
||||||
// through them.
|
// 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)
|
// 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
|
the render nodes call it when a layer carries shadowElevation > 0
|
||||||
(Modifier.shadow, m3 Surface tonal+shadow elevation). The outline is the
|
(Modifier.shadow, m3 Surface tonal+shadow elevation). The outline is the
|
||||||
layer's resolved shape in LAYER-LOCAL pixels — the canvas is already
|
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).
|
paint BEFORE the layer's clip (it lives outside the bounds).
|
||||||
|
|
||||||
Implementations:
|
Implementations:
|
||||||
- SkiaBackedCanvas — a real Gaussian blur MaskFilter on the outline.
|
- 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). */
|
|
||||||
interface NativeShadowCanvas {
|
interface NativeShadowCanvas {
|
||||||
fun drawDropShadow(
|
fun drawDropShadow(
|
||||||
inOutline: Outline,
|
inOutline: Outline,
|
||||||
+4
-4
@@ -11,10 +11,10 @@ import androidx.compose.ui.geometry.RoundRect
|
|||||||
a rounded-rect / circle OUTLINE (not just its bounding box).
|
a rounded-rect / circle OUTLINE (not just its bounding box).
|
||||||
|
|
||||||
The Skia backend does NOT implement this: Skia's `Canvas.clipPath` already
|
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
|
clips to a rounded path natively. This side-channel exists for any backend
|
||||||
(only a rectangular render clip), so `Sdl3Canvas` implements this by rendering
|
that has no path-clip primitive (only a rectangular render clip) and must
|
||||||
the clipped subtree into an offscreen target and masking it to the rounded shape
|
emulate it by rendering the clipped subtree into an offscreen target and
|
||||||
(lazily — see the pending-clip machinery there).
|
masking it to the rounded shape.
|
||||||
|
|
||||||
The render nodes check `canvas is NativeShapeClipCanvas` when a layer has an
|
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()` /
|
`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
|
/** Shared bridge the commonMain layout pass uses to size images and read raw
|
||||||
bytes. The active RenderBackend installs an implementation at startup
|
bytes. The active RenderBackend installs an implementation at startup.
|
||||||
(parallel to currentTextMeasurer). Actual drawing is done by the renderer
|
Actual drawing is done by the renderer
|
||||||
directly off the LayoutNode painter leaf, reusing the backend's decode
|
directly off the LayoutNode painter leaf, reusing the backend's decode
|
||||||
cache — so a decoded texture is created once and shared by measure + draw. */
|
cache — so a decoded texture is created once and shared by measure + draw. */
|
||||||
interface ImageLoader {
|
interface ImageLoader {
|
||||||
@@ -56,6 +56,13 @@ private val kFallbackImageLoader = object : ImageLoader {
|
|||||||
/** Installed by ComposeWindow from renderBackend.imageLoader. */
|
/** Installed by ComposeWindow from renderBackend.imageLoader. */
|
||||||
var currentImageLoader: ImageLoader = kFallbackImageLoader
|
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
|
// 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.
|
the renderer, so this reduced type carries just the resource pointer.
|
||||||
|
|
||||||
[onDraw] casts the current Canvas to [NativePainterCanvas] (implemented by
|
[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
|
If the Canvas doesn't implement NativePainterCanvas (offscreen layer capture
|
||||||
for graphics-layer effects) the paint is skipped.
|
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
|
// translate + scaledSize, so we paint the resource FillBounds into the
|
||||||
// full DrawScope size. Alpha is threaded via applyAlpha.
|
// full DrawScope size. Alpha is threaded via applyAlpha.
|
||||||
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
|
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
|
||||||
resourcePath, kind, 0f, 0f, size.width, size.height,
|
resourcePath, kind, 0f, 0f, size.width, size.height, fAlpha,
|
||||||
androidx.compose.ui.layout.ContentScale.FillBounds, fAlpha,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -11,4 +11,4 @@ import androidx.compose.ui.graphics.GraphicsContext
|
|||||||
(the createRenderBackend trick). SDL → the project ProjectGraphicsContext (record/
|
(the createRenderBackend trick). SDL → the project ProjectGraphicsContext (record/
|
||||||
replay via the SDL NativeRenderNode); Skia → upstream's SkiaGraphicsContext (owns a
|
replay via the SDL NativeRenderNode); Skia → upstream's SkiaGraphicsContext (owns a
|
||||||
skiko RenderNodeContext). See RENDERER.md §4 (B2). */
|
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
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
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
|
/** Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
|
||||||
drawable [ImageBitmap]. The active renderer registers its implementation at
|
drawable [ImageBitmap]. The active renderer registers its implementation at
|
||||||
backend construction — the SDL backend goes through SDL3_image
|
backend construction — the Skia backend goes through Image.makeFromEncoded /
|
||||||
(Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
|
|
||||||
SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
|
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
|
Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
|
||||||
painterResource(Res.drawable.x) works without that module reaching into
|
painterResource(Res.drawable.x) works without that module reaching into
|
||||||
renderer internals. */
|
renderer internals. */
|
||||||
interface EncodedImageDecoder {
|
interface EncodedImageDecoder {
|
||||||
fun decode(inBytes: ByteArray): ImageBitmap?
|
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
|
/** Volatile: written by the render backend on the main thread, read by the
|
||||||
resources pipeline on Dispatchers.Default workers. */
|
resources pipeline on Dispatchers.Default workers. */
|
||||||
@kotlin.concurrent.Volatile
|
@kotlin.concurrent.Volatile
|
||||||
var encodedImageDecoder: EncodedImageDecoder? = null
|
var encodedImageDecoder: EncodedImageDecoder? = null
|
||||||
internal set
|
|
||||||
|
|
||||||
/** Decode via the active renderer's registered decoder — null when no renderer
|
/** Decode via the active renderer's registered decoder — null when no renderer
|
||||||
* has initialised yet or the bytes aren't a supported image. */
|
* has initialised yet or the bytes aren't a supported image. */
|
||||||
fun decodeEncodedImageBitmap(inBytes: ByteArray): ImageBitmap? =
|
fun decodeEncodedImageBitmap(inBytes: ByteArray): ImageBitmap? =
|
||||||
encodedImageDecoder?.decode(inBytes)
|
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
|
// Builds the layer transform (translation / rotation / scale / camera about a
|
||||||
// pivot) into [matrix]. Matches the skiko copy so hit-testing agrees across legs.
|
// pivot) into [matrix]. Matches the skiko copy so hit-testing agrees across legs.
|
||||||
internal fun prepareLayerTransformationMatrix(
|
fun prepareLayerTransformationMatrix(
|
||||||
matrix: Matrix,
|
matrix: Matrix,
|
||||||
pivotX: Float,
|
pivotX: Float,
|
||||||
pivotY: Float,
|
pivotY: Float,
|
||||||
+5
-7
@@ -17,11 +17,11 @@
|
|||||||
// DERIVED (copy + edit) from compose-multiplatform-core -- MANUAL VENDOR, NON-IDEMPOTENT.
|
// 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
|
// 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
|
// ONLY change vs upstream: this class also implements the port draw contracts
|
||||||
// (NativeTextCanvas / NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so
|
// (NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so the
|
||||||
// the port text engine (SkiaTextRenderer, P3.1 metrics) + resource-image cache + elevation
|
// resource-image cache + elevation shadows keep working on the Skia renderer,
|
||||||
// shadows keep working on the Skia leg, which now draws through THIS canvas instead of the
|
// which draws through THIS canvas instead of the retired project SkiaCanvas.
|
||||||
// retired project SkiaCanvas. Bridged methods forward to `skiaLeafDrawer` using
|
// Bridged methods forward to `skiaLeafDrawer` using `internalSkiaCanvas`.
|
||||||
// `internalSkiaCanvas`. See RENDERER.md (B6.1, section 4).
|
// See RENDERER.md (B6.1, section 4).
|
||||||
|
|
||||||
package androidx.compose.ui.graphics
|
package androidx.compose.ui.graphics
|
||||||
|
|
||||||
@@ -503,7 +503,6 @@ internal class SkiaBackedCanvas(
|
|||||||
inY: Float,
|
inY: Float,
|
||||||
inWidth: Float,
|
inWidth: Float,
|
||||||
inHeight: Float,
|
inHeight: Float,
|
||||||
inContentScale: androidx.compose.ui.layout.ContentScale,
|
|
||||||
inAlpha: Float,
|
inAlpha: Float,
|
||||||
) {
|
) {
|
||||||
val drawer = skiaLeafDrawer ?: return
|
val drawer = skiaLeafDrawer ?: return
|
||||||
@@ -515,7 +514,6 @@ internal class SkiaBackedCanvas(
|
|||||||
inY = inY,
|
inY = inY,
|
||||||
inW = inWidth,
|
inW = inWidth,
|
||||||
inH = inHeight,
|
inH = inHeight,
|
||||||
inScale = inContentScale,
|
|
||||||
inAlpha = inAlpha,
|
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
|
/** The Skia leg uses upstream's SkiaGraphicsContext, which owns a skiko
|
||||||
RenderNodeContext and creates upstream GraphicsLayer(skiko.RenderNode) instances.
|
RenderNodeContext and creates upstream GraphicsLayer(skiko.RenderNode) instances.
|
||||||
See RENDERER.md (B6.2, section 4). */
|
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
|
Returns null if anything along the way fails: the Surface may not yet
|
||||||
exist (first frame), the GPU readback may fail, or bitmap.readPixels()
|
exist (first frame), the GPU readback may fail, or bitmap.readPixels()
|
||||||
may not be exposed in this Skiko build. */
|
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
|
val vSurface = inSurface ?: return null
|
||||||
if (inWidth <= 0 || inHeight <= 0) return null
|
if (inWidth <= 0 || inHeight <= 0) return null
|
||||||
val vBitmap = Bitmap()
|
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()
|
||||||
+2
-2
@@ -10,8 +10,8 @@ package com.compose.sdl.renderer.skia
|
|||||||
`SkiaBackedCanvas`'s `NativePainterCanvas` contract forwards to. Set once by
|
`SkiaBackedCanvas`'s `NativePainterCanvas` contract forwards to. Set once by
|
||||||
`SkiaRenderBackend`; any `SkiaBackedCanvas` (frame, offscreen, or a GraphicsLayer
|
`SkiaRenderBackend`; any `SkiaBackedCanvas` (frame, offscreen, or a GraphicsLayer
|
||||||
recording — B6.2) can then draw images onto its own `internalSkiaCanvas`. */
|
recording — B6.2) can then draw images onto its own `internalSkiaCanvas`. */
|
||||||
internal var skiaLeafDrawer: SkiaLeafDrawer? = null
|
var skiaLeafDrawer: SkiaLeafDrawer? = null
|
||||||
|
|
||||||
internal class SkiaLeafDrawer(
|
class SkiaLeafDrawer(
|
||||||
val imageCache: SkiaImageCache,
|
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>()
|
private val fFonts = mutableMapOf<String, ByteArray>()
|
||||||
// Families that render as single variable-axis glyphs (Material Symbols),
|
// Families that render as single variable-axis glyphs (Material Symbols),
|
||||||
// vs ordinary text fonts. The SDL3 renderer routes only these through its
|
// vs ordinary text fonts — icon families are drawn one codepoint at a time,
|
||||||
// single-glyph FreeType icon path; text families (e.g. a bundled
|
// while text families (e.g. a bundled monospace) go through normal
|
||||||
// monospace) must go through normal full-string rendering.
|
// full-string rendering.
|
||||||
private val fIconFamilies = mutableSetOf<String>()
|
private val fIconFamilies = mutableSetOf<String>()
|
||||||
|
|
||||||
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
|
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
|
||||||
@@ -32,9 +32,9 @@ object IconFont {
|
|||||||
fFonts[inFamily] = inBytes
|
fFonts[inFamily] = inBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register an ICON font family (Material Symbols et al.) — variable-axis,
|
/** 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
|
Same byte store as register(), but also flags the family as an icon
|
||||||
register(), but also flags the family as an icon font. */
|
font. */
|
||||||
fun registerIcon(inFamily: String, inBytes: ByteArray) {
|
fun registerIcon(inFamily: String, inBytes: ByteArray) {
|
||||||
fFonts[inFamily] = inBytes
|
fFonts[inFamily] = inBytes
|
||||||
fIconFamilies += inFamily
|
fIconFamilies += inFamily
|
||||||
+7
-1
@@ -38,12 +38,18 @@ class NamedFont(
|
|||||||
override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
|
override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
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 {
|
override fun hashCode(): Int {
|
||||||
var h = name.hashCode()
|
var h = name.hashCode()
|
||||||
h = 31 * h + weight.hashCode()
|
h = 31 * h + weight.hashCode()
|
||||||
h = 31 * h + style.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
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
+2
-3
@@ -10,9 +10,8 @@ package com.compose.sdl.text
|
|||||||
something the renderer can't deliver. Today the only consumer is the
|
something the renderer can't deliver. Today the only consumer is the
|
||||||
Material Symbols install() path, which emits a one-shot warning when
|
Material Symbols install() path, which emits a one-shot warning when
|
||||||
variable-font axes won't be honoured. The Skia renderer sets
|
variable-font axes won't be honoured. The Skia renderer sets
|
||||||
supportsFontVariations = true via Typeface.makeClone; the SDL3 renderer
|
supportsFontVariations = true via Typeface.makeClone; a renderer that
|
||||||
leaves it false because SDL3_ttf 3.2 has no axis-set API (filename /
|
couldn't honour variable-font axes would leave it false.
|
||||||
iostream / size / face / dpi only).
|
|
||||||
|
|
||||||
`supportsFontVariations` is null until a renderer initialises — code
|
`supportsFontVariations` is null until a renderer initialises — code
|
||||||
that warns on install() ignores the null case so it doesn't fire when
|
that warns on install() ignores the null case so it doesn't fire when
|
||||||
+1
-1
@@ -20,7 +20,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
SDL_TEXT_INPUT_EVENT), replace this with a real implementation.
|
SDL_TEXT_INPUT_EVENT), replace this with a real implementation.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
internal object NoOpPlatformTextInputService : PlatformTextInputService {
|
object NoOpPlatformTextInputService : PlatformTextInputService {
|
||||||
override fun startInput(
|
override fun startInput(
|
||||||
value: TextFieldValue,
|
value: TextFieldValue,
|
||||||
imeOptions: ImeOptions,
|
imeOptions: ImeOptions,
|
||||||
+17
-9
@@ -1,18 +1,25 @@
|
|||||||
package androidx.compose.ui.text
|
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)
|
// MARK: FontRasterizationSettings native actual (Skia-free)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hand-written port of upstream `FontRasterizationSettings.skiko.kt` minus
|
* Rule-3 manual vendor of upstream `FontRasterizationSettings.skiko.kt` minus the
|
||||||
* the Skia-only `toSkFontEdging` / `toSkFontHinting` extensions and the
|
* Skia-only `toSkFontEdging` / `toSkFontHinting` extensions (the skiko renderer
|
||||||
* `currentPlatform()` lookup. Vendored TextStyle.native.kt references
|
* maps `PlatformDefault` to skiko enums itself, in SkiaParagraphEngine). It lives
|
||||||
* `FontRasterizationSettings.PlatformDefault` so we need the class +
|
* in the skiko-FREE `nativeMain` because vendored `TextStyle.native.kt` references
|
||||||
* companion to exist; the renderer doesn't read these values yet so a
|
* `FontRasterizationSettings.PlatformDefault`, and `nativeMain` is the shared parent
|
||||||
* single shared "PlatformDefault" instance (AntiAlias / Normal /
|
* of the official-skiko (mac/linux) and fork-skiko (mingw) legs — so it can't carry
|
||||||
* subpixel-on / autohint-off — matches upstream's macOS/Linux default)
|
* skiko. `PlatformDefault` reproduces upstream's per-OS defaults VERBATIM (verified
|
||||||
* is fine.
|
* 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
|
@ExperimentalTextApi
|
||||||
@@ -38,9 +45,10 @@ class FontRasterizationSettings(
|
|||||||
val autoHintingForced: Boolean,
|
val autoHintingForced: Boolean,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@OptIn(ExperimentalNativeApi::class)
|
||||||
val PlatformDefault: FontRasterizationSettings = FontRasterizationSettings(
|
val PlatformDefault: FontRasterizationSettings = FontRasterizationSettings(
|
||||||
smoothing = FontSmoothing.AntiAlias,
|
smoothing = FontSmoothing.AntiAlias,
|
||||||
hinting = FontHinting.Normal,
|
hinting = if (Platform.osFamily == OsFamily.LINUX) FontHinting.Slight else FontHinting.Normal,
|
||||||
subpixelPositioning = true,
|
subpixelPositioning = true,
|
||||||
autoHintingForced = false,
|
autoHintingForced = false,
|
||||||
)
|
)
|
||||||
+43
-34
@@ -27,19 +27,12 @@ private fun makeSkiaParagraph(
|
|||||||
ellipsize: Boolean,
|
ellipsize: Boolean,
|
||||||
density: Float,
|
density: Float,
|
||||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||||
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
): Paragraph = SkiaParagraph(
|
): Paragraph = SkiaParagraph(
|
||||||
text, style, width,
|
text, style, width,
|
||||||
buildParagraphOps(text, style, width, maxLines, ellipsize, density, spanStyles),
|
buildParagraphOps(text, style, width, maxLines, ellipsize, density, spanStyles, placeholders),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** [min, max] intrinsic width from a throwaway unbounded skiko layout. */
|
|
||||||
internal expect fun paragraphIntrinsicWidths(
|
|
||||||
text: String,
|
|
||||||
style: TextStyle,
|
|
||||||
density: Float,
|
|
||||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
|
||||||
): FloatArray
|
|
||||||
|
|
||||||
/** Carries text+style for the intrinsics-based Paragraph factories; intrinsic
|
/** Carries text+style for the intrinsics-based Paragraph factories; intrinsic
|
||||||
widths come from an unbounded skiko layout. `density` is the LocalDensity
|
widths come from an unbounded skiko layout. `density` is the LocalDensity
|
||||||
scalar (dpr on Retina): it converts sp → pixels so intrinsic widths land in
|
scalar (dpr on Retina): it converts sp → pixels so intrinsic widths land in
|
||||||
@@ -49,13 +42,35 @@ internal class NativeParagraphIntrinsics(
|
|||||||
val paragraphStyle: TextStyle,
|
val paragraphStyle: TextStyle,
|
||||||
val density: Float,
|
val density: Float,
|
||||||
val spanStyles: List<AnnotatedString.Range<SpanStyle>> = emptyList(),
|
val spanStyles: List<AnnotatedString.Range<SpanStyle>> = emptyList(),
|
||||||
|
val placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList(),
|
||||||
) : ParagraphIntrinsics {
|
) : ParagraphIntrinsics {
|
||||||
private val widths = paragraphIntrinsicWidths(paragraphText, paragraphStyle, density, spanStyles)
|
// Shape ONCE here (unbounded, no line cap). The shaped paragraph is RETAINED
|
||||||
override val minIntrinsicWidth: Float = widths[0]
|
// and reused by Paragraph(this, …) for the final layout — skiko re-breaks at
|
||||||
override val maxIntrinsicWidth: Float = widths[1]
|
// 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
|
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 =
|
private fun widthFrom(constraints: Constraints): Float =
|
||||||
if (constraints.hasBoundedWidth) constraints.maxWidth.toFloat() else Float.POSITIVE_INFINITY
|
if (constraints.hasBoundedWidth) constraints.maxWidth.toFloat() else Float.POSITIVE_INFINITY
|
||||||
|
|
||||||
@@ -69,7 +84,7 @@ actual fun ParagraphIntrinsics(
|
|||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
density: Density,
|
density: Density,
|
||||||
resourceLoader: Font.ResourceLoader,
|
resourceLoader: Font.ResourceLoader,
|
||||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles)
|
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles, placeholders)
|
||||||
|
|
||||||
actual fun ParagraphIntrinsics(
|
actual fun ParagraphIntrinsics(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -78,7 +93,7 @@ actual fun ParagraphIntrinsics(
|
|||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
density: Density,
|
density: Density,
|
||||||
fontFamilyResolver: FontFamily.Resolver,
|
fontFamilyResolver: FontFamily.Resolver,
|
||||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles)
|
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles, placeholders)
|
||||||
|
|
||||||
actual fun ParagraphIntrinsics(
|
actual fun ParagraphIntrinsics(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -87,7 +102,7 @@ actual fun ParagraphIntrinsics(
|
|||||||
density: Density,
|
density: Density,
|
||||||
fontFamilyResolver: FontFamily.Resolver,
|
fontFamilyResolver: FontFamily.Resolver,
|
||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
|
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles(), placeholders)
|
||||||
|
|
||||||
actual fun ParagraphIntrinsics(
|
actual fun ParagraphIntrinsics(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -97,7 +112,7 @@ actual fun ParagraphIntrinsics(
|
|||||||
fontFamilyResolver: FontFamily.Resolver,
|
fontFamilyResolver: FontFamily.Resolver,
|
||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
softWrap: Boolean,
|
softWrap: Boolean,
|
||||||
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
|
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles(), placeholders)
|
||||||
|
|
||||||
// AnnotatedString.Annotation is a sealed interface (SpanStyle, ParagraphStyle,
|
// AnnotatedString.Annotation is a sealed interface (SpanStyle, ParagraphStyle,
|
||||||
// LinkAnnotation, StringAnnotation, TtsAnnotation, …). Only SpanStyle affects
|
// LinkAnnotation, StringAnnotation, TtsAnnotation, …). Only SpanStyle affects
|
||||||
@@ -120,7 +135,7 @@ actual fun Paragraph(
|
|||||||
width: Float,
|
width: Float,
|
||||||
density: Density,
|
density: Density,
|
||||||
resourceLoader: Font.ResourceLoader,
|
resourceLoader: Font.ResourceLoader,
|
||||||
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles)
|
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -132,7 +147,7 @@ actual fun Paragraph(
|
|||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
ellipsis: Boolean,
|
ellipsis: Boolean,
|
||||||
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles)
|
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -144,7 +159,7 @@ actual fun Paragraph(
|
|||||||
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
ellipsis: Boolean,
|
ellipsis: Boolean,
|
||||||
): Paragraph = makeSkiaParagraph(text, style, widthFrom(constraints), maxLines, ellipsis, density.density, spanStyles)
|
): Paragraph = makeSkiaParagraph(text, style, widthFrom(constraints), maxLines, ellipsis, density.density, spanStyles, placeholders)
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -157,7 +172,7 @@ actual fun Paragraph(
|
|||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
overflow: TextOverflow,
|
overflow: TextOverflow,
|
||||||
): Paragraph = makeSkiaParagraph(
|
): Paragraph = makeSkiaParagraph(
|
||||||
text, style, widthFrom(constraints), maxLines, overflow == TextOverflow.Ellipsis, density.density, spanStyles,
|
text, style, widthFrom(constraints), maxLines, overflow == TextOverflow.Ellipsis, density.density, spanStyles, placeholders,
|
||||||
)
|
)
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
@@ -165,30 +180,24 @@ actual fun Paragraph(
|
|||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
ellipsis: Boolean,
|
ellipsis: Boolean,
|
||||||
width: Float,
|
width: Float,
|
||||||
): Paragraph {
|
): Paragraph =
|
||||||
val i = paragraphIntrinsics as NativeParagraphIntrinsics
|
paragraphFromIntrinsics(paragraphIntrinsics as NativeParagraphIntrinsics, width, maxLines, ellipsis)
|
||||||
return makeSkiaParagraph(i.paragraphText, i.paragraphStyle, width, maxLines, ellipsis, i.density, i.spanStyles)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
paragraphIntrinsics: ParagraphIntrinsics,
|
paragraphIntrinsics: ParagraphIntrinsics,
|
||||||
constraints: Constraints,
|
constraints: Constraints,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
ellipsis: Boolean,
|
ellipsis: Boolean,
|
||||||
): Paragraph {
|
): Paragraph =
|
||||||
val i = paragraphIntrinsics as NativeParagraphIntrinsics
|
paragraphFromIntrinsics(paragraphIntrinsics as NativeParagraphIntrinsics, widthFrom(constraints), maxLines, ellipsis)
|
||||||
return makeSkiaParagraph(i.paragraphText, i.paragraphStyle, widthFrom(constraints), maxLines, ellipsis, i.density, i.spanStyles)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun Paragraph(
|
actual fun Paragraph(
|
||||||
paragraphIntrinsics: ParagraphIntrinsics,
|
paragraphIntrinsics: ParagraphIntrinsics,
|
||||||
constraints: Constraints,
|
constraints: Constraints,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
overflow: TextOverflow,
|
overflow: TextOverflow,
|
||||||
): Paragraph {
|
): Paragraph =
|
||||||
val i = paragraphIntrinsics as NativeParagraphIntrinsics
|
paragraphFromIntrinsics(
|
||||||
return makeSkiaParagraph(
|
paragraphIntrinsics as NativeParagraphIntrinsics, widthFrom(constraints), maxLines,
|
||||||
i.paragraphText, i.paragraphStyle, widthFrom(constraints), maxLines,
|
overflow == TextOverflow.Ellipsis,
|
||||||
overflow == TextOverflow.Ellipsis, i.density, i.spanStyles,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
+11
@@ -78,6 +78,16 @@ internal interface NativeParagraphOps {
|
|||||||
fun wordBoundary(offset: Int): IntArray // [start, end]
|
fun wordBoundary(offset: Int): IntArray // [start, end]
|
||||||
|
|
||||||
fun rebuildAndPaint(canvas: Canvas, color: Color, shadow: Shadow?, decoration: TextDecoration?)
|
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). */
|
/** Bridge to the skiko ops impl (actual in skikoRendererMain). */
|
||||||
@@ -89,6 +99,7 @@ internal expect fun buildParagraphOps(
|
|||||||
ellipsize: Boolean,
|
ellipsize: Boolean,
|
||||||
density: Float,
|
density: Float,
|
||||||
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
|
||||||
|
placeholders: List<AnnotatedString.Range<Placeholder>>,
|
||||||
): NativeParagraphOps
|
): NativeParagraphOps
|
||||||
|
|
||||||
internal class SkiaParagraph(
|
internal class SkiaParagraph(
|
||||||
+1
-1
@@ -10,7 +10,7 @@ package androidx.compose.ui.text.font
|
|||||||
* that-isn't-a-FontListFontFamily" adapter. Upstream's skiko actual routes
|
* that-isn't-a-FontListFontFamily" adapter. Upstream's skiko actual routes
|
||||||
* `Default`, `SansSerif`, `Serif`, `Monospace`, `Cursive`, and `LoadedFontFamily`
|
* `Default`, `SansSerif`, `Serif`, `Monospace`, `Cursive`, and `LoadedFontFamily`
|
||||||
* through `SkiaFontLoader` to produce a real `SkTypeface`. Our text renderer
|
* 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
|
* [com.compose.sdl.text.NamedFont] directly to look the family up
|
||||||
* against its registered font table, so the resolver's typeface is never
|
* against its registered font table, so the resolver's typeface is never
|
||||||
* consulted. A no-op adapter (returns an [TypefaceResult.Immutable] wrapping
|
* 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
|
// 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.
|
// reports none. Cheap: the tag list is cached after the first non-empty read.
|
||||||
private fun systemCurrentLocale(): Locale =
|
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 {
|
object : PlatformLocaleDelegate {
|
||||||
override val current: LocaleList
|
override val current: LocaleList
|
||||||
get() {
|
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")))
|
return if (tags.isEmpty()) LocaleList(listOf(Locale("en-US")))
|
||||||
else LocaleList(tags.map(::Locale))
|
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