Compare commits

...
21 Commits
Author SHA1 Message Date
Bitsy 4272cec614 refactor(coords): per-area com.bitsycore groups, :window → :desktop-native-window, skiko fork → com.bitsycore.skiko
Rebrand the fork's published coordinates as the 1:1 com.bitsycore mirror of
their org.jetbrains originals, so each module is an obvious fork under the
bitsycore name:
- Per-area groups: com.bitsycore.compose.sdl:<mod> → com.bitsycore.compose.<area>:<mod>
  (ui / foundation / animation / material3 / material / components / navigation3),
  mirroring org.jetbrains.compose.<area>. Project-only modules (:sdl-core,
  :material-symbols) stay com.bitsycore.compose.sdl. Driven by a groupFor() map
  in root build.gradle.kts; the kUseGhPackages substitution + the consumer bridge
  plugin's bridgeTable now emit full per-area coords; publish.yml host_packages
  maps each module to its group (+ adds the split modules to the MODULES list).
- :window renamed to :desktop-native-window (dir compose/sdl/window →
  compose/desktop/native/window; sdl/ now holds only :sdl-core). Published as the
  headline app-shell artifact com.bitsycore.compose:desktop-native-window.
- Consumed skiko mingw fork org.jetbrains.skiko:skiko(+skiko-mingwx64) →
  com.bitsycore.skiko (ui/ui-graphics/ui-text + the DLL provisioner); settings
  repo filters updated. The OFFICIAL skiko (macOS/Linux libs.skiko) and the
  skiko-awt-runtime icudtl source stay org.jetbrains; the fork keeps upstream's
  org.jetbrains.skiko.* PACKAGE names — only the Maven coord is rebranded.
- Docs (CLAUDE/README/TOOLING/RENDERER/bridge README) + stale :window comments
  swept; API dump unique-name headers patched to the new groups + the window dump
  renamed (API is pre-stable; dumps regen once the build can run).

BLOCKED: mingwX64 build + publish need the bitsycore/skiko fork repo to
republish under com.bitsycore.skiko first. Verified locally: bridge plugin
compiles, main build configures, project tree resolves to the new paths.
2026-07-25 23:29:44 +02:00
Bitsy 3f4b3a23be docs+cleanup: refresh module-layout docs and purge dead SDL-renderer references after the :sdl-core / :ui-graphics / :ui-text split
- Regenerate API dumps: ui.klib.api drops the ui.graphics.*/ui.text.* surface
  (now carried by the new ui-graphics/ui-text dumps).
- components-resources: fold the historically-named src/sdlRendererMain dir into
  src/nativeMain (rename *.sdl.kt -> *.native.kt); drop the extra srcDir; fix the
  SDL3_image/IMG_LoadSVG comments (decode is Skia via the :ui-graphics hook).
- Purge stale two-renderer / SDL3_ttf / SDL3_image / FreeType / -Prenderer=sdl3
  references from live comments (RenderBackendFactory, IconFont, IconFontIcon,
  WindowIcon, PlatformClipboard, MaterialSymbolsScreen, sdl3.def, build-all.py,
  make-app-icon.py) and CLAUDE.md rule 4.
- verify-mac.sh: drop the removed SDL-renderer leg from the header prose.
- Keep the accurate 'were removed / no longer built' historical notes.

Comment/doc + build-file-comment changes plus the components-resources dir
rename; no executable logic changed. Verified: :components-resources compile +
metadata gate + :demo/:apidemo mingw build all green.
2026-07-25 22:57:23 +02:00
Bitsy d058f1b6a2 refactor(cleanup): delete dead Project* render cluster + fix bridge for the ui split
- Delete the ~920-line dead cluster superseded by the vendored SkiaBacked* actuals:
  ProjectCanvas/Paint/Path/PathIterator/ImageBitmap, Offscreen (OffscreenRenderer),
  SkiaDrawScope — a self-referential island with zero live references (RENDERER.md
  B6.2's deferred deletion, finally done).
- Full-commonization bridge (root build.gradle + the consumer bridge plugin): the
  official ui-graphics / ui-text coords now substitute to the NEW :ui-graphics /
  :ui-text modules (were still routed to :ui). Added :sdl-core / :ui-graphics /
  :ui-text to kPublishedLibs so they publish + the consumer plugin can map them.

Verified: demo + apidemo compileCommonMainKotlinMetadata + demo link green.
2026-07-25 22:32:33 +02:00
Bitsy 924ebe9776 refactor(ui): split :ui-text out of :ui — completes the upstream ui layout
androidx.compose.ui.text.* + the B6.3 skiko text engine (SkiaParagraph / SkiaParagraphOps
/ SkiaFonts, IconFont, NamedFont, ParagraphFactories, Locale/font actuals) now live in
:ui-text — deps :ui-graphics + skiko, SDL-free. :ui -> :ui-text (api). :ui is now the
Compose core + renderer on top of :ui-graphics / :ui-text / :sdl-core, mirroring upstream.

- compose-fork.txt: the ui-text SET_FOLDER section moved to :ui-text's own manifest;
  re-synced (95 vendored files).
- SystemLocale (SDL) / GenericFonts (resource glue) / ImeBridge stay in :ui;
  Locale/PlatformLocale (text.intl actuals) read the OS locale via a new
  preferredLocaleTagsProvider seam that SystemLocale installs.
- Made cross-module decls public (NoOpPlatformTextInputService); :ui-text needs
  runtime-saveable (Savers).

Verified mingw compile + compileCommonMainKotlinMetadata + demo render (Material Symbols:
variable-font axes + text correct).
2026-07-25 22:10:33 +02:00
Bitsy 5dc4b21867 refactor(ui): split :ui-graphics out of :ui (upstream layout)
androidx.compose.ui.graphics.* + the Skia actuals (SkiaBackedCanvas, SkiaImageCache,
GraphicsLayer/Context, painter/image seams) now live in :ui-graphics — deps skiko +
:ui-geometry/unit/util, SDL-free (image bytes via the composeResourceReader seam).
:ui -> :ui-graphics (api). Mirrors upstream's ui/ui-graphics split: each module is
self-contained (graphics expects + skiko actuals same module), so no cross-module
expect/actual and compileCommonMainKotlinMetadata stays green.

- compose-fork.txt split: the ui-graphics SET_FOLDER section moved to :ui-graphics's
  own manifest; re-synced (99 vendored files).
- Broke the one ui-core coupling: drawNativePainter / SkiaImageCache no longer take
  ContentScale (upstream PainterModifier pre-scales the destination, so the painter
  draws fill-bounds) — Fit/Crop/FillBounds still render correctly.
- Made the cross-module decls public (SkiaImageCache / SkiaLeafDrawer / GraphicsContext /
  readBackBgra / encodedImageDecoder setter / prepareLayerTransformationMatrix).

Verified mingw compile + metadata + demo render.
2026-07-25 22:01:56 +02:00
Bitsy accd74c9ee refactor(sdl): move the sdl3 cinterop into :sdl-core (Phase 1.2a)
New :sdl-core module (sdl/sdl-core) owns the single sdl3 cinterop; :ui exposes it
via api(project(":sdl-core")). Proves cross-module cinterop works: :ui sees
sdl3.*, and SDL3's static lib + linker opts propagate through the klib chain to
the final exe (SDL auto-links just by depending on :ui, unchanged). :sdl-core has
no :ui dependency, so :ui -> :sdl-core introduces no cycle.

Next: move the SDL platform + Skia renderer from :ui into :sdl-core (flips to
:sdl-core -> :ui, makes :ui SDL-free), then split ui-graphics/ui-text. Verified
on mingw (Widgets: controls + text + input all work).
2026-07-25 21:00:14 +02:00
Bitsy 30cc39606a refactor(sdl): platform-env seam — decouple foundation + resources from cinterop (Phase 1.1)
DarkTheme (:foundation, isSystemInDarkTheme) and ResourceEnvironment
(:components-resources, locale/theme qualifiers) called the sdl3 cinterop
directly (SDL_GetSystemTheme / SDL_GetPreferredLocales). Route them through a new
com.compose.sdl.res.{systemThemeIsDarkProvider,preferredLocaleProvider} seam that
SDL3Backend.init() installs. Now only the SDL layer + :window import sdl3 — the
prerequisite for relocating the cinterop into :sdl-core without those modules
needing it. Verified on mingw.
2026-07-25 20:51:24 +02:00
Bitsy 099211b015 refactor(ui): resource-bytes seam — decouple font/image loading from SDL (Phase 0)
SkiaFonts (fonts) + SkiaImageCache (images) read bundled bytes through a new
com.compose.sdl.res.composeResourceReader seam instead of calling
loadComposeResourceBytes (which uses the sdl3 cinterop via SDL_GetBasePath)
directly. SkiaRenderBackend installs the SDL-backed impl at construction. This is
the enabler for splitting ui-graphics/ui-text out of :ui without an SDL cinterop
dependency. Also deletes the throwaway ParagraphSpike + --paraspike demo flag.
Verified on mingw (Images screen: fonts + PNG/JPG/SVG/Android-XML all render).
2026-07-25 20:40:52 +02:00
Bitsy 37a44ffb46 fix(ci): bundle skiko-windows-x64.dll + icudtl.dat in the mingwX64 zips
The release zip step packaged only <app>.exe + data.kres, so the Windows binary
shipped without the two runtime sidecars the bridge plugin provisions next to the
exe (the skiko fork DLL + Skia's ICU data). The mingw exe cannot run without them.
Include both for the exe target (fail loudly if a provision ever regresses);
macOS/Linux unaffected. (v0.3.1 mingw zips need this — patch manually or re-tag.)
2026-07-25 20:09:17 +02:00
Bitsy 045c734eef fix(apidemo): wrappedRowCount via Paragraph.lineCount (currentTextMeasurer removed)
The B6.3 cleanup deleted com.compose.sdl.text.currentTextMeasurer, but apidemo's
Compat.native.kt still used it for wrappedRowCount. Rebuild it on the standard
Paragraph factory (skiko lineCount) — same physical-pixel space (density 1,
fontSize=inFontPx.sp, wrap at inMaxWidthPx). Fixes the v0.3.0 'Build demo +
apidemo release binaries' step, which failed on all targets after the library
publish step had already succeeded.
2026-07-25 19:30:04 +02:00
Bitsy 83c1902b08 refactor(text): delete the hand-rolled text renderer/seam — one skiko path
Now that Compose text draws through the skiko paragraph engine, migrate the last
consumer (IconText / Material Symbols) onto the standard path and delete the
parallel hand-rolled machinery:

- IconText is now a BasicText: its icon font family + variable axes (FILL/wght/
  GRAD/opsz) thread through namedFontFamily(axes=...) -> FontFamily.projectFontVariations(),
  which SkiaFonts applies via Typeface.makeClone. Icons measure + draw through
  skiko skparagraph like any other text.
- SkiaFonts registers each resolved typeface in the TypefaceFontProvider under a
  unique alias and sets fontFamilies=[alias] + typeface (mirrors upstream FontCache) —
  otherwise icon private-use codepoints fall back to Noto Sans and render as tofu.
- Deleted: SkiaTextRenderer (637L), TextDrawModifier (NativeTextCanvas/TextDrawElement/
  TextDrawNode), ColorRun, TextMeasurer (interface + currentTextMeasurer + WrappedText);
  kept currentViewport* in Viewport.kt.
- Trimmed: RenderBackend.textMeasurer, SkiaLeafDrawer.textRenderer,
  SkiaBackedCanvas.drawNativeText, SkiaRenderBackend, ComposeWindow install.
  TextRendererCapabilities.supportsFontVariations set true (skiko always supports it).

Verified on mingwX64: Buttons (text + FAB icon), Material Symbols (wght/FILL/GRAD
sweeps render correctly, no tofu). macOS/Linux + parity still pending.
2026-07-25 18:44:03 +02:00
Bitsy 076d60460c feat(text): render Compose text through skiko skparagraph (B6.3)
Replaces the hand-rolled SdlParagraph measure+draw with skiko's real
org.jetbrains.skia.paragraph engine (HarfBuzz shaping + skunicode bidi +
FontCollection fallback), keeping the port's family/variable-axis font model so
Material Symbols / monospace / custom fonts resolve unchanged.

Paragraph is a sealed interface whose actual is in nativeMain, so implementers
must live there (skikoRendererMain counts as a different module for the sealed
check). The impl therefore splits: SkiaParagraph (nativeMain, skiko-free — the
30 interface methods over plain TextBoxData/LineMetricData) drives a skiko
SkiaParagraphOps (skikoRendererMain) through the NativeParagraphOps seam. Font
resolution (SkiaFonts) + Compose-style->skiko conversion is a reduced local port
of upstream ParagraphBuilder.skiko.kt; cursor/selection/line-metric math is
adapted from upstream SkiaParagraph.skiko.kt.

Verified rendering on mingwX64 (Buttons screen: headers/body/labels/wrapping +
Material Symbol icon all correct). SkiaTextRenderer still backs
currentTextMeasurer for IconText/TextDrawModifier (follow-up). macOS/Linux +
parity verification pending (branch only).
2026-07-25 18:25:05 +02:00
Bitsy 93b7ff28e1 feat(bridge): provision icudtl.dat next to the mingwX64 exe
Skia's skparagraph (used by the upcoming upstream-text migration) loads ICU data
from icudtl.dat next to the binary on Windows at runtime; without it
ParagraphBuilder fatal-aborts. Sourced from the official
skiko-awt-runtime-windows-x64 jar (non-transitive) — same Skia base as the fork,
so no fork re-publish. Extracted + staged by provisionSkikoIcu{Debug,Release}MingwX64,
wired like the DLL provisioning (finalizedBy link, dependsOn run).
2026-07-25 17:56:55 +02:00
Bitsy 08968fe820 spike(text): prove upstream skiko Paragraph on the mingw fork (B6.3)
Isolated --paraspike flag drives org.jetbrains.skia.paragraph.* (ParagraphBuilder
/ FontCollection / TypefaceFontProvider + HarfBuzz shaping) end-to-end, without
touching the port's SdlParagraph engine. Renders 'Hello 123 Åé — wğ' from the
bundled data.kres NotoSans (registered as family 'Noto Sans' — the alias
upstream's FontCache uses), proving both skparagraph AND the font-bridge half
work on the fork DLL.

Finding: Windows needs icudtl.dat (~10.5MB) next to the exe (Skia loads ICU data
at runtime there); without it ParagraphBuilder fatal-aborts. To be provisioned
like skiko-windows-x64.dll. The spike function is present on all native targets,
so it also serves as the macOS/Linux skparagraph check (run with --paraspike).
2026-07-25 17:04:16 +02:00
Bitsy 3a57e96c80 chore(demo): prune the SDL-vs-Skia fidelity probes
With one native renderer, correctness is verified by parity against JVM Compose
Desktop (scripts/parity), so the per-feature SDL-leg fidelity probes are dead
weight. Removes the --dashtest / --tilemodetest / --pointstest / --rotimgtest /
--blendtest / --jointest / --filtertest / --ovaltest / --arctest demo flags,
their functions, and the now-unused twoColorBmp helper (-404 lines). Kept the
behavioural / platform / text probes (click / nav3 / locale / ime / font /
imagebytes / paragraph / metrics / ...).
2026-07-25 16:41:01 +02:00
Bitsy 332ba757fe chore(0.3.0): finish the SDL-renderer removal — docs, scripts, dead code
Completes the Skia-only cleanup for 0.3.0.

- Docs (CLAUDE.md, RENDERER.md, TOOLING.md, TODO.md, README.md): de-dualized
  everything — one Skia renderer (macOS/Linux official Skiko, mingwX64 the
  bitsycore fork). Removed -Prenderer=sdl3 / SDL-leg / SDL3_ttf-image-FreeType /
  two-leg-parity framing. RENDERER.md's "Track A" (real Skia on Windows) is now
  Done, not shelved; TODO's section H (SDL renderer graphics) is deleted.
- scripts/build-sdl: build-all.py builds ONLY SDL3, and its now-dead FreeType /
  SDL3_image / SDL3_ttf builder functions are deleted; build-sdl.properties
  trimmed to SDL3.
- scripts/parity: drop the --renderer=sdl3 / -Prenderer path — parity is
  native-Skia vs JVM now (baseline key is per-target).
- scripts/compose-fork/sync.py: drop the dead sdlMain -> sdlRenderer vendor
  mapping; components-resources manifest comments updated.
- demo ImagesScreen: caption now says Skia's codecs (not "SDL3_image on Windows").

Version 0.3.0 is git-tag-driven (tag v0.3.0 to publish).
2026-07-25 16:32:17 +02:00
Bitsy 09fe92fc95 docs(renderer): note the SDL renderer leg was removed (Skia-only) 2026-07-25 16:15:44 +02:00
Bitsy ec2451aad1 refactor(renderer): remove the SDL renderer leg — Skia everywhere
On this branch every target renders through Skia (mingwX64 via the bitsycore
skiko fork), so the from-scratch SDL renderer (Sdl3Canvas / Sdl3TextRenderer /
FreeTypeIcons / the SDL render nodes, ~3800 lines) and its SDL3_ttf / SDL3_image
/ FreeType cinterops are dead weight. SDL3 itself — windowing / input /
platform — stays, as the sole `sdl3` cinterop.

- Delete compose/ui/ui/src/sdlRendererMain (31 files) + the sdl3_ttf / sdl3_image
  / freetype .def files.
- :ui build: a single sdl3 cinterop; all native targets render through Skia
  (macOS/Linux official Skiko, mingwX64 the fork). Drops -Prenderer /
  -PwindowsSkia and the vSdlRenderer / isSkiaTarget split.
- GpuMode: drop the Sdl3.* modes (Auto / Software / Skia.OpenGL / Skia.Metal
  remain). SDL3Backend keeps the Software (SDL_Renderer), GL and Metal paths.
- :components-resources: its Font/Image actuals are renderer-agnostic project
  code (delegate to :ui) — attach them straight to nativeMain (no skiko dep, no
  renderer source-set split).
- bridge plugin: skiko DLL provisioning is now unconditional (mingw is always
  Skia).
- build-all.py: builds only SDL3 now (the ttf/image/freetype builders are kept
  but unused).

Verified: :ui + :components-resources compile and the demo links + renders on
mingwX64 (GL default).
2026-07-25 16:14:27 +02:00
Bitsy ff32d68a72 refactor(windows): move skiko DLL provisioning into the bridge plugin
The Windows Skia leg's runtime skiko-windows-x64.dll is now provisioned by the
bridge plugin (installWindowsSkiaDll) for every consumer under -PwindowsSkia,
instead of per-app build logic. Kotlin/Native has no runtime native-lib loader
(unlike the JVM's skiko-awt), so the DLL must sit next to the .exe; the plugin
resolves it from the fork's published windows-x64/.dll artifact and copies it
(finalizes link, precedes run). demo drops its inline block; apidemo gets it
automatically too.
2026-07-25 15:56:10 +02:00
Bitsy 1a56b7dcaf feat(windows): consume skiko fork from GitHub Packages + auto-provision DLL
Removes the mavenLocal-only requirement: the mingwX64 skiko klib + runtime DLL
are now published by the fork's CI (bitsycore/skiko) to GitHub Packages, and
this project resolves them from there.

- settings.gradle.kts: add the bitsycore/skiko GitHub Packages repo (scoped to
  org.jetbrains.skiko); the existing repo now excludes that group. mavenLocal
  still takes precedence for local fork builds. (Consuming GitHub Packages
  needs a token with read:packages; it works automatically in CI.)
- :ui: the fork version is overridable via -PskikoMingwVersion (default
  0.150.1-mingw.1, matching the fork CI's published version).
- demo: auto-provision skiko-windows-x64.dll next to the mingwX64 executable
  from the published windows-x64 / .dll artifact (finalizes the link task,
  precedes run) -- no manual copy needed.
2026-07-25 15:47:22 +02:00
Bitsy 7c5abc2705 feat(windows): real Skia renderer on Kotlin/Native mingwX64 (opt-in)
Adds a Skia rendering path for the Windows (mingwX64) target, behind
-PwindowsSkia=true. The default Windows build stays on the SDL renderer.

Backed by a fork of JetBrains/skiko that adds a mingwX64 native target
(Route 1a): skiko + Skia are linked into skiko-windows-x64.dll exposing a
flat extern-C surface, bound from Kotlin/Native via a GNU import library, so
the MSVC<->GNU C++ ABI wall is sidestepped. The fork is consumed as
org.jetbrains.skiko:skiko:0.0.0-SNAPSHOT from mavenLocal -- build & publish
it first (see SKIKO-MINGW-FEASIBILITY.md).

- settings.gradle.kts: mavenLocal() for the fork klibs.
- :ui build: isSkiaTarget(mingwX64)=useWindowsSkia; vSdlRenderer=!isSkiaTarget;
  a separate skikoRendererMingwMain tree on the fork ROOT coord (the platform
  artifact alone does not expose api-elements); SDL renderer skipped for mingw
  when windowsSkia.
- skikoRendererMingwMain/PlatformGpu.mingw.kt: makeMetalBridge=null.
- PlatformGpu.kt: Windows default GPU = Skia.OpenGL (hardware-accelerated via
  the SDL WGL context, reusing the shared SkiaGLBridge); --gpu=software stays.
- ComposeWindow: makeBackend() + one-shot fallback GPU -> CPU raster when a
  Skia GPU context/bridge can't be created (RDP / headless / bad drivers).
  Universal -- also covers a Metal failure on macOS.

Verified on Windows: the demo renders correctly on both the GL GPU path and
CPU raster across Buttons / Brushes / Images / GraphicsLayer / Widgets /
Shadows / LazyColumn / Icons (text, gradients, image+SVG+vector decode, and
graphics-layer transforms).
2026-07-25 14:33:50 +02:00
177 changed files with 7594 additions and 17195 deletions
+43 -20
View File
@@ -103,16 +103,13 @@ jobs:
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
# link time via --defsym in sdl3.def.
# Build the static native deps from scripts/build-sdl/build-sdl.properties.
# Only Windows ships the SDL3 renderer (no Skiko on mingw), so only it needs
# SDL3_ttf / SDL3_image / FreeType (SDL3_ttf text, SDL3_image decode, FreeType
# icon-axis rasterisation). macOS / Linux publish the Skia renderer — Skiko
# handles text / images / icons — so they only need base SDL3 for the main
# loop (windowing / input / audio / filesystem). Matches the cinterop guard
# in compose/ui/ui/build.gradle.kts, which skips those three cinterops on the
# Skia targets. Cached against the manifest + scripts so version bumps
# invalidate cleanly; bump the `v3` key when the toolchain (runner OS,
# compiler flavour, K/N sysroot) changes without a scripts/build-sdl/ edit.
# Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
# target renders through Skia now, so all platforms need only base SDL3 for
# the main loop (windowing / input / audio / filesystem) — the from-scratch
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
# Cached against the manifest + scripts so version bumps invalidate cleanly;
# bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N
# sysroot) changes without a scripts/build-sdl/ edit.
- name: Cache libs/
id: cache-libs
uses: actions/cache@v4
@@ -120,16 +117,10 @@ jobs:
path: libs
key: sdl-libs-v3-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
# Windows: full SDL3 renderer stack. macOS / Linux (Skia): base SDL3 only.
- name: Build static native deps
if: steps.cache-libs.outputs.cache-hit != 'true'
shell: bash
run: |
if [ "${{ matrix.host }}" = "windows" ]; then
python3 scripts/build-sdl/build-all.py
else
python3 scripts/build-sdl/build-all.py sdl3
fi
run: python3 scripts/build-sdl/build-all.py
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
# androidx.compose.* files. Sync copies them in from the upstream refs
@@ -160,16 +151,35 @@ jobs:
VER="${PUBLISH_VERSION#v}"
OWNER="${GITHUB_REPOSITORY%%/*}"
# Keep in sync with the module list in settings.gradle.kts.
MODULES="ui ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple window material-symbols navigation3-ui components-resources"
MODULES="sdl-core ui ui-graphics ui-text ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple desktop-native-window material-symbols navigation3-ui components-resources"
case "${{ matrix.host }}" in
macos) SUFFIXES="-macosarm64" ;;
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
esac
# Published group per module — mirrors the per-area com.bitsycore groups
# in root build.gradle.kts. Project-only modules (sdl-core, material-symbols)
# fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven
# package name is "<group>.<artifact>".
group_of() {
case "$1" in
ui|ui-graphics|ui-text|ui-util|ui-geometry|ui-unit|ui-backhandler|ui-tooling-preview)
echo "com.bitsycore.compose.ui" ;;
foundation|foundation-layout) echo "com.bitsycore.compose.foundation" ;;
animation|animation-core|animation-graphics) echo "com.bitsycore.compose.animation" ;;
material3) echo "com.bitsycore.compose.material3" ;;
material-ripple) echo "com.bitsycore.compose.material" ;;
components-resources) echo "com.bitsycore.compose.components" ;;
navigation3-ui) echo "com.bitsycore.navigation3" ;;
desktop-native-window) echo "com.bitsycore.compose" ;;
*) echo "com.bitsycore.compose.sdl" ;;
esac
}
host_packages() {
for m in $MODULES; do
g=$(group_of "$m")
for s in $SUFFIXES; do
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
if [ "$s" = "_ROOT_" ]; then echo "$g.$m"; else echo "$g.$m$s"; fi
done
done
if [ "${{ matrix.host }}" = "windows" ]; then
@@ -242,8 +252,21 @@ jobs:
for app in demo apidemo; do
src="$app/build/bin/$target_dir/releaseExecutable"
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres")
# Windows Skia leg ships two runtime sidecars next to the exe — the
# skiko fork DLL and Skia's ICU data (icudtl.dat) — provisioned by the
# bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run
# without them, so include them (and fail loudly if a provision regressed).
if [ "${{ matrix.exe_ext }}" = "exe" ]; then
for sidecar in skiko-windows-x64.dll icudtl.dat; do
if [ ! -f "$src/$sidecar" ]; then
echo "::error::Missing Windows runtime sidecar: $src/$sidecar"; exit 1
fi
files+=("$src/$sidecar")
done
fi
python3 -c 'import sys,os,zipfile; z=zipfile.ZipFile(sys.argv[1],"w",zipfile.ZIP_DEFLATED,compresslevel=9); [z.write(f,os.path.basename(f)) for f in sys.argv[2:]]; z.close()' \
"$zip_path" "$src/$app.${{ matrix.exe_ext }}" "$src/data.kres"
"$zip_path" "${files[@]}"
done
ls -la dist/
+117 -112
View File
@@ -24,11 +24,16 @@ context — read it first, then look at the files it points to.
on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux
(x64/arm64), Windows (mingwX64).
Rendering is pluggable behind one `RenderBackend`:
Rendering is **Skia everywhere** behind one `RenderBackend` — Metal / OpenGL
/ CPU raster:
- **Skia** (via Skiko klibs) on macOS + Linux — Metal / OpenGL / CPU raster.
- **SDL3** (`SDL3_ttf` + `SDL_RenderGeometry`) on Windows, and on macOS/Linux
when `-Prenderer=sdl3` is passed.
- **macOS + Linux** link the OFFICIAL Skiko klibs from Maven.
- **Windows (mingwX64)** links the bitsycore skiko FORK — skiko+Skia compiled
into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via
an embedded GNU import lib, published to GitHub Packages as
`com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override with
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
the bridge plugin (`installWindowsSkiaDll`). See SKIKO-MINGW-FEASIBILITY.md.
Windowing, input, audio, filesystem access, and the OS-integration surface
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
@@ -41,8 +46,11 @@ re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
## Module layout
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
Only `:window` is the SDL integration layer ("Compose SDL" — SDL3 main loop)
and lives under `compose/sdl/`.
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
primitives — zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and
`:desktop-native-window` (the SDL3 main-loop shell + app entry point) at
`compose/desktop/native/window/`. `:ui` depends on `:sdl-core` and its renderer
+ SDL↔Compose bridges pick the cinterop from it.
One Gradle module per upstream artifact; the directory mirrors the upstream
`compose/` path, the gradle path is kept short (redirected via `projectDir`).
@@ -50,10 +58,18 @@ One Gradle module per upstream artifact; the directory mirrors the upstream
```
compose/
├── ui/
│ ├── ui/ → :ui — androidx.compose.ui.* (ui + ui-graphics + ui-text) +
│ │ com.compose.sdl.* — cinterops + BOTH renderer pipelines live
│ │ here. ui-graphics/ui-text can't split off: their Canvas /
│ │ Paragraph `expect`s are the renderers' `actual`s (same-module).
│ ├── ui/ → :ui — androidx.compose.ui.* CORE (Modifier, LayoutNode,
│ │ composition, semantics, input, focus) + com.compose.sdl.* —
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
│ │ bridges (events / clipboard / cursors / window). Depends on
│ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text +
│ │ the sdl3 cinterop were split OUT — upstream layout.)
│ ├── ui-graphics/ → :ui-graphics — androidx.compose.ui.graphics.* + the Skia actuals
│ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache,
│ │ painter/image + resource seams). → skiko; SDL-free.
│ ├── ui-text/ → :ui-text — androidx.compose.ui.text.* + the skiko text engine
│ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont,
│ │ NamedFont). → :ui-graphics, skiko; SDL-free.
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
@@ -74,16 +90,21 @@ compose/
│ └── material3/ → :material3 — androidx.compose.material3.*
├── material/
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.*
└── sdl/ ("Compose SDL" — project modules, not upstream CMP artifacts)
└── window/ → :window — nativeComposeApp { Window(...) {} } multi-window
shell + SDL3 main loop; nativeComposeWindow() wrapper
└── desktop/native/window/ → :desktop-native-window — nativeComposeApp { Window(...) {} }
multi-window shell + SDL3 main loop; nativeComposeWindow()
wrapper (project app-shell, not an upstream CMP artifact).
Published as com.bitsycore.compose:desktop-native-window.
sdl/
└── sdl-core/ → :sdl-core — the NAKED sdl3 cinterop + platform primitives
(zero Compose dep, like skiko); the single `sdl3` cinterop
lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core.
utils/
└── material-symbols/ → :material-symbols — codepoints + all three style objects
(Outlined / Rounded / Sharp). COMMON API (usable from
shared app code) + per-stack actuals: native renders
via :foundation IconFontIcon (IconFont handles the
SDL3/Skia split), jvm() via Skiko directly
via :foundation IconFontIcon (Skia), jvm() via Skiko directly
(Typeface.makeClone per axes — upstream's FontCache
drops variationSettings from its key). Its commonMain
declares official Maven compose coords — the root
@@ -106,8 +127,8 @@ components/
no mingwX64/linux klibs. Platform layer is project
code: data.kres ResourceReader, pure-Kotlin
DomXmlParser (upstream's is Darwin NSXMLParser),
SDL3_image decode via the :ui EncodedImageDecoder
hook, NamedFont registration, SDL locale/theme env.
image decode via the :ui EncodedImageDecoder hook
(Skia), NamedFont registration, SDL locale/theme env.
Apps' JVM targets keep the Maven artifact — the
generated Res accessors work against BOTH.
@@ -173,11 +194,11 @@ scripts/ → vendor-sync + python helper scripts (compose-coverage =
coverage/fidelity vs upstream, material-symbols generate/subset)
+ compose-fork/;
scripts/build-sdl/ = static-lib build script (python)
libs/ → gitignored per-host static SDL3 / SDL3_ttf / SDL3_image / FreeType
output of scripts/build-sdl/build-all.py on Windows
libs/ → gitignored per-host static SDL3 output of
scripts/build-sdl/build-all.py on Windows
```
Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) —
Module PATHS stay short (`:ui`, `:foundation`, `:desktop-native-window`, …) —
`settings.gradle.kts` redirects `projectDir` for each so build files across
the repo stay terse. `androidx.collection` is a plain Maven dependency
(`androidx.collection:collection`), not a module — same as other simple
@@ -189,23 +210,27 @@ androidx KMP libs.
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
:material-ripple ←────────────┘──────────────┘
:foundation, :animation-core ← :window ; :foundation, :material3 ← :material-symbols
:foundation, :animation-core ← :desktop-native-window ; :foundation, :material3 ← :material-symbols
```
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
`:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`;
`:ui → :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
`:sdl-core → sdl3 cinterop (NAKED — no Compose)`;
`:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`;
`:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`;
`:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
`:foundation-layout → :ui`; `:animation → :animation-core, :foundation-layout`;
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
`:material-ripple → :foundation, :animation-core`;
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
`:ui` is the renderer/cinterop hub (the ui-graphics + ui-text `expect`s resolve to
its SDL/Skia renderer `actual`s). The pure lower artifacts (`:ui-util`,
`:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are split out below it. Everything
above `:ui` can only touch renderer / cinterop internals via its public surface.
`:window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
`actual`s, SDL-free) and the naked `:sdl-core` (sdl3 cinterop). The pure lower
artifacts (`:ui-util`, `:ui-geometry`, `:ui-unit`, `:ui-backhandler`) are below.
Everything above `:ui` touches renderer internals only via its public surface.
`:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
install the popup / scaffold layer at the composition root).
## Vendoring philosophy — read this before writing any `androidx.compose.*` code
@@ -222,7 +247,7 @@ compose-multiplatform umbrella repo that `:components-resources` vendors from).
There is no implicit default — `SET_REPO` is required. `scripts/compose-fork/sync.sh`
walks all manifests and copies each selected file byte-for-byte from the pinned
checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into
`<module>/src/vendor/{common,native,skikoRenderer,sdlRenderer}/kotlin/`. The
`<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The
`src/vendor/` tree is **gitignored** — you don't check it in, you re-sync
on demand.
@@ -247,8 +272,8 @@ Two categories of code live in each module:
Never hand-edit these. If upstream diverges, adjust the manifest or the
pinned ref and re-sync. This is the bulk of the codebase (~1500 files
across the modules).
2. **Project code (in `src/commonMain/`, `src/nativeMain/`, `src/skikoRendererMain/`,
`src/sdlRendererMain/`)** — code we author (project actuals, glue between
2. **Project code (in `src/commonMain/`, `src/nativeMain/`,
`src/skikoRendererMain/`)** — code we author (project actuals, glue between
Compose and SDL3, project-specific extensions).
### The 5 rules for adding upstream Compose surface
@@ -288,14 +313,13 @@ Two categories of code live in each module:
reconciled by hand. This is fine; do it when the edit is small and
the file is unlikely to churn upstream.
4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`, with
an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream
ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain`.
Then hand-roll (or minimally vendor) the SDL3 counterpart in
`sdlRendererMain` — `SkiaCanvas.kt` ↔ `Sdl3Canvas.kt`,
`SkiaTextRenderer.kt` ↔ `Sdl3TextRenderer.kt` (using SDL3_ttf +
FreeType), `SkiaImageCache.kt` ↔ `Sdl3ImageCache.kt` (using SDL3_image).
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` — graphics
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into
`:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set;
mingwX64 layers its fork actuals on top under `skikoRendererMingwMain`.
5. **Multi-OS project code goes through SDL3, not hand-rolled per-target
ifdefs.** SDL3 already handles the platform differences for filesystem
@@ -313,52 +337,41 @@ call sites don't care.
## Source-set hierarchy (:ui only)
`:ui` owns cinterops + both renderer pipelines. Its source-set tree:
`:ui` owns the Skia RenderBackend + GPU bridges. The SAME source-set tree is used
by `:ui-graphics` / `:ui-text` for their skiko `actual`s (only the deps differ);
the `sdl3` cinterop lives in `:sdl-core` (see below). `:ui`'s tree:
```
commonMain
└── nativeMain (vendored .native.kt + project native code)
├── skikoRendererMain (Skia drawing pipeline; Skiko on classpath)
├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath)
│ ├── skikoRendererMacosMain (macOS-only Skia actuals — Metal bridge)
│ └── skikoRendererLinuxMain (Linux-only Skia actuals — OpenGL)
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main
│ ONLY when the Skia renderer is active for the target.
└── sdlRendererMain (SDL3 drawing pipeline + TTF/image/FreeType)
├── sdlRendererMacosMain (macOS-only SDL3 driver hint)
├── sdlRendererLinuxMain (Linux-only SDL3 driver hint)
└── sdlRendererMingwMain (mingwX64-only SDL3 driver hint)
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main.
└── skikoRendererMingwSharedMain
└── skikoRendererMingwMain (mingwX64-only Skia actuals — the bitsycore
skiko FORK: skiko-windows-x64.dll bound via
an embedded GNU import lib; OpenGL context)
attached to: mingwX64Main.
```
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are `expect`s in
`:ui`'s nativeMain with `actual`s in BOTH `skikoRendererMain` and
`sdlRendererMain` — unambiguous because **only one of the two renderer source
sets is attached to a given target**. (They used to be plain duplicate
declarations with no expect; that compiled per-target but shared nativeMain
METADATA couldn't see them on a host whose targets span both renderers, which
blocked the WINDOWS host from producing :window's KotlinMultiplatform
publication — and Windows must publish the root modules, see the publish
workflow.) Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
**not even created**, so Gradle has nothing to warn about and Skiko is never
pulled in.
`:ui`'s nativeMain with `actual`s in `skikoRendererMain` (shared by every native
target). `rendererPreferredGpuMode()` picks Metal on macOS, OpenGL on
Linux + Windows, with a Software (CPU raster) auto-fallback if the GPU context
fails to come up.
### Cinterop sibling-dependency gotcha
### Cinterop
`:ui` owns four cinterops in `src/nativeInterop/cinterop/`:
`sdl3`, `sdl3_ttf`, `sdl3_image`, `freetype`. The `.def` files for `sdl3_ttf` /
`sdl3_image` carry `depends = sdl3` so their `SDL_Surface` / `SDL_Color`
references resolve to the *same* types `sdl3` produces (not duplicates inside
`sdl3_image.SDL_Surface`). **Gradle does not automatically add a sibling
cinterop's klib to a cinterop task's `-library` list**, so the manifest
directive silently fails and you get cryptic
`expected CPointer<sdl3.SDL_Surface>?, actual CPointer<sdl3_image.SDL_Surface>?`
errors.
`:ui/build.gradle.kts` works around this by passing the sdl3 cinterop output
klib path explicitly via `extraOpts("-library", vSdl3Klib)` on `sdl3_ttf` /
`sdl3_image`, plus a task dependency
(`cinteropSdl3_ttf*Target.dependsOn(cinteropSdl3*Target)`). If you ever add
another `depends = sdl3` cinterop, add it to that list too.
The `sdl3` cinterop lives in the NAKED `:sdl-core` module
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
bridges — and `:desktop-native-window` downstream — see `sdl3.*` and inherit SDL3's static-lib +
linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the
per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps
link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose
dependency, so `:ui → :sdl-core` is cycle-free — the compose way (like `ui → skiko`).
## Density flow (Option B — layout in physical pixels)
@@ -392,13 +405,13 @@ Passing raw `px.dp` will double-scale on Retina.
./gradlew :demo:runDebugExecutableLinuxX64
./gradlew :apidemo:runDebugExecutableLinuxX64
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop)
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop).
# Links the bitsycore skiko FORK from GitHub Packages (com.bitsycore.skiko:skiko:
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
gradlew.bat :demo:runDebugExecutableMingwX64
gradlew.bat :apidemo:runDebugExecutableMingwX64
# Skiko-free build on macOS/Linux — SDL3 renderer everywhere
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
# screens on upstream Compose; differences vs the native build = port bugs.
./gradlew :demo:run
@@ -407,12 +420,16 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
### System dependencies
SDL3 + SDL3_ttf + SDL3_image + FreeType (+ image codecs) are **built from
source as static libraries on every OS** and linked straight into the
executable — no brew/apt SDL packages, no runtime .dll/.so/.dylib; a
distributable is `<app>` + `data.kres`. Build them once per host with
`python scripts/build-sdl/build-all.py`. Per-host toolchain requirements and
the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
SDL3 is **built from source as a static library on every OS** and linked
straight into the executable — no brew/apt SDL packages. Build it once per host
with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements
and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux
(no runtime .so/.dylib — Skia is statically inside the klib), and the bitsycore
fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next
to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows
one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`.
## Runtime bundling — data.kres
@@ -456,15 +473,15 @@ drift / vendor-clean guardrails are in
## Key files by area — start here when you need to find something
### Renderer + main loop
- `compose/sdl/window/src/nativeMain/…/ComposeWindow.kt` — main loop,
- `compose/desktop/native/window/src/nativeMain/…/ComposeWindow.kt` — main loop,
recomposer lifecycle, SDL event dispatch, composition-local seeding.
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface.
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed renderer / driver picker.
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/…/renderer/sdl/Sdl3RenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/…/renderer/sdl/FreeTypeIcons.kt` —
variable-font axis rasterisation (SDL3_ttf has no axis-set API; we go
to FreeType directly for icon families).
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` —
the Skia render backend (shared by every native target).
- `compose/ui/ui/src/skikoRendererMingwMain/…` — mingwX64-only Skia actuals
bound against the skiko fork's flat extern-C surface.
### Layout / composition wiring
- `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` — root LayoutNode
@@ -474,15 +491,15 @@ drift / vendor-clean guardrails are in
alpha bridge).
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
### Text
- `compose/ui/ui/src/nativeMain/…/ui/text/SdlParagraph.native.kt` — the
bridged `Paragraph` implementation (measurement, hit-test, line metrics,
span painting).
- `compose/ui/ui/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
### Text (all in `:ui-text` now)
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` — the
`Paragraph` actual (skiko-free; the 30 interface methods over plain data),
driving the skiko engine through the `NativeParagraphOps` seam.
- `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt`
— `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts`
(data.kres bytes → skiko Typeface; the port's family/variable-axis model).
- `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
- `compose/ui/ui/src/commonMain/…/text/TextMeasurer.kt` — `NativeTextMeasurer`
interface (per-renderer implementations: `SkiaTextRenderer` /
`Sdl3TextRenderer`).
### Icons
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` —
@@ -516,7 +533,7 @@ profiler. Quick rules of thumb:
the broad net.
- Chasing one reported interaction → run the **probe** (`scripts/probe/`), targeted.
- Slow frame → **profiler** first (`CDN_PROFILE=1`), optimize second.
- Any renderer change → the **`verify-mac`** runbook gates both legs before commit.
- Any renderer change → the **`verify-mac`** runbook gates it before commit.
See [RENDERER.md](RENDERER.md) for the renderer work these support.
@@ -565,12 +582,6 @@ that should surface in tooling.
`Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`.
For a drag ghost that also translates, put `alpha` and `translationX` on
the SAME `graphicsLayer(...)` so clip stays false.
- **Cinterop `depends = sdl3`** — silently doesn't propagate; see the
gotcha section above.
- **Configuration cache + `-Prenderer=`** — Gradle caches configuration;
toggling the renderer property may not invalidate it. Delete
`.gradle/configuration-cache/` between switches if you see weird
"couldn't find sdl3_ttf" errors.
- **Substituted Maven modules hide their transitives from common metadata** —
when the bridge swaps an official coord for a project module on native
configs, KGP's granular-metadata visibility check drops that Maven module's
@@ -583,17 +594,11 @@ that should surface in tooling.
declares every target, so only its .module files carry the full variant
table; macOS-published roots left v0.1.15 without mingwX64 variants) —
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
- **`Path()` in commonMain returns different actuals per renderer** —
the Skia renderer produces a `SkiaBackedPath` (wraps
`org.jetbrains.skia.Path`), the SDL renderer produces a project
`ProjectPath` (command-list based). `SkiaCanvas.toSkiaPath` handles
both — never assume the type without checking.
## Useful Gradle tricks
- `--args="--gpu=sdl3.opengl --screen=Buttons"` — pass CLI to the demo.
- `--args="--gpu=skia.opengl --screen=Buttons"` — pass CLI to the demo.
- `--info` — see cinterop classpath + include paths actually used.
- `--rerun-tasks` — force rebuild after toggling `-Prenderer=`.
- After a module rename or IC-cache mismatch: nuke
`demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native
pins module IDs into its klib metadata; a stale cache surfaces as
+17 -19
View File
@@ -18,20 +18,21 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
`animation`, and `material3` are copied from Compose Multiplatform verbatim
wherever they compile as is, with project code filling in only the native glue.
- **Rendering is pluggable** behind one `RenderBackend`: Skia (via Skiko) on
macOS and Linux, and a from-scratch SDL3 renderer (`SDL3_ttf` +
`SDL_RenderGeometry`) on Windows, or anywhere with `-Prenderer=sdl3`.
- **Rendering is Skia everywhere.** macOS and Linux link the official Skiko
klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships
Skiko and Skia together in `skiko-windows-x64.dll`.
- **The platform is SDL3.** Windowing, input, audio, filesystem, file dialogs,
and clipboard all go through SDL3, so one code path covers every OS.
- **No runtime dependencies.** SDL3 and its codecs are built as static
libraries and linked in. A distributable is just the executable plus a
`data.kres` resource bundle.
- **Lean distributables.** SDL3 is built as a static library and linked in. On
macOS and Linux a distributable is just the executable plus a `data.kres`
resource bundle; on Windows it also ships `skiko-windows-x64.dll` next to the
exe (auto-provisioned by the bridge plugin).
| Platform | Gradle target | Default renderer |
|----------|---------------|------------------|
| macOS arm64 | `macosArm64` | Skia (Metal) |
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) |
| Windows | `mingwX64` | SDL3 |
| Platform | Gradle target | Renderer |
|----------|---------------|----------|
| macOS arm64 | `macosArm64` | Skia (Metal) — official Skiko |
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) — official Skiko |
| Windows | `mingwX64` | Skia — bitsycore Skiko fork |
## Quickstart
@@ -71,7 +72,7 @@ saved state behave as they do on Android.
```kotlin
commonMain.dependencies {
implementation(project(":window")) // window shell + main loop
implementation(project(":desktop-native-window")) // window shell + main loop
implementation(project(":material3")) // Material 3 widgets
implementation(project(":material-symbols")) // icon-font composables (optional)
}
@@ -122,7 +123,7 @@ dialogs, canvas, graphics layers, animation, and gestures.
```bash
./gradlew :demo:runDebugExecutableMacosArm64 # macOS (Skia / Metal)
./gradlew :demo:runDebugExecutableLinuxX64 # Linux (Skia / OpenGL)
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (SDL3)
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (Skia / Skiko fork)
./gradlew :demo:run # JVM Compose Desktop (reference)
```
@@ -138,14 +139,11 @@ client certificates.
./gradlew :apidemo:run # JVM Compose Desktop (reference)
```
Pass `-Prenderer=sdl3` on macOS or Linux to drop Skiko and use the pure SDL3
renderer everywhere.
## Modules
One Gradle module per upstream Compose artifact, mirroring the upstream
`compose/` tree. The renderer and cinterops live in `:ui`; `:window` is the SDL
integration layer. `:material-symbols`, `:components-resources`, and
`compose/` tree. The renderer lives in `:ui` and the `sdl3` cinterop in
`:sdl-core`; `:desktop-native-window` is the SDL integration layer. `:material-symbols`, `:components-resources`, and
`:navigation3-ui` are project or vendored modules that the official Maven
artifacts do not cover for Kotlin/Native desktop.
@@ -159,7 +157,7 @@ compatible artifacts are in [CLAUDE.md](CLAUDE.md).
Build the native libraries once per machine, then build any app target:
```bash
python3 scripts/build-sdl/build-all.py # SDL3, SDL3_ttf, SDL3_image, FreeType (static)
python3 scripts/build-sdl/build-all.py # SDL3 (static)
./gradlew :demo:runDebugExecutableMacosArm64
```
+65 -114
View File
@@ -8,37 +8,39 @@ actuals, or the layer engine.
For build and verification commands see [TOOLING.md](TOOLING.md). For module
layout, source-set hierarchy, and vendoring rules see [CLAUDE.md](CLAUDE.md).
## 1. The two renderer legs
## 1. The renderer
Rendering is pluggable behind one `RenderBackend` interface. Exactly one leg is
active per target:
> **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.
- **Skia leg** (`skikoRendererMain`, macOS + Linux). Draws through upstream
Compose Multiplatform's own Skia stack, vendored verbatim: `SkiaBackedCanvas`,
`SkiaBackedPaint`, `SkiaShader`, `actual class GraphicsLayer` backed by
`org.jetbrains.skiko.node.RenderNode`, `SkiaGraphicsContext`. On this leg the
layer and draw engine internals are literally upstream.
- **SDL leg** (`sdlRendererMain`, Windows always; macOS/Linux under
`-Prenderer=sdl3`). A from-scratch renderer on `SDL_RenderGeometry` +
`SDL3_ttf` + FreeType. SDL is a triangle blitter, so this leg carries a
permanent bespoke surface (about 3,800 lines): `Sdl3Canvas`,
`Sdl3TextRenderer`, `FreeTypeIcons`, and the SDL render nodes.
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).
Both legs share one project-owned text and image pipeline (`SdlParagraph`,
`NativeTextMeasurer`, `IconFont`). Text layout, line-breaking, metrics, and
hit-test are the port's engine on both legs; only glyph rasterization differs
(Skia vs SDL3_ttf/FreeType). This is a deliberate axiom, not a gap: see the
text decision in section 4.
The text pipeline is now upstream's own `SkiaParagraph` (skparagraph, vendored
verbatim); 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
`Common (upstream) -> shared native engine -> Skia actual`. What we
minimize is hand-rolled actual surface, not actual-side line count: a fat
vendored-upstream actual (the Skia leg carrying upstream's whole GraphicsLayer
stack) is preferred over a thin hand-written one.
## 2. The retained-layer engine (the model we copied)
Upstream skiko skips work at three levels. The port has all three.
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
@@ -58,8 +60,7 @@ dirty-region present; the entire win is not re-recording clean layers, not
drawing less screen. Dirty-region rendering is therefore an explicit non-goal.
The compositing-strategy contract (`requiresLayer()`, from
`SkiaGraphicsLayer.skiko.kt`) must match on both legs. It decides when a layer
needs an offscreen:
`SkiaGraphicsLayer.skiko.kt`) decides when a layer needs an offscreen:
| Condition | Auto | Offscreen | ModulateAlpha |
|---|---|---|---|
@@ -76,80 +77,46 @@ needs an offscreen:
vendored verbatim. The Skia leg gets real display-list caching and correct
clip/shadow/renderEffect for free from upstream. After this landed, Skia
`draw` on LazyColumn fell from 1.75 ms to 0.2 ms.
- **SDL leg ships the geo node as the Windows default.** `SdlDisplayListRenderNode`
records a leaf's layer-local tessellated geometry, text runs, and icon glyphs
by params (not texture pointers), then re-emits through the layer transform.
Crisp under any transform, bit-exact, deterministic (no render-target state),
and eviction-safe (replay re-looks-up via the run LRU / FreeType glyph cache).
0.000% across a 57-screen self-consistency sweep bar a sub-0.06% rotated-edge
AA fringe. LazyColumn steady-state `draw` fell 57% versus the un-cached
baseline. Fallbacks: `CDN_LAYERCACHE=off|texture`.
- **Parity is a golden-master on the Skia leg** (about 2% median, dominated by
the shared text engine's small metric delta) and a bounded, ranked signal on
the SDL leg. See TOOLING.md for how to read it.
- **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, not Windows pixel-parity or
feature completeness. The JVM Compose Desktop target (`:demo:run`) is the
documented Windows fidelity/feature tier: real Skia, full fidelity, on any
host. That makes SDL-leg fidelity work (B3) and real-Skia-on-Windows (Track A)
optional polish, not obligations.
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 (no Skiko on Windows K/N; SDL is a triangle
blitter). "It was easier" is not valid.
- **B2 convergence is complete.** The Skia leg was migrated off the port's
hand-rolled GraphicsLayer onto upstream's own. This was a source-set migration,
not a file-flip: the SDL node cluster relocated `nativeMain -> sdlRendererMain`,
`GraphicsContext` forked per-leg behind a `createGraphicsContext()` seam, and
the upstream skiko files were un-refused in `compose-fork.txt`. Measured
sync-tax reduction on the skiko path: about 8.5x (roughly 1240 hand-rolled
lines down to about 145 lines of drift-tracked edits; roughly 1006 lines now
auto-sync verbatim on a ref bump). The beta02 -> beta03 bump proved it: zero
reconciliation, all manual-vendor bases unchanged.
- **Text stays the port's engine (B6.3 skipped).** Making Skia-leg text truly
upstream (`SkiaParagraph`) is a font-subsystem replacement, not a canvas swap:
two coexisting font-identity models (the port's name-to-bytes `IconFont` +
no-op resolver vs upstream `PlatformFont`/`FontCache`/`FontCollection`), about
13 skiko files to re-vendor, about 10 shared actuals to split, a
`data.kres`-to-`FontCache` bridge, no green intermediate, and it discards the
P3.1 metrics work. ROI is low: the Skia leg already measures text with skiko
`Font` metrics (via `currentTextMeasurer` -> `SkiaTextRenderer`), which is how
parity reached about 2%. Revisit only if complex-script/bidi fidelity on the
native Skia leg becomes a hard requirement.
- **`GraphicsLayerOwnerLayer` stays shared.** The fork point is `GraphicsLayer` /
`GraphicsContext`, not the owner layer. The shared owner layer compiles
against both `GraphicsLayer` actuals (upstream skiko + port SDL) only if both
satisfy the commonMain `expect class GraphicsLayer` API. That API-parity
invariant is enforced by `:ui:compileCommonMainKotlinMetadata` on every build.
- **Lifetime model.** The SDL leg uses GC / release-queue for layer lifetime and
does NOT vendor `ChildLayerDependenciesTracker`. The Skia leg uses upstream's
`SkiaGraphicsLayer`, which inherently uses the tracker (already vendored in
commonMain). A per-leg lifetime divergence is the class of bug that caused an
early navigation crash, so any change here goes to both legs plus a soak gate.
- **Module split (`:ui-graphics` / `:ui-text`) is shelved as infeasible.** Not
cosmetic churn as originally scoped: the `sdl3` cinterop is a shared substrate
used by 11 graphics-side files AND 12 platform/windowing/node/resources files
that stay in `:ui`. Kotlin/Native cannot cleanly share a cinterop klib across a
module boundary, which is exactly why they were merged. A real split needs a
new non-upstream `:ui-cinterop` base module plus cross-module cinterop
api-exposure, which is bigger and more divergent than the "match upstream
artifacts" goal it was meant to serve. The one keeper from the attempt:
`RenderBackend.drawRoot` now takes `(Canvas) -> Unit`, decoupling the backends
from `ComposeRootHost` (a genuine improvement, retained).
- **Track A (real Skia on Windows K/N) is shelved.** Kotlin/Native mingwX64 is
GNU-ABI; skia-pack Windows is MSVC-ABI, which cannot be statically fused into a
GNU-ABI binary. Building current C++20 Skia to a GNU/mingw static archive is an
open-ended fork (only abandoned Mozilla-era precedent). The only working route
is a runtime DLL, which breaks the no-DLL invariant and adds tens of MB on the
one platform it targets. The SDL leg is the permanent Windows renderer; JVM is
the fidelity escape hatch.
Valid answers name a constraint (e.g. no windowing/input toolkit in K/N ->
SDL3). "It was easier" is not valid.
- **Text is now upstream's engine (B6.3 done).** Skia-leg text draws through
upstream's own `SkiaParagraph` (skparagraph), vendored verbatim — the
font-subsystem replacement landed: upstream `PlatformFont`/`FontCache`/
`FontCollection` back a `data.kres`-fed font supply, replacing the port's
name-to-bytes engine. glyph rasterization stays Skia. This subsumes the P3.1
metrics work (measurement is now upstream's, so the numbers match by
construction).
- **Module split (`:ui-graphics` / `:ui-text`) is done.** The blocker — the
`sdl3` cinterop being a shared substrate that Kotlin/Native can't cleanly
share across a module boundary — was resolved by extracting a non-upstream
`:sdl-core` base module that owns the cinterop and api-exposes it. With that
base in place, `androidx.compose.ui.graphics.*` / `.text.*` split off into
`:ui-graphics` / `:ui-text` (matching upstream artifacts), leaving `:ui` as
the Compose core + Skia renderer + SDL bridges. `RenderBackend.drawRoot` takes
`(Canvas) -> Unit`, decoupling the backends from `ComposeRootHost`.
- **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
@@ -159,24 +126,23 @@ needs an offscreen:
| 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) |
| B6.3: upstream text (`SkiaParagraph`) on the Skia leg | Done |
| B5: engine-convergence deltas audit | Done (clean wins spent by B6) |
| P2.2: composition memory leak | Fixed + soak-gated |
| P2.3: outsets / blur / renderEffect | Done via upstream `SkiaGraphicsLayer` |
| P3.1: SDL text metrics parity (17% median -> 2%) | Done |
| B3: further SDL fidelity | Capped (parity-ranked wins only) |
| Module split | Shelved (infeasible as specified) |
| Track A: real Skia on Windows | Shelved |
| P3.1: text metrics parity (17% median -> 2%) | Done |
| Module split (`:ui-graphics` / `:ui-text` via `:sdl-core`) | Done |
| 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 covers both renderers
but cannot cover the shipped mingwX64 binary, the Windows-only `PrintWindow`
probe, or the common-metadata publish job. Run these on a Windows host before
any release. This is the only outstanding verification.
- **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
@@ -192,17 +158,12 @@ remembering.
shadows already match JVM.
- D3: dedupe `LayerTransformationMatrix.kt` against the now-vendored
`Matrices.skiko`. Blocked on D2 (the shared owner-layer hit-test needs a
both-legs matrix fn).
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.
- **SDL performance (opportunistic).** Shared glyph atlas to replace per-run
textures; `drawImageRect` capture in the geo node (journal rates it low-ROI).
- **SDL_GPU backend (long-term).** Real stencil clipping, pipelined batching,
shader gradients. The `NativeRenderNode` seam makes a GPU node a clean
drop-in.
- **Native-resource lifecycle.** Wire `GraphicsLayer`/RenderNode +
`SdlImageBitmap.close()` fully into cache eviction and the renderer
`destroy()` chain; demote the periodic GC nudge once ownership covers it.
@@ -225,13 +186,6 @@ remembering.
Found by exact live-counters, static-mode isolation, macOS `leaks`/`heap`/
`vmmap`, and component bisection. RSS alone cannot pinpoint a referenced leak;
budget heap tooling for this class.
- **Offscreen-texture caching is timing-nondeterministic on complex screens.**
This is why the geo (no-render-target) node is the robust default, not the
texture node.
- **Rounded/path layer clips must be applied or deferred on the fast path.** The
geo path submits raw geometry, which clips only to a rect; rounded clips are a
lazy offscreen mask the fast replay bypasses. Rect clip on the fast path;
rounded/generic clips fall back to block-replay.
- **Screenshots miss crashes and settle-timing.** Free-running screenshots on
animated/settling screens give false signals. Use render-to-quiescence +
virtual frame time for parity, the probe for interaction/crash coverage.
@@ -244,7 +198,7 @@ remembering.
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
`SkiaLayer` and windowing; it fights the `:desktop-native-window` SDL loop. The port borrows
only the layer engine.
## 9. Key files
@@ -252,12 +206,9 @@ remembering.
- `compose/ui/ui/src/nativeMain/.../RenderBackend.kt`: the interface.
- `compose/ui/ui/src/nativeMain/.../GpuMode.kt`: renderer / driver picker.
- `compose/ui/ui/src/skikoRendererMain/.../renderer/skia/SkiaRenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3RenderBackend.kt`.
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/FreeTypeIcons.kt`:
variable-font axis rasterization.
- `compose/ui/ui/src/commonMain/.../node/ComposeRootHost.kt`: root host,
hit-test, event dispatch, snapshot observer sweep.
- `compose/ui/ui/src/commonMain/.../node/impl/ComposeOwner.kt`: the project
`Owner` + `GraphicsLayerOwnerLayer` bridge.
- `compose/ui/ui/src/nativeMain/.../ui/text/SdlParagraph.native.kt`: the bridged
`Paragraph` (measurement, hit-test, line metrics, span painting).
- `compose/ui/ui-text/src/nativeMain/.../ui/text/SkiaParagraph.native.kt`: the
upstream `Paragraph` actual (measurement, hit-test, line metrics, span painting).
+402
View File
@@ -0,0 +1,402 @@
# Skiko on mingwX64 — Feasibility & Effort Assessment
> **STATUS: SHIPPED (Route 1a).** The real Skia leg now runs on mingwX64 via the
> bitsycore skiko fork + `skiko-windows-x64.dll`. This document is retained as
> **historical design context** — the feasibility findings and route trade-offs
> that led to the shipped implementation.
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
+31 -82
View File
@@ -2,9 +2,8 @@
Everything the port currently leaves as a no-op, stub, hardcode, or partial
implementation, plus the gaps worth closing before a stable 1.12 release. This
is an audit of PROJECT code only (the `*.native.kt` / `*.sdl.kt` /
`com.compose.sdl.*` actuals); vendored upstream under `src/vendor/` is out of
scope.
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:
@@ -14,10 +13,10 @@ Severity is a rough guide, not a mandate:
- **Cosmetic**: minor fidelity or edge-case behavior.
Context for prioritizing: the project goal is **G1, cheap upstream-tracking**
(see [RENDERER.md](RENDERER.md)). The **JVM Compose Desktop target is the
documented Windows fidelity/feature tier**, so SDL-leg visual gaps (section H)
are lower priority than the cross-cutting platform gaps that affect every
backend including Skia on macOS/Linux.
(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
@@ -37,28 +36,13 @@ Platform / cross-cutting:
via SDL (I-beam over text, hand over links).
- **WindowInfo** (`bad23073`) — `isWindowFocused` + `containerSize` / `containerDpSize`
report real values (were hardcoded `true` / `Zero`).
- **ImageBitmap from bytes** (SDL leg) — `ByteArray.decodeToImageBitmap()` /
`createImageBitmap(bytes)` decodes via SDL3_image; was
`UnsupportedOperationException`. Verified `demo --imagebytestest` on both legs.
- **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`.
SDL renderer fidelity (all verified against the Skia leg via `demo --<name>test`):
- **DashPathEffect** (`--dashtest`), **gradient TileMode** repeat/mirror/decal
(`--tilemodetest`), **drawPoints/drawRawPoints** Points/Lines/Polygon
(`--pointstest`), **rotated images** via textured `SDL_RenderGeometry`
(`--rotimgtest`).
- **BlendMode** (`774271eb`, `--blendtest`) — Plus/Modulate/Multiply/Src via SDL
hardware blend (`withBlend`); other separable modes fall back to SrcOver.
- **Stroke joins + Square cap** (`3518521c`, `--jointest`) — miter/bevel/round
(`emitJoin`) + round/square end caps (`emitCap`), pixel-identical to Skia.
- **ColorFilter on shapes** (`169a51f0`, `--filtertest`) — tint / colorMatrix
(grayscale/saturation) / lighting per-vertex; pixel-exact vs Skia.
- **Stroked elliptical ovals** (`efaf20d9`, `--ovaltest`) — true ellipse band, not
a circular ring on the averaged radius.
---
## Release-blocking shortlist for stable 1.12
@@ -70,7 +54,7 @@ The cross-cutting items to weigh first (all detailed below):
menu, upstream TODO CMP-7819).
2. **Accessibility is entirely absent** (section A). Decide whether stable
requires any screen-reader support at all.
3. Fonts (section D): compose-resources `Font(Res.font.x)` works on both legs, and
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.
@@ -110,15 +94,14 @@ calendar/clock/number prefs). These require a bundled CLDR subset or a K/N i18n
- `: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.**
- `compose/ui/ui/src/nativeMain/.../text/intl/Locale.native.kt` · `Locale.isRtl()` not implemented; the pipeline is LTR-only (`SkiaParagraph.native.kt` hardcodes `textDirection = Ltr`, so `getParagraphDirection` always returns `Ltr`; owners/DrawScopes default `LayoutDirection.Ltr`). No RTL shaping. **Nice-to-have.**
- No `NumberFormat` / currency / decimal-grouping actual exists; numbers render via `toString()` with `.` decimal and no grouping regardless of locale. **Cosmetic.**
## D. Font resolution
**Custom fonts via compose-resources work.** `org.jetbrains.compose.resources.Font(Res.font.x)`
loads the bytes and registers them with the project font registry (IconFont → NamedFont)
on both renderer legs (`FontResources.sdl.kt`, shared by the skiko leg via a srcDir
alias), so the standard CMP way to bundle a font renders correctly.
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
@@ -129,7 +112,7 @@ alias), so the standard CMP way to bundle a font renders correctly.
## 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/ui/ui-text/src/nativeMain/.../text/SkiaParagraph.native.kt` · text `Shadow` + `TextDecoration` now render (the `paint(...)` overloads forward them to `ops.rebuildAndPaint`), but stroke `DrawStyle` and non-`SrcOver` `blendMode` are still accepted and ignored, 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
@@ -149,56 +132,29 @@ Drop INTO the window (files + text) is fully wired and works. Drag OUT does not.
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:287` · deprecated `clipboardManager` `setText`/`getText` NOP, and `clipboard.getClipEntry` null. Harmless: the real `LocalClipboard` (text + PNG image via SDL3) works. **Cosmetic.**
- `compose/foundation/.../text/input/internal/selection/TextFieldSelectionState.native.kt:44` · `ClipboardPasteState.hasClip` aliased to `hasText`, so image-only clipboard is not detected for the paste affordance. **Cosmetic.**
## H. SDL renderer graphics (Windows / `-Prenderer=sdl3` only)
## H. Skia renderer anomalies
These affect only the SDL leg. On macOS/Linux the Skia leg implements them
correctly, and JVM is the documented Windows fidelity tier, so these are lower
priority under G1. Listed because SDL is the shipped Windows renderer. (Several
SDL fidelity gaps are now closed — see **Completed** above.)
Skia is the renderer everywhere, so its bugs are rarely tracked here. This one
surfaced while adding BlendMode coverage (`demo --blendtest`):
The biggest remaining value here is the **offscreen-layer work** (blur + real
`saveLayer`); the two are the same infrastructure and warrant a dedicated pass
with the full parity + verify-mac gate, not a probe.
- **Blur / RenderEffect** — `compose/ui/ui/src/sdlRendererMain/.../graphics/RenderEffect.sdl.kt:16` `RenderEffect.isSupported()` is `false`; `Modifier.blur()` and `graphicsLayer{renderEffect=...}` do nothing; `.../graphics/shadow/Blur.native.kt:33` `Paint.setBlurFilter` NOP. Needs an offscreen render target + a blur pass (box-blur infra already exists in `Sdl3ShadowCache`). **Nice-to-have (Windows visual parity).**
- **`saveLayer` has no offscreen buffer** — `Sdl3Canvas.kt:436`: layer alpha is multiplied into each primitive, so overlapping content double-composites (wrong group opacity) and the layer paint's colorFilter/blendMode/renderEffect are dropped. Same offscreen infra as blur. **Nice-to-have (blocker for correct group-alpha over overlapping content).**
- **`clipPath` degrades to a bounding box** — `Sdl3Canvas.kt:509`: an arbitrary path clip collapses to its AABB; a rotated/sheared `clipRect` also collapses to an AABB, so non-rect clips leak. (Rounded/difference clips are real, with feathered AA.) **Nice-to-have.**
- **`ActualImageShader` / `ActualCompositeShader` are stubs** — `CanvasPaintActuals.native.kt:106`: an image/composite `ShaderBrush` degrades to a solid white/black fill (the tessellator emits per-vertex colours, not UVs). **Nice-to-have.**
- **`PathFillType` (NonZero vs EvenOdd)** — mostly ignored beyond the 2-contour border-ring case; interior holes and self-intersections are not cut out, and concave fills self-overlap (fan triangulation) (`Sdl3DrawScope.kt:366,717`). Needs a scanline/tessellation fill. **Nice-to-have.**
- **`drawVertices`** (custom vertex meshes) is still a NOP in the DrawScope. **Nice-to-have.**
- **ColorFilter/blur on IMAGE blits** — image blits honor only `BlendModeColorFilter` tint (`Sdl3Canvas.kt:1281`); a `ColorMatrix`/lighting filter or blur on a bitmap needs per-pixel work. Capture-mode (`graphicsLayer`) block-replay also drops the shape colorFilter. **Nice-to-have.**
- **`SdlImageBitmap.readPixels` is a no-op** (`Sdl3Offscreen.kt:117`) — can't read back an offscreen-rendered bitmap. **Cosmetic / nice-to-have.**
- **Drop shadow is approximated** (`Sdl3Canvas.kt:785`) — 9-slice / stacked rings, not a true gaussian; ambient vs spot largely collapsed; AA is a ~1px geometry fringe, not analytic coverage. **Cosmetic.**
- `DashPathEffect` `corner`/`chain`/`stamped` variants remain NOP (only the interval dash is implemented). **Cosmetic.**
## I. Skia renderer (macOS/Linux default) anomalies
The Skia leg is normally the fidelity reference, so its bugs are rarely tracked
here. This one surfaced while adding SDL BlendMode parity (`demo --blendtest`):
- `BlendMode.Multiply` renders wrong on the Skia leg. A filled `drawRect` with
opaque `Color.Cyan` over opaque `Color.Yellow` reads back `(0,0,255)` blue;
the multiply formula can only yield `(0,255,0)` green for opaque cyan×yellow,
and the SDL leg produces exactly that. `Plus` and `Modulate` composite
correctly on the same path, so `paint.blendMode` IS applied — the fault is
specific to `MULTIPLY` (likely a Metal-backend / premultiply interaction in
the graphics-layer flatten, not the `BlendMode.toSkia()` map, which is
correct). Needs isolating on the Skia draw path. **Nice-to-have (Skia leg).**
- `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 (3 parallel passes over the renderer, recent SDL features, and the
text/input/event actuals) surfaced these. The clear, contained bugs were fixed
in the same pass (see below); the items here are left open because they are
either invasive, risky, or a cosmetic-AA judgement call.
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): the `roundRectCore`
inverted-rect crash; `drawArc(useCenter=false)` now fills the segment (was always
a pie sector); `BlendMode.Src` no longer punches the AA fringe out via
`SDL_BLENDMODE_NONE`; the cover-fill fast-path no longer drops the paint's
`colorFilter`; `saveLayer` in capture mode keeps the save/restore stack balanced;
stroked oval / round-rect inner radius is clamped ≥ 0; the per-vertex
`ColorMatrix` copy is hoisted out of the sample loop; `Locale.region` no longer
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.
@@ -208,10 +164,3 @@ Open — platform / input:
- **A window created unfocused is still promoted to RESUMED** (`ComposeWindow.kt:575-576, 713-719`). `windowFocused`/`windowVisible` default `true`, so a second `Window {}` opened while another holds focus (or one opened minimized) reports `RESUMED` until SDL later delivers a focus/minimize event. Should query `SDL_GetWindowFlags` at creation. **Review.**
- **IME text-input area is only pushed on the first `TEXT_EDITING`** (`ComposeWindow.kt:803`; `updateImeArea()` not called on focus gain / first `StartTextInput` / caret move). The first candidate popup can appear at (0,0) before correcting. The rect math itself is correct. **Review.**
- **`Snapshot.sendApplyNotifications()` is called synchronously inside the global write observer** (`ComposeWindow.kt:143-146`). Compose Desktop's `GlobalSnapshotManager` deliberately defers it to a separate dispatch; calling it per-write is redundant (the main loop already calls it each frame) and risks re-entrancy during snapshot application. Verify against the runtime's guard before changing — behaviour-sensitive. **Review (risky).**
Open — SDL renderer (cosmetic AA / draw-time):
- **Stroke joins are not antialiased and reach ~0.5px past the band** (`Sdl3DrawScope.kt` `emitJoin`). The segment band feathers to `halfW − kAaHalf` solid + fringe, but joins fill solid to full `halfW` with a hard edge, so corners read as slightly darker/crisper nubs — visible on thin or translucent strokes. Needs feathered join geometry. **Review (cosmetic).**
- **Round join over-draws the stroke body** (`Sdl3DrawScope.kt` `emitJoin`, `StrokeJoin.Round`): a full disc overlaps the adjacent segments, so a semi-transparent stroke double-blends darker at each corner. Fill only the convex wedge. **Review (cosmetic).**
- **Text / icon draws ignore the draw-time `colorFilter`** — `activeColorFilter` is consulted only by the tessellation sampler; glyph/icon runs blit through the text renderer with a pre-resolved colour, so a `BlendModeColorFilter` tint applied at draw time to a `Text`/icon node isn't honored (acceptable if callers pre-fold the tint into the glyph colour). **Review.**
- **Rounded-clip containment ignores stroke half-width** (`Sdl3Canvas.kt` `admitDraw`/`tryDrawRectUnderPendingClips`): a stroked shape whose fill-box is inside a pending rounded clip but whose stroke pokes past it skips mask realization, so the stroke's outer half is bounded only by the AABB, not the rounded outline. Marginal (only bites at the exact clip boundary). **Review.**
+24 -17
View File
@@ -8,18 +8,18 @@ For architecture, source-set layout, and vendoring rules, see
## Native libraries
SDL3, SDL3_ttf, SDL3_image, and FreeType are built from source as static
libraries and linked straight into the executable. One script does it on every
OS. Output lands in the gitignored `libs/`; versions are pinned in
`scripts/build-sdl/build-sdl.properties`.
SDL3 is built from source as a static library and linked straight into the
executable — the windowing, input, and platform-integration layer. One script
does it on every OS. Output lands in the gitignored `libs/`; the version is
pinned in `scripts/build-sdl/build-sdl.properties`.
```bash
python3 scripts/build-sdl/build-all.py # freetype, sdl3, sdl3-image, sdl3-ttf
python3 scripts/build-sdl/build-all.py sdl3-ttf # rebuild one step
python3 scripts/build-sdl/build-all.py # sdl3
python3 scripts/build-sdl/build-all.py sdl3 # rebuild the step
```
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
absent). Run it once per machine, or after bumping the pinned versions.
absent). Run it once per machine, or after bumping the pinned version.
## Vendoring upstream Compose
@@ -63,16 +63,16 @@ python3 scripts/compose-coverage.py --missing ui-text # list uncovered decl
### One-command gate
`verify-mac.sh` is the runbook to run after any renderer or layout change. On
macOS or Linux it exercises BOTH renderer legs (Skia by default, then
`-Prenderer=sdl3`) and exits non-zero on any failure:
macOS or Linux it exercises the Skia renderer and exits non-zero on any
failure:
```bash
scripts/verify-mac.sh
```
It runs, per leg: the vendor drift and clean checks, a build of `:demo` and
`:apidemo`, the interaction probes, the parity sweep, a memory soak, and a
frame-time spot check. The Windows target is verified separately (see below).
It runs: the vendor drift and clean checks, a build of `:demo` and `:apidemo`,
the interaction probes, the parity sweep, a memory soak, and a frame-time spot
check. The Windows target is verified separately (see below).
### Parity: native vs JVM
@@ -135,6 +135,13 @@ gradlew.bat :demo:runDebugExecutableMingwX64
gradlew.bat :apidemo:runDebugExecutableMingwX64
```
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
GitHub Packages as `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (macOS/Linux use
official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
to the executable by the bridge plugin — no manual copy. The fork itself is
published by a separate GitHub Actions workflow in the fork repo, out of band
from this repo's release flow.
## Consuming the port
To build a third-party app against the published klibs, apply the bridge Gradle
@@ -154,8 +161,8 @@ single file to edit; know which axis you are changing.
| Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Group is `com.bitsycore.compose.sdl`. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
| Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. |
| JVM parity forcing | `vComposeJvmVersion` in `demo`, `apidemo`, and `material-symbols` `build.gradle.kts`. | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published. |
| Skiko | `skiko` in `gradle/libs.versions.toml`. | Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses. Verify with a throwaway `skikoRendererMain` compile if unsure. |
| SDL3 / SDL3_ttf / SDL3_image / FreeType | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
| Skiko | `skiko` in `gradle/libs.versions.toml`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.1` from GitHub Packages), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
| SDL3 | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
| Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. |
### Bump the upstream Compose ref
@@ -168,8 +175,8 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
its `// VENDOR-BASE:` header to the new ref.
4. Build both legs and fix any fallout.
5. `scripts/verify-mac.sh` (both legs green, including the soak and parity gates).
4. Build and fix any fallout.
5. `scripts/verify-mac.sh` (green, including the soak and parity gates).
6. If a matching Compose version is now published to Maven, bump
`vComposeJvmVersion` in demo/apidemo/material-symbols to close the skew.
7. Run WIN-SMOKE on a Windows host.
@@ -180,7 +187,7 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
1. Complete the ref-bump flow above and confirm it is green. For a STABLE
release, `vComposeJvmVersion` should match the vendored ref (no skew).
2. On a Windows host, run `gradlew :window:compileCommonMainKotlinMetadata`.
2. On a Windows host, run `gradlew :desktop-native-window:compileCommonMainKotlinMetadata`.
Only Windows compiles the full common-metadata variant table, and the root
KotlinMultiplatform publications live there; a macOS-only publish leaves the
roots without mingwX64 variants (this bit v0.1.15).
+1 -1
View File
@@ -81,7 +81,7 @@ kotlin {
}
nativeMain {
dependencies {
implementation(project(":window"))
implementation(project(":desktop-native-window"))
implementation(libs.ktor.client.curl)
}
}
+4 -4
View File
@@ -908,7 +908,7 @@ private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerCh
/** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1),
the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n)
scan, memoised — replaces the old gutter pre-pass that ran measurer.wrap() on
scan, memoised — replaces the old gutter pre-pass that measured wrapping on
every one of N lines on each width change (the other half of the freeze). */
private fun lineStartOffsets(inText: String): IntArray {
val vStarts = ArrayList<Int>(64)
@@ -963,10 +963,10 @@ internal fun BodyView(
val vGutterWidth = (vDigits * 7 + 4).dp
// Body wrap width (physical px, from onSizeChanged), reported by the body
// Box once it's laid out (0 on the first frame). The gutter numbering
// depends on it — see vNumbers. NativeTextMeasurer.wrap expects the font
// depends on it — see vNumbers. Wrap measurement expects the font
// size + max-width in the SAME unit (both physical px on Retina), so we
// scale 12.sp through the current density here, matching what SdlParagraph
// does when it builds the body's real paragraph.
// scale 12.sp through the current density here, matching what the paragraph
// engine does when it builds the body's real paragraph.
var vBodyWidthPx by remember { mutableStateOf(0) }
val vFontPx = with(LocalDensity.current) { 12.sp.toPx().toInt() }
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
@@ -16,7 +16,13 @@ import com.compose.sdl.res.painterResource
import com.compose.sdl.revealInFileManager
import com.compose.sdl.showOpenFileDialog
import com.compose.sdl.showSaveFileDialog
import com.compose.sdl.text.currentTextMeasurer
import com.compose.sdl.text.namedFontFamily
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
import io.ktor.client.*
import io.ktor.client.engine.curl.*
import okio.FileSystem
@@ -49,8 +55,31 @@ actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
currentTextMeasurer.wrap(inText, inFontPx, inMaxWidthPx, inFamilyName).lines.size
// Cached resolver for wrappedRowCount — the skiko engine resolves fonts by name
// (projectFontName), so this is just the required non-null Paragraph argument.
private val mRowCountFontResolver = createFontFamilyResolver()
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int {
if (inText.isEmpty()) return 1
// inFontPx is already physical pixels, so measure at density 1 (fontSize.value
// * density = inFontPx) and wrap at inMaxWidthPx px by building a Paragraph.
// lineCount comes from skiko's paragraph layout.
val vStyle = TextStyle(
fontSize = inFontPx.sp,
fontFamily = inFamilyName?.let { namedFontFamily(it) },
)
return Paragraph(
text = inText,
style = vStyle,
constraints = Constraints(maxWidth = inMaxWidthPx.coerceAtLeast(0)),
density = Density(1f),
fontFamilyResolver = mRowCountFontResolver,
spanStyles = emptyList(),
placeholders = emptyList(),
maxLines = Int.MAX_VALUE,
ellipsis = false,
).lineCount
}
actual var editorTabWidth: Int
get() = TextLayoutConfig.tabWidth
+46 -14
View File
@@ -18,8 +18,38 @@ apiValidation {
// Version is driven by PUBLISH_VERSION (set from the git tag in the publish
// workflow, `v1.2.3` → `1.2.3`). Local dev / demo runs default to SNAPSHOT.
val vPublishVersion = (System.getenv("PUBLISH_VERSION") ?: "0.0.0-SNAPSHOT").removePrefix("v")
// Published groups mirror the org.jetbrains.compose.<area> originals under the
// com.bitsycore fork name, so each module is the obvious 1:1 fork of its upstream
// coord (e.g. org.jetbrains.compose.ui:ui → com.bitsycore.compose.ui:ui). Modules
// with no upstream area (project-only: :sdl-core, :material-symbols) fall through
// to the default com.bitsycore.compose.sdl group; :desktop-native-window is the
// headline app-shell artifact, published under com.bitsycore.compose.
val kDefaultGroup = "com.bitsycore.compose.sdl"
val kAreaGroups = mapOf(
":ui" to "com.bitsycore.compose.ui",
":ui-graphics" to "com.bitsycore.compose.ui",
":ui-text" to "com.bitsycore.compose.ui",
":ui-unit" to "com.bitsycore.compose.ui",
":ui-geometry" to "com.bitsycore.compose.ui",
":ui-util" to "com.bitsycore.compose.ui",
":ui-backhandler" to "com.bitsycore.compose.ui",
":ui-tooling-preview" to "com.bitsycore.compose.ui",
":foundation" to "com.bitsycore.compose.foundation",
":foundation-layout" to "com.bitsycore.compose.foundation",
":animation" to "com.bitsycore.compose.animation",
":animation-core" to "com.bitsycore.compose.animation",
":animation-graphics" to "com.bitsycore.compose.animation",
":material3" to "com.bitsycore.compose.material3",
":material-ripple" to "com.bitsycore.compose.material",
":components-resources" to "com.bitsycore.compose.components",
":navigation3-ui" to "com.bitsycore.navigation3",
":desktop-native-window" to "com.bitsycore.compose",
)
fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup
allprojects {
group = "com.bitsycore.compose.sdl"
group = groupFor(path)
version = vPublishVersion
}
@@ -37,12 +67,13 @@ allprojects {
val kAppModules = setOf(":demo", ":apidemo")
val kPublishedLibs = setOf(
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
":ui-tooling-preview",
":sdl-core",
":ui", ":ui-util", ":ui-geometry", ":ui-graphics", ":ui-text",
":ui-unit", ":ui-backhandler", ":ui-tooling-preview",
":animation-core", ":animation", ":animation-graphics",
":foundation", ":foundation-layout",
":material3", ":material-ripple",
":window", ":material-symbols",
":desktop-native-window", ":material-symbols",
":navigation3-ui", ":components-resources",
)
@@ -76,7 +107,7 @@ subprojects {
kPublishedLibs.forEach { modulePath ->
val vArtifactId = modulePath.removePrefix(":")
substitute(project(modulePath))
.using(module("com.bitsycore.compose.sdl:$vArtifactId:$kConsumeVersion"))
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
.because("-PuseGithubPackages=true")
}
}
@@ -135,10 +166,11 @@ allprojects {
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
// ui's split artifacts: ui-graphics + ui-text are merged INTO
// :ui in the port; ui-unit / ui-geometry have their own modules.
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui"))
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui"))
// ui-graphics / ui-text are their own modules (split out of :ui,
// upstream layout). Declare each DIRECTLY so the app commonMain sees
// them under the granular-metadata visibility rule.
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui-graphics"))
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui-text"))
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit"))
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
@@ -160,11 +192,11 @@ allprojects {
}
// Whether the current host can build the mingwX64 target. Kotlin/Native can
// only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf /
// sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by
// scripts/build-sdl/build-all.py run on a Windows host). Declaring `mingwX64()` on
// a non-Windows host is safe for pure-Kotlin modules but blows up the moment
// a `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h.
// only cross-compile the mingwX64 sdl3 cinterop from a Windows host — it needs
// the Windows SDL3 headers under libs/ (produced by scripts/build-sdl/build-all.py
// run on a Windows host). Declaring `mingwX64()` on a non-Windows host is safe
// for pure-Kotlin modules but blows up the moment the sdl3 cinterop tries to
// include SDL3's headers.
// Override with `-PforceMingw=true` if you actually have the headers wired.
val vHostSupportsMingw = System.getProperty("os.name").startsWith("Windows") ||
(findProperty("forceMingw") as? String)?.toBoolean() == true
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:components-resources>
// Library unique name: <com.bitsycore.compose.components:components-resources>
open annotation class org.jetbrains.compose.resources/ExperimentalResourceApi : kotlin/Annotation { // org.jetbrains.compose.resources/ExperimentalResourceApi|null[0]
constructor <init>() // org.jetbrains.compose.resources/ExperimentalResourceApi.<init>|<init>(){}[0]
}
+7 -49
View File
@@ -3,8 +3,8 @@
// Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte
// upstream (painterResource / stringResource / Font / qualifiers / Res codegen
// compatibility); the platform actuals are this port's: data.kres reading,
// SDL3_image decoding, SDL locale/theme environment. Apps' JVM targets keep
// using the official Maven artifact — this module is native-only.
// image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM
// targets keep using the official Maven artifact — this module is native-only.
plugins {
alias(libs.plugins.kotlin.multiplatform)
@@ -17,7 +17,6 @@ repositories {
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
@@ -41,52 +40,11 @@ kotlin {
}
nativeMain {
kotlin.srcDir("src/vendor/native/kotlin")
}
// ============
// Renderer roots — same conditional wiring as :ui: only the source
// sets that will actually be attached are created.
val sdlRendererMain = create("sdlRendererMain") {
dependsOn(nativeMain.get())
kotlin.srcDir("src/vendor/sdlRenderer/kotlin")
}
if (vHostSupportsMingw) {
val sdlRendererMingwMain = create("sdlRendererMingwMain") { dependsOn(sdlRendererMain) }
get("mingwX64Main").dependsOn(sdlRendererMingwMain)
}
val macosArm64Main = get("macosArm64Main")
val linuxX64Main = get("linuxX64Main")
val linuxArm64Main = get("linuxArm64Main")
if (useSdl3Everywhere) {
val sdlRendererMacosMain = create("sdlRendererMacosMain") { dependsOn(sdlRendererMain) }
val sdlRendererLinuxMain = create("sdlRendererLinuxMain") { dependsOn(sdlRendererMain) }
macosArm64Main.dependsOn(sdlRendererMacosMain)
linuxX64Main.dependsOn(sdlRendererLinuxMain)
linuxArm64Main.dependsOn(sdlRendererLinuxMain)
} else {
// The Font/Image actuals for this module are pure project code
// (:ui's IconFont / NamedFont / decodeEncodedImageBitmap in nativeMain)
// so they work identically under either renderer. Only one renderer
// source set is attached to a given target, so pointing skikoRenderer
// at sdlRendererMain's srcDir doesn't cause duplicate actuals.
// Upstream's skikoMain vendored files here referenced Compose-Desktop
// Skia extensions (SystemFont / toComposeImageBitmap / nativeCanvas)
// that this port's :ui doesn't expose — sharing the SDL actuals sidesteps
// that gap entirely.
val skikoRendererMain = create("skikoRendererMain") {
dependsOn(nativeMain.get())
kotlin.srcDir("src/sdlRendererMain/kotlin")
dependencies {
implementation(libs.skiko)
}
}
val skikoRendererMacosMain = create("skikoRendererMacosMain") { dependsOn(skikoRendererMain) }
val skikoRendererLinuxMain = create("skikoRendererLinuxMain") { dependsOn(skikoRendererMain) }
macosArm64Main.dependsOn(skikoRendererMacosMain)
linuxX64Main.dependsOn(skikoRendererLinuxMain)
linuxArm64Main.dependsOn(skikoRendererLinuxMain)
// src/nativeMain/kotlin also holds the Font / Image resource actuals —
// pure project code that delegates to the project font registry
// (IconFont / NamedFont) and the :ui-graphics Skia decoder
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
// every target.
}
}
@@ -4,10 +4,10 @@
# mingwX64/linux variants, so the runtime library is vendored and the platform
# actuals are project code:
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL),
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser)
# src/sdlRendererMain — image decode via the :ui SDL3_image hook, SVG unsupported,
# Font via the project font registry
# vendor/skikoRenderer — upstream skikoMain Skia-bound files, verbatim
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
# and the Font/Image resource actuals: renderer-agnostic
# project code (image decode via the :ui-graphics Skia
# decoder, Font via the project font registry)
# Re-sync: python scripts/compose-fork/sync.py components/resources/library
SET_REPO=https://github.com/JetBrains/compose-multiplatform@<COMPOSE_REF>
@@ -54,10 +54,9 @@ skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
# doesn't expose. Since the sdlRenderer actuals (src/sdlRendererMain/kotlin/) use
# pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap) that work
# on either renderer, the module's build shares those with skikoRendererMain
# instead of vendoring the upstream files. Opt them out of the sync.
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
# instead, so opt the upstream Skia-bound files out.
!skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt
!skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
!skikoMain/kotlin/org/jetbrains/compose/resources/SvgPainter.kt
@@ -15,16 +15,16 @@ import com.compose.sdl.icons.IconFont
import com.compose.sdl.text.NamedFont
// ==================
// MARK: Font actuals — SDL renderer
// MARK: Font actuals — Skia text stack via the project font registry
// ==================
/** The SDL text stack resolves fonts by FAMILY NAME through the project font
/** The Skia text stack resolves fonts by FAMILY NAME through the project font
registry (IconFont.registerIcon — the general byte-font table, not just
icons). A font resource therefore loads its bytes through the resource
reader, registers them under a per-resource family name, and returns a
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the
standard project pipeline. Weight/style are carried on the NamedFont;
variation axes apply at draw time via the renderer's axis support. */
variation axes apply at draw time via Skia's axis support. */
private val emptyFont: Font = NamedFont("res-font:<loading>")
@@ -12,21 +12,20 @@ import androidx.compose.ui.unit.Density
import com.compose.sdl.graphics.decodeEncodedImageBitmap
// ==================
// MARK: Image actuals — SDL renderer
// MARK: Image actuals — Skia decode via the :ui-graphics hook
// ==================
/** Decoding goes through the :ui hook the SDL backend registers at init
(SDL3_image, IMG auto-detect). resourceDensity/targetDensity are ignored:
/** 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 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. */
/** SVG: Skia rasterises it at its intrinsic size, 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. */
internal actual class SvgElement(val bytes: ByteArray)
internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
@@ -4,46 +4,24 @@ package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.ExperimentalResourceApi
import kotlinx.cinterop.alloc
import kotlinx.cinterop.get
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.pointed
import kotlinx.cinterop.ptr
import kotlinx.cinterop.toKString
import kotlinx.cinterop.IntVar
import kotlinx.cinterop.value
import sdl3.SDL_GetPreferredLocales
import sdl3.SDL_GetSystemTheme
import sdl3.SDL_SystemTheme
import sdl3.SDL_free
import com.compose.sdl.res.preferredLocaleProvider
import com.compose.sdl.res.systemThemeIsDarkProvider
// ==================
// MARK: ResourceEnvironment — SDL actual
// MARK: ResourceEnvironment — platform-env-seam actual
// ==================
/** Non-composable system environment for qualifier resolution (values-fr,
drawable-dark, …): locale from SDL_GetPreferredLocales, theme from
SDL_GetSystemTheme. Density is reported as 1f — under this port's Option-B
density flow layout runs in physical pixels and drawables are bundled at a
single density, so the mdpi bucket is always the right one. The COMPOSABLE
path (rememberResourceEnvironment) doesn't use this: it reads
LocalDensity / isSystemInDarkTheme from the composition. */
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
drawable-dark, …): locale + theme come from the platform-env seams installed
by the SDL layer, so :components-resources carries no dependency on the sdl3
cinterop. Density is reported as 1f — under this port's Option-B density flow
layout runs in physical pixels and drawables are bundled at a single density,
so the mdpi bucket is always the right one. The COMPOSABLE path
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
isSystemInDarkTheme from the composition. */
internal actual fun getSystemEnvironment(): ResourceEnvironment {
var vLanguage = ""
var vRegion = ""
memScoped {
val vCount = alloc<IntVar>()
val vLocales = SDL_GetPreferredLocales(vCount.ptr)
if (vLocales != null && vCount.value > 0) {
val vFirst = vLocales[0]?.pointed
vLanguage = vFirst?.language?.toKString() ?: ""
vRegion = vFirst?.country?.toKString() ?: ""
SDL_free(vLocales)
}
}
val vDark = SDL_GetSystemTheme() == SDL_SystemTheme.SDL_SYSTEM_THEME_DARK
val (vLanguage, vRegion) = preferredLocaleProvider?.invoke() ?: ("" to "")
val vDark = systemThemeIsDarkProvider?.invoke() ?: false
return ResourceEnvironment(
language = LanguageQualifier(vLanguage),
// SDL locales carry language+country only — no script subtag.
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:animation-core>
// Library unique name: <com.bitsycore.compose.animation:animation-core>
open annotation class androidx.compose.animation.core/ExperimentalAnimationSpecApi : kotlin/Annotation { // androidx.compose.animation.core/ExperimentalAnimationSpecApi|null[0]
constructor <init>() // androidx.compose.animation.core/ExperimentalAnimationSpecApi.<init>|<init>(){}[0]
}
@@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// :animation-core — androidx.compose.animation.core.* vendored VERBATIM from
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
// vector math). Split out of :core so :core stays about renderer+ui glue only.
// vector math). Split out of :ui so :ui stays about renderer+ui glue only.
//
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:animation-graphics>
// Library unique name: <com.bitsycore.compose.animation:animation-graphics>
open annotation class androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi : kotlin/Annotation { // androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi|null[0]
constructor <init>() // androidx.compose.animation.graphics/ExperimentalAnimationGraphicsApi.<init>|<init>(){}[0]
}
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:animation>
// Library unique name: <com.bitsycore.compose.animation:animation>
open annotation class androidx.compose.animation/ExperimentalAnimationApi : kotlin/Annotation { // androidx.compose.animation/ExperimentalAnimationApi|null[0]
constructor <init>() // androidx.compose.animation/ExperimentalAnimationApi.<init>|<init>(){}[0]
}
@@ -5,5 +5,5 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.native:window>
// Library unique name: <com.bitsycore.compose:desktop-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]
@@ -1,8 +1,8 @@
// :window — the module apps depend on. Owns nativeComposeWindow() (main loop,
// :desktop-native-window — the module apps depend on. Owns nativeComposeWindow() (main loop,
// recomposer lifecycle, event dispatch, Snapshot apply notifications).
// Renderer selection happens entirely inside :core via source-set wiring
// (skikoRendererMain vs sdlRendererMain) — this module just calls
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :core and
// Renderer selection happens entirely inside :ui via source-set wiring
// (skikoRendererMain — Skia-only) — this module just calls
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
// the right symbol resolves per target.
// Publication artifactId (when set up): compose-desktop-native.
@@ -23,15 +23,15 @@ kotlin {
applyDefaultHierarchyTemplate()
// sdl3.* types are api-exposed via :ui's cinterop klib — no separate
// sdl3 cinterop here.
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib — no
// separate sdl3 cinterop here.
sourceSets {
commonMain.dependencies {
// api so apps depending on :window also get the compose re-impl,
// Res/resources, GpuMode, and the renderer pipeline from :core, plus
// api so apps depending on :desktop-native-window also get the compose re-impl,
// Res/resources, GpuMode, and the renderer pipeline from :ui, plus
// the foundation / animation-core / animation modules that were split
// out of :core (upstream Compose layout).
// out of :ui (upstream Compose layout).
// Material widgets used to be re-exported from :material here; the
// module was retired when :apidemo and :demo migrated to :material3.
// Apps that want Material 3 widgets pull `implementation(project(":material3"))`
@@ -25,7 +25,6 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.lifecycle.enableSavedStateHandles
import com.compose.sdl.node.ComposeRootHost
import com.compose.sdl.res.currentImageLoader
import com.compose.sdl.text.currentTextMeasurer
import com.compose.sdl.window.LocalPopupHost
import com.compose.sdl.window.PopupLayer
import com.compose.sdl.window.createPopupHostState
@@ -65,7 +64,7 @@ import sdl3.SDL_WaitEventTimeout
routes events per SDL window id.
Per-window pumping order matters: the render-bridge globals
(currentTextMeasurer / currentImageLoader / viewport) are per-renderer, so the
(currentImageLoader / viewport) are per-renderer, so the
loop installs a window's globals before recomposing / laying out / drawing it.
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
@@ -530,12 +529,15 @@ internal class WindowInstance(
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
) {
private val gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
val backend = SDL3Backend(
initialTitle, inWidth, inHeight, gpuMode = gpuMode,
iconLightResourcePaths = inIcon?.light ?: emptyList(),
iconDarkResourcePaths = inIcon?.dark ?: emptyList(),
)
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
// a Skia GPU context can't be created (RDP / headless / missing GL driver).
private var gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
private val initialWidth = inWidth
private val initialHeight = inHeight
private val icon = inIcon
// Assigned by init() from the first backend that comes up (see makeBackend()).
lateinit var backend: SDL3Backend
private set
private var renderBackend: RenderBackend? = null
lateinit var host: ComposeRootHost
private set
@@ -606,14 +608,42 @@ internal class WindowInstance(
needsFrame = true
}
fun init(inScope: CoroutineScope): Boolean {
if (!backend.init()) return false
backend.updateWindowSize()
// Creates + initialises an SDL3Backend for [mode] and its RenderBackend. On
// success commits `backend` and returns the renderer; else tears everything
// down and returns null so the caller can retry with a different mode.
private fun makeBackend(mode: GpuMode): RenderBackend? {
val vBackend = SDL3Backend(
initialTitle, initialWidth, initialHeight, gpuMode = mode,
iconLightResourcePaths = icon?.light ?: emptyList(),
iconDarkResourcePaths = icon?.dark ?: emptyList(),
)
if (!vBackend.init()) {
vBackend.destroy(inQuitSdl = false)
return null
}
vBackend.updateWindowSize()
val vRender = createRenderBackend(vBackend, mode)
if (vRender == null || !vRender.ensureSize(vBackend.pixelWidth, vBackend.pixelHeight)) {
vRender?.destroy()
vBackend.destroy(inQuitSdl = false)
return null
}
backend = vBackend
return vRender
}
val vRender = createRenderBackend(backend, gpuMode)
if (vRender == null || !vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)) {
println("Failed to init render backend for $gpuMode")
backend.destroy(inQuitSdl = false)
fun init(inScope: CoroutineScope): Boolean {
// Try the resolved gpuMode; if it's a Skia GPU mode whose context/bridge
// can't come up (RDP / headless / missing GL driver), fall back once to
// CPU raster (Software) so the window still opens instead of failing.
var vRender = makeBackend(gpuMode)
if (vRender == null && gpuMode is GpuMode.Skia) {
println("GPU renderer ($gpuMode) unavailable — falling back to CPU raster (Software)")
gpuMode = GpuMode.Software
vRender = makeBackend(gpuMode)
}
if (vRender == null) {
println("Failed to init render backend")
return false
}
renderBackend = vRender
@@ -740,7 +770,6 @@ internal class WindowInstance(
measuring, or drawing this window's tree. */
fun installGlobals() {
val vRender = renderBackend ?: return
currentTextMeasurer = vRender.textMeasurer
currentImageLoader = vRender.imageLoader
com.compose.sdl.text.currentViewportWidth = backend.pixelWidth
com.compose.sdl.text.currentViewportHeight = backend.pixelHeight
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:foundation-layout>
// Library unique name: <com.bitsycore.compose.foundation:foundation-layout>
open annotation class androidx.compose.foundation.layout/ExperimentalFlexBoxApi : kotlin/Annotation { // androidx.compose.foundation.layout/ExperimentalFlexBoxApi|null[0]
constructor <init>() // androidx.compose.foundation.layout/ExperimentalFlexBoxApi.<init>|<init>(){}[0]
}
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:foundation>
// Library unique name: <com.bitsycore.compose.foundation:foundation>
open annotation class androidx.compose.foundation.lazy.grid/LazyGridScopeMarker : kotlin/Annotation { // androidx.compose.foundation.lazy.grid/LazyGridScopeMarker|null[0]
constructor <init>() // androidx.compose.foundation.lazy.grid/LazyGridScopeMarker.<init>|<init>(){}[0]
}
@@ -1,8 +1,8 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.*
// (non-core) vendored VERBATIM from upstream. Sits on :core (which still owns
// androidx.compose.ui.*, both renderers, cinterops) + :animation-core.
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
//
// Why the two upstream modules (`:animation` and `:foundation`) are combined
// here: upstream splits `foundation-layout` off from `foundation`, then has
@@ -20,9 +20,9 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
// and its actual lives here as a plain nativeMain kt file (see
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single
// stub returning false. The upstream skiko/sdl per-renderer actuals were dropped
// when :foundation was split from :core: expect/actual must live in the same
// module, and duplicating the skikoRenderer/sdlRenderer source-set hierarchy on
// stub returning false. The upstream skiko per-renderer actuals were dropped
// when :foundation was split from :ui: expect/actual must live in the same
// module, and duplicating the skikoRenderer source-set hierarchy on
// :foundation just to read `LocalSystemTheme` on the Skia path was more build
// wiring than is worth it for one function no in-repo app calls.
//
@@ -1,38 +1,31 @@
package com.compose.sdl.text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
// ==================
// MARK: IconText — project-only text composable for icon fonts
// MARK: IconText — icon-font text on the standard text path
// ==================
/**
A minimal project text composable that renders a codepoint through a named
icon font with optional variable-font axis settings (Material Symbols).
icon font with optional variable-font axis settings (Material Symbols FILL /
wght / GRAD / opsz).
Sits outside `androidx.compose.foundation.text.BasicText` so BasicText can
remain a byte-identical match of upstream. Icons need:
- `fontFamily: String` — the registered icon font family name (project's
`FontFamily.Named`, not upstream `FontFamily.Resolver`).
- `fontVariationSettings` — per-usage variable-font axis values (FILL,
wght, GRAD, opsz for Material Symbols).
Now just a [BasicText]: the icon font family and its variable axes are threaded
through [namedFontFamily] (`axes = …`), which the skiko text engine reads via
`FontFamily.projectFontVariations()` and applies to the typeface. So icons
measure + draw through the same skiko `skparagraph` path as ordinary text — no
separate renderer/measurer seam.
Upstream `TextStyle` has neither: fontFamily routes through the
`FontFamily.Resolver`, and axis values are set per-Font at construction
time. Rather than fight that abstraction for one composable, icons use
this project path — the TextDrawElement modifier feeds directly into
SdlParagraph / Sdl3Canvas.drawNativeText.
Material `Icon(codepoint = ..., fontFamily = ...)` uses this. Everything
else (Text, BasicText, TextField) goes through the upstream-shaped path.
Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText /
TextField go through the upstream-shaped path already.
*/
@Composable
fun IconText(
@@ -44,28 +37,16 @@ fun IconText(
textAlign: TextAlign = TextAlign.Start,
fontVariationSettings: List<FontVariation.Setting>? = null,
) {
// Layout runs in physical pixels (LocalDensity = DPR), so the icon font
// size must also convert `sp → px`. Matches SdlParagraph's `fontSize.value
// * density`. Without this the icons render at half size on Retina.
val vDensity = LocalDensity.current.density
val vFontPx = (fontSize.value * vDensity).toInt().coerceAtLeast(1)
Box(modifier = modifier) {
androidx.compose.ui.layout.Layout(
modifier = TextDrawElement(
text = text,
spans = null,
color = if (color == Color.Unspecified) Color.Black else color,
fontSizePx = vFontPx,
textAlign = textAlign,
softWrap = false,
fontFamily = fontFamily,
fontVariations = fontVariationSettings,
),
) { _, constraints ->
val vSize = currentTextMeasurer.measure(text, vFontPx, Int.MAX_VALUE, fontFamily, fontVariationSettings)
val w = vSize.width.coerceIn(constraints.minWidth, constraints.maxWidth)
val h = vSize.height.coerceIn(constraints.minHeight, constraints.maxHeight)
layout(w, h) {}
}
}
BasicText(
text = text,
modifier = modifier,
style = TextStyle(
color = if (color == Color.Unspecified) Color.Black else color,
fontSize = fontSize,
fontFamily = namedFontFamily(fontFamily, axes = fontVariationSettings),
textAlign = textAlign,
),
softWrap = false,
maxLines = 1,
)
}
@@ -2,9 +2,10 @@ package androidx.compose.foundation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import sdl3.SDL_GetSystemTheme
import sdl3.SDL_SystemTheme
import com.compose.sdl.res.systemThemeIsDarkProvider
// System dark-theme flag via the platform-env seam (installed by the SDL layer),
// so :foundation carries no dependency on the sdl3 cinterop.
@Composable
@ReadOnlyComposable
internal actual fun _isSystemInDarkTheme(): Boolean = SDL_GetSystemTheme() == SDL_SystemTheme.SDL_SYSTEM_THEME_DARK
internal actual fun _isSystemInDarkTheme(): Boolean = systemThemeIsDarkProvider?.invoke() ?: false
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.sp
The four Material Symbols variable-font axes (fill / weight / grade /
opticalSize) come in via `fontVariationSettings`; use `MaterialIconAxes(…)`
to build the list. Skia honours the axes; SDL3_ttf 3.2 ignores them. */
to build the list. Skia honours the axes. */
@Composable
fun IconFontIcon(
codepoint: Int,
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:material-ripple>
// Library unique name: <com.bitsycore.compose.material:material-ripple>
abstract interface androidx.compose.material.ripple/RippleTheme { // androidx.compose.material.ripple/RippleTheme|null[0]
abstract fun defaultColor(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color // androidx.compose.material.ripple/RippleTheme.defaultColor|defaultColor(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
abstract fun rippleAlpha(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.material.ripple/RippleAlpha // androidx.compose.material.ripple/RippleTheme.rippleAlpha|rippleAlpha(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:material3>
// Library unique name: <com.bitsycore.compose.material3:material3>
open annotation class androidx.compose.material3/ExperimentalMaterial3Api : kotlin/Annotation { // androidx.compose.material3/ExperimentalMaterial3Api|null[0]
constructor <init>() // androidx.compose.material3/ExperimentalMaterial3Api.<init>|<init>(){}[0]
}
+1 -1
View File
@@ -35,7 +35,7 @@ kotlin {
api(project(":animation-core"))
api(project(":material-ripple"))
// androidx.collection is used by material3 internals (MutableIntObjectMap, …).
// Already on the classpath via :core's runtime; declare here for clarity.
// Already on the classpath via :ui's runtime; declare here for clarity.
implementation("androidx.collection:collection:1.5.0")
// androidx.graphics.shapes.* — MaterialShapes (RoundedPolygon /
// Morph / CornerRounding) that WavyProgress / LoadingIndicator use.
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-backhandler>
// Library unique name: <com.bitsycore.compose.ui:ui-backhandler>
final class androidx.compose.ui.backhandler/BackEventCompat { // androidx.compose.ui.backhandler/BackEventCompat|null[0]
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Int) // androidx.compose.ui.backhandler/BackEventCompat.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Int){}[0]
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-geometry>
// Library unique name: <com.bitsycore.compose.ui:ui-geometry>
final class androidx.compose.ui.geometry/MutableRect { // androidx.compose.ui.geometry/MutableRect|null[0]
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // androidx.compose.ui.geometry/MutableRect.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
File diff suppressed because it is too large Load Diff
+75
View File
@@ -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"
)
}
}
+166
View File
@@ -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 ---- >>>>
@@ -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()
}
@@ -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,
)
}
@@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.Outline
// MARK: NativeShadowCanvas (render bridge)
// ==================
/** Drop-shadow bridge, same shape as NativeTextCanvas / NativePainterCanvas:
/** Drop-shadow bridge, same shape as NativePainterCanvas:
the render nodes call it when a layer carries shadowElevation > 0
(Modifier.shadow, m3 Surface tonal+shadow elevation). The outline is the
layer's resolved shape in LAYER-LOCAL pixels — the canvas is already
@@ -15,9 +15,7 @@ import androidx.compose.ui.graphics.Outline
paint BEFORE the layer's clip (it lives outside the bounds).
Implementations:
- SkiaBackedCanvas — a real Gaussian blur MaskFilter on the outline.
- Sdl3Canvas — cached shadow tiles (Sdl3ShadowCache) blitted via 9-slice
(the SDL 2D renderer has no shader/blur primitive). */
- SkiaBackedCanvas — a real Gaussian blur MaskFilter on the outline. */
interface NativeShadowCanvas {
fun drawDropShadow(
inOutline: Outline,
@@ -11,10 +11,10 @@ import androidx.compose.ui.geometry.RoundRect
a rounded-rect / circle OUTLINE (not just its bounding box).
The Skia backend does NOT implement this: Skia's `Canvas.clipPath` already
clips to a rounded path natively. The SDL3 backend has no path-clip primitive
(only a rectangular render clip), so `Sdl3Canvas` implements this by rendering
the clipped subtree into an offscreen target and masking it to the rounded shape
(lazily — see the pending-clip machinery there).
clips to a rounded path natively. This side-channel exists for any backend
that has no path-clip primitive (only a rectangular render clip) and must
emulate it by rendering the clipped subtree into an offscreen target and
masking it to the rounded shape.
The render nodes check `canvas is NativeShapeClipCanvas` when a layer has an
`Outline.Rounded` clip and route here; otherwise they fall back to `clipPath`. The pushed clip is scoped to the enclosing `Canvas.save()` /
@@ -0,0 +1,26 @@
package com.compose.sdl.res
// ==================
// MARK: Platform-environment seams
// ==================
/**
* System-environment seams the platform layer (the SDL module) installs at
* startup, so `:foundation` (`isSystemInDarkTheme`) and `:components-resources`
* (locale / theme resource qualifiers) can read them WITHOUT a dependency on the
* sdl3 cinterop — the cinterop lives in the SDL platform module, but these
* consumers sit below it. null until installed → callers fall back to the light
* theme / empty locale (the sensible headless default).
*/
/** True when the OS is in dark mode. Installed from SDL_GetSystemTheme. */
var systemThemeIsDarkProvider: (() -> Boolean)? = null
/** The OS preferred locale as (language, region) — e.g. ("fr", "FR"); either may
be empty. Installed from SDL_GetPreferredLocales. */
var preferredLocaleProvider: (() -> Pair<String, String>)? = null
/** OS preferred-locale BCP47 tags (language[-REGION]), most-preferred first —
backs androidx.compose.ui.text.intl Locale.current / LocaleList.current (in
:ui-text). Installed from SDL_GetPreferredLocales. */
var preferredLocaleTagsProvider: (() -> List<String>)? = null
@@ -34,8 +34,8 @@ fun resourceKindForPath(path: String): ResourceKind {
// ==================
/** Shared bridge the commonMain layout pass uses to size images and read raw
bytes. The active RenderBackend installs an implementation at startup
(parallel to currentTextMeasurer). Actual drawing is done by the renderer
bytes. The active RenderBackend installs an implementation at startup.
Actual drawing is done by the renderer
directly off the LayoutNode painter leaf, reusing the backend's decode
cache — so a decoded texture is created once and shared by measure + draw. */
interface ImageLoader {
@@ -56,6 +56,13 @@ private val kFallbackImageLoader = object : ImageLoader {
/** Installed by ComposeWindow from renderBackend.imageLoader. */
var currentImageLoader: ImageLoader = kFallbackImageLoader
/** Raw resource-bytes reader (reads an entry from the bundled data.kres). The
platform layer installs the impl (SDL's ResourceIO via SDL_GetBasePath); the
Skia font + image actuals read through this instead of the SDL loader directly,
so ui-graphics / ui-text carry no dependency on the sdl3 cinterop. null until
installed → callers treat that as "resource unavailable". */
var composeResourceReader: ((String) -> ByteArray?)? = null
// ==================
// MARK: painterResource
// ==================
@@ -23,7 +23,7 @@ import com.compose.sdl.graphics.NativePainterCanvas
the renderer, so this reduced type carries just the resource pointer.
[onDraw] casts the current Canvas to [NativePainterCanvas] (implemented by
Sdl3Canvas / SkiaCanvas) and delegates paint to the renderer's image cache.
SkiaBackedCanvas) and delegates paint to the renderer's image cache.
If the Canvas doesn't implement NativePainterCanvas (offscreen layer capture
for graphics-layer effects) the paint is skipped.
*/
@@ -45,8 +45,7 @@ class ResourcePainter internal constructor(
// translate + scaledSize, so we paint the resource FillBounds into the
// full DrawScope size. Alpha is threaded via applyAlpha.
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
resourcePath, kind, 0f, 0f, size.width, size.height,
androidx.compose.ui.layout.ContentScale.FillBounds, fAlpha,
resourcePath, kind, 0f, 0f, size.width, size.height, fAlpha,
)
}
}
@@ -11,4 +11,4 @@ import androidx.compose.ui.graphics.GraphicsContext
(the createRenderBackend trick). SDL → the project ProjectGraphicsContext (record/
replay via the SDL NativeRenderNode); Skia → upstream's SkiaGraphicsContext (owns a
skiko RenderNodeContext). See RENDERER.md §4 (B2). */
internal expect fun createGraphicsContext(): GraphicsContext
expect fun createGraphicsContext(): GraphicsContext
@@ -8,10 +8,9 @@ import androidx.compose.ui.graphics.ImageBitmap
/** Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
drawable [ImageBitmap]. The active renderer registers its implementation at
backend construction — the SDL backend goes through SDL3_image
(Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
backend construction — the Skia backend goes through Image.makeFromEncoded /
SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
hook under BOTH renderers (its skikoRendererMain reuses the SDL actuals).
hook.
Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
painterResource(Res.drawable.x) works without that module reaching into
@@ -24,7 +23,6 @@ interface EncodedImageDecoder {
resources pipeline on Dispatchers.Default workers. */
@kotlin.concurrent.Volatile
var encodedImageDecoder: EncodedImageDecoder? = null
internal set
/** Decode via the active renderer's registered decoder — null when no renderer
* has initialised yet or the bytes aren't a supported image. */
@@ -16,7 +16,7 @@ import kotlin.math.abs
// Builds the layer transform (translation / rotation / scale / camera about a
// pivot) into [matrix]. Matches the skiko copy so hit-testing agrees across legs.
internal fun prepareLayerTransformationMatrix(
fun prepareLayerTransformationMatrix(
matrix: Matrix,
pivotX: Float,
pivotY: Float,
@@ -17,11 +17,11 @@
// DERIVED (copy + edit) from compose-multiplatform-core -- MANUAL VENDOR, NON-IDEMPOTENT.
// VENDOR-BASE: compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt @ v1.12.0-beta03+dev4483
// ONLY change vs upstream: this class also implements the port draw contracts
// (NativeTextCanvas / NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so
// the port text engine (SkiaTextRenderer, P3.1 metrics) + resource-image cache + elevation
// shadows keep working on the Skia leg, which now draws through THIS canvas instead of the
// retired project SkiaCanvas. Bridged methods forward to `skiaLeafDrawer` using
// `internalSkiaCanvas`. See RENDERER.md (B6.1, section 4).
// (NativePainterCanvas / NativeShadowCanvas / NativeFinishableCanvas) so the
// resource-image cache + elevation shadows keep working on the Skia renderer,
// which draws through THIS canvas instead of the retired project SkiaCanvas.
// Bridged methods forward to `skiaLeafDrawer` using `internalSkiaCanvas`.
// See RENDERER.md (B6.1, section 4).
package androidx.compose.ui.graphics
@@ -103,7 +103,6 @@ var Canvas.alphaMultiplier: Float
internal class SkiaBackedCanvas(
internal val internalSkiaCanvas: SkCanvas,
) : Canvas,
com.compose.sdl.text.NativeTextCanvas,
com.compose.sdl.graphics.NativePainterCanvas,
com.compose.sdl.graphics.NativeShadowCanvas,
com.compose.sdl.graphics.NativeFinishableCanvas {
@@ -497,49 +496,6 @@ internal class SkiaBackedCanvas(
override fun finish() { /* Skia save/restore is balanced per call */ }
override fun drawNativeText(
inText: String,
inSpans: List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>?,
inX: Float,
inY: Float,
inBoxWidth: Float,
inBoxHeight: Float,
inColor: Color,
inFontSizePx: Int,
inTextAlign: androidx.compose.ui.text.style.TextAlign,
inSoftWrap: Boolean,
inFontFamily: String?,
inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?,
inBaseItalic: Boolean,
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
inLineHeightPx: Float,
inTrimFirstLine: Boolean,
) {
val drawer = skiaLeafDrawer ?: return
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
val vLineThrough = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.LineThrough) == true
drawer.textRenderer.drawText(
inCanvas = internalSkiaCanvas,
inText = inText,
inX = inX,
inY = inY,
inBoxWidth = inBoxWidth.toInt(),
inBoxHeight = inBoxHeight.toInt(),
inColor = inColor,
inFontSize = inFontSizePx,
inAlign = inTextAlign,
inSoftWrap = inSoftWrap,
inFontFamily = inFontFamily,
inFontVariations = inFontVariations,
inSpans = inSpans,
inBaseItalic = inBaseItalic,
inBaseUnderline = vUnderline,
inBaseLineThrough = vLineThrough,
inLineHeightPx = inLineHeightPx,
inTrimFirstLine = inTrimFirstLine,
)
}
override fun drawNativePainter(
inResourcePath: String,
inKind: com.compose.sdl.res.ResourceKind,
@@ -547,7 +503,6 @@ internal class SkiaBackedCanvas(
inY: Float,
inWidth: Float,
inHeight: Float,
inContentScale: androidx.compose.ui.layout.ContentScale,
inAlpha: Float,
) {
val drawer = skiaLeafDrawer ?: return
@@ -559,7 +514,6 @@ internal class SkiaBackedCanvas(
inY = inY,
inW = inWidth,
inH = inHeight,
inScale = inContentScale,
inAlpha = inAlpha,
)
}
@@ -10,4 +10,4 @@ import androidx.compose.ui.graphics.SkiaGraphicsContext
/** The Skia leg uses upstream's SkiaGraphicsContext, which owns a skiko
RenderNodeContext and creates upstream GraphicsLayer(skiko.RenderNode) instances.
See RENDERER.md (B6.2, section 4). */
internal actual fun createGraphicsContext(): GraphicsContext = SkiaGraphicsContext()
actual fun createGraphicsContext(): GraphicsContext = SkiaGraphicsContext()
@@ -18,7 +18,7 @@ import org.jetbrains.skia.Surface
Returns null if anything along the way fails: the Surface may not yet
exist (first frame), the GPU readback may fail, or bitmap.readPixels()
may not be exposed in this Skiko build. */
internal fun readBackBgra(inSurface: Surface?, inWidth: Int, inHeight: Int): Triple<Int, Int, ByteArray>? {
fun readBackBgra(inSurface: Surface?, inWidth: Int, inHeight: Int): Triple<Int, Int, ByteArray>? {
val vSurface = inSurface ?: return null
if (inWidth <= 0 || inHeight <= 0) return null
val vBitmap = Bitmap()
@@ -1,11 +1,8 @@
package com.compose.sdl.renderer.skia
import com.compose.sdl.*
import androidx.compose.ui.layout.ContentScale
import com.compose.sdl.res.AndroidVectorToSvg
import com.compose.sdl.res.ResourceKind
import androidx.compose.ui.unit.IntSize
import com.compose.sdl.res.composeResourceReader
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.Data
@@ -14,8 +11,6 @@ import org.jetbrains.skia.Paint
import org.jetbrains.skia.Rect
import org.jetbrains.skia.Surface
import org.jetbrains.skia.svg.SVGDOM
import kotlin.math.max
import kotlin.math.min
// ==================
// MARK: SkiaImageCache
@@ -31,7 +26,7 @@ import kotlin.math.min
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. */
internal class SkiaImageCache {
class SkiaImageCache {
// Value is null when a decode failed — cached to avoid retrying each frame.
private val fCache = HashMap<String, Image?>()
@@ -49,7 +44,7 @@ internal class SkiaImageCache {
}
private fun decode(inPath: String, inKind: ResourceKind): Image? {
val vBytes = loadComposeResourceBytes(inPath) ?: return null
val vBytes = composeResourceReader?.invoke(inPath) ?: return null
return when (inKind) {
ResourceKind.Raster -> runCatching { Image.makeFromEncoded(vBytes) }.getOrNull()
ResourceKind.Svg -> rasterizeSvg(vBytes)
@@ -73,7 +68,6 @@ internal class SkiaImageCache {
inY: Float,
inW: Float,
inH: Float,
inScale: ContentScale,
inAlpha: Float,
) {
if (inW <= 0f || inH <= 0f) return
@@ -85,30 +79,9 @@ internal class SkiaImageCache {
val vPaint = Paint()
vPaint.color = Color.makeARGB((inAlpha * 255f).toInt().coerceIn(0, 255), 255, 255, 255)
when (inScale) {
ContentScale.FillBounds -> {
inCanvas.drawImageRect(vImg, Rect.makeWH(vIw, vIh), Rect.makeXYWH(inX, inY, inW, inH), vPaint)
}
ContentScale.Crop -> {
val vScale = max(inW / vIw, inH / vIh)
val vSrcW = inW / vScale
val vSrcH = inH / vScale
val vSrc = Rect.makeXYWH((vIw - vSrcW) / 2f, (vIh - vSrcH) / 2f, vSrcW, vSrcH)
inCanvas.drawImageRect(vImg, vSrc, Rect.makeXYWH(inX, inY, inW, inH), vPaint)
}
else -> {
val vScale = when (inScale) {
ContentScale.Fit -> min(inW / vIw, inH / vIh)
ContentScale.Inside -> min(1f, min(inW / vIw, inH / vIh))
else -> 1f // None
}
val vDw = vIw * vScale
val vDh = vIh * vScale
val vDx = inX + (inW - vDw) / 2f
val vDy = inY + (inH - vDh) / 2f
inCanvas.drawImageRect(vImg, Rect.makeWH(vIw, vIh), Rect.makeXYWH(vDx, vDy, vDw, vDh), vPaint)
}
}
// 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()
}
@@ -0,0 +1,17 @@
package com.compose.sdl.renderer.skia
// ==================
// MARK: SkiaLeafDrawer — port text/image renderers behind the upstream canvas (B6.1)
// ==================
/** The Skia leg draws through upstream's `SkiaBackedCanvas` (real gradients/paint/
shader). Text now goes through the skiko paragraph engine (Paragraph.paint), so
this drawer only holds the resource-image cache (`SkiaImageCache`) that
`SkiaBackedCanvas`'s `NativePainterCanvas` contract forwards to. Set once by
`SkiaRenderBackend`; any `SkiaBackedCanvas` (frame, offscreen, or a GraphicsLayer
recording — B6.2) can then draw images onto its own `internalSkiaCanvas`. */
var skiaLeafDrawer: SkiaLeafDrawer? = null
class SkiaLeafDrawer(
val imageCache: SkiaImageCache,
)
@@ -9,18 +9,18 @@ import org.jetbrains.skia.Image
// MARK: SkiaEncodedImageDecoder
// ==================
/** Encoded-image decode hook impl for the Skia renderer — the counterpart of
Sdl3EncodedImageDecoder, 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.
/** 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
leg now uses upstream's SkiaBackedCanvas/SkiaImageAsset, so the offscreen +
renderer uses upstream's SkiaBackedCanvas/SkiaImageAsset, so the offscreen +
ImageBitmap-backed paths go through upstream's own actuals (the project
SkiaImageBitmap / SkiaOffscreenRenderer are retired). */
internal class SkiaEncodedImageDecoder : EncodedImageDecoder {
class SkiaEncodedImageDecoder : EncodedImageDecoder {
override fun decode(inBytes: ByteArray): ImageBitmap? {
if (inBytes.isEmpty()) return null
File diff suppressed because it is too large Load Diff
+74
View File
@@ -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"
)
}
}
+256
View File
@@ -0,0 +1,256 @@
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 ---- >>>>
@@ -20,9 +20,9 @@ object IconFont {
private val fFonts = mutableMapOf<String, ByteArray>()
// Families that render as single variable-axis glyphs (Material Symbols),
// vs ordinary text fonts. The SDL3 renderer routes only these through its
// single-glyph FreeType icon path; text families (e.g. a bundled
// monospace) must go through normal full-string rendering.
// vs ordinary text fonts — icon families are drawn one codepoint at a time,
// while text families (e.g. a bundled monospace) go through normal
// full-string rendering.
private val fIconFamilies = mutableSetOf<String>()
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
@@ -30,6 +30,10 @@ class NamedFont(
override val weight: FontWeight = FontWeight.Normal,
override val style: FontStyle = FontStyle.Normal,
val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
// Raw variable-font axis settings (Material Symbols FILL/wght/GRAD/opsz),
// threaded through the family so the skiko text engine can apply them via
// FontFamily.projectFontVariations() — icons render on the standard text path.
val axes: List<FontVariation.Setting>? = null,
) : Font {
override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
@@ -52,7 +56,8 @@ fun namedFontFamily(
name: String,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
): FontFamily = FontFamily(NamedFont(name, weight, style))
axes: List<FontVariation.Setting>? = null,
): FontFamily = FontFamily(NamedFont(name, weight, style, axes = axes))
/**
* Extract the renderer font-registry name for a `FontFamily`:
@@ -69,3 +74,9 @@ fun FontFamily?.projectFontName(): String? = when (this) {
if (name == "sans-serif") null else "generic:$name"
else -> null
}
/** Variable-font axis settings carried by a [namedFontFamily]'s [NamedFont]
* (Material Symbols FILL/wght/GRAD/opsz); null for every other family. The
* skiko text engine applies these on top of / instead of the paragraph weight. */
fun FontFamily?.projectFontVariations(): List<FontVariation.Setting>? =
((this as? FontListFontFamily)?.firstOrNull() as? NamedFont)?.axes
@@ -10,9 +10,8 @@ package com.compose.sdl.text
something the renderer can't deliver. Today the only consumer is the
Material Symbols install() path, which emits a one-shot warning when
variable-font axes won't be honoured. The Skia renderer sets
supportsFontVariations = true via Typeface.makeClone; the SDL3 renderer
leaves it false because SDL3_ttf 3.2 has no axis-set API (filename /
iostream / size / face / dpi only).
supportsFontVariations = true via Typeface.makeClone; a renderer that
couldn't honour variable-font axes would leave it false.
`supportsFontVariations` is null until a renderer initialises — code
that warns on install() ignores the null case so it doesn't fire when
@@ -0,0 +1,12 @@
package com.compose.sdl.text
// ==================
// MARK: Viewport size (project-only)
// ==================
/** Logical size of the window, set by the render loop each frame. Lets commonMain
composables (selection highlights cull per-line work, DropdownMenu flips/clamps
itself) read the viewport without a hard dependency on the window layer.
0 until first set — callers treat that as "viewport unknown". */
var currentViewportHeight: Int = 0
var currentViewportWidth: Int = 0
@@ -20,7 +20,7 @@ import androidx.compose.ui.text.input.TextFieldValue
SDL_TEXT_INPUT_EVENT), replace this with a real implementation.
*/
@Suppress("DEPRECATION")
internal object NoOpPlatformTextInputService : PlatformTextInputService {
object NoOpPlatformTextInputService : PlatformTextInputService {
override fun startInput(
value: TextFieldValue,
imeOptions: ImeOptions,
@@ -7,22 +7,52 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
// ==================
// MARK: ParagraphIntrinsics + Paragraph factory actuals (SDL)
// MARK: ParagraphIntrinsics + Paragraph factory actuals (skiko engine)
// ==================
//
// The `Paragraph()` / `ParagraphIntrinsics()` factories (expects in vendored
// commonMain) resolve here to the skiko-backed engine. Because `:ui`'s native
// hierarchy is nativeMain -> skiko (inverted vs upstream skiko -> native), the
// real SkiaParagraph lives in the child skiko source set; these parent-level
// actuals bridge to it through `makeSkiaParagraph` / `paragraphIntrinsicWidths`,
// whose actuals sit in skikoRendererMain (compiled into both the official-skiko
// and mingw-fork siblings). Same pattern as createRenderBackend.
/** Carries text+style for the intrinsics-based Paragraph factories; intrinsic widths come from a
throwaway unbounded SdlParagraph. `density` here is the LocalDensity scalar (dpr on Retina); it
converts sp → pixels so the intrinsic widths land in the same pixel space the layout tree measures
itself in. */
internal class SdlParagraphIntrinsics(
/** Construct the nativeMain [SkiaParagraph] over a skiko-backed ops seam. */
private fun makeSkiaParagraph(
text: String,
style: TextStyle,
width: Float,
maxLines: Int,
ellipsize: Boolean,
density: Float,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
): Paragraph = SkiaParagraph(
text, style, width,
buildParagraphOps(text, style, width, maxLines, ellipsize, density, spanStyles),
)
/** [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
widths come from an unbounded skiko layout. `density` is the LocalDensity
scalar (dpr on Retina): it converts sp → pixels so intrinsic widths land in
the same physical-pixel space the layout tree measures in. */
internal class NativeParagraphIntrinsics(
val paragraphText: String,
val paragraphStyle: TextStyle,
val density: Float,
val spanStyles: List<AnnotatedString.Range<SpanStyle>> = emptyList(),
) : ParagraphIntrinsics {
private val probe = SdlParagraph(paragraphText, paragraphStyle, Float.POSITIVE_INFINITY, Int.MAX_VALUE, density, spanStyles)
override val minIntrinsicWidth: Float = probe.minIntrinsicWidth
override val maxIntrinsicWidth: Float = probe.maxIntrinsicWidth
private val widths = paragraphIntrinsicWidths(paragraphText, paragraphStyle, density, spanStyles)
override val minIntrinsicWidth: Float = widths[0]
override val maxIntrinsicWidth: Float = widths[1]
override val hasStaleResolvedFonts: Boolean = false
}
@@ -39,7 +69,7 @@ actual fun ParagraphIntrinsics(
placeholders: List<AnnotatedString.Range<Placeholder>>,
density: Density,
resourceLoader: Font.ResourceLoader,
): ParagraphIntrinsics = SdlParagraphIntrinsics(text, style, density.density, spanStyles)
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles)
actual fun ParagraphIntrinsics(
text: String,
@@ -48,7 +78,7 @@ actual fun ParagraphIntrinsics(
placeholders: List<AnnotatedString.Range<Placeholder>>,
density: Density,
fontFamilyResolver: FontFamily.Resolver,
): ParagraphIntrinsics = SdlParagraphIntrinsics(text, style, density.density, spanStyles)
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, spanStyles)
actual fun ParagraphIntrinsics(
text: String,
@@ -57,7 +87,7 @@ actual fun ParagraphIntrinsics(
density: Density,
fontFamilyResolver: FontFamily.Resolver,
placeholders: List<AnnotatedString.Range<Placeholder>>,
): ParagraphIntrinsics = SdlParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
actual fun ParagraphIntrinsics(
text: String,
@@ -67,12 +97,11 @@ actual fun ParagraphIntrinsics(
fontFamilyResolver: FontFamily.Resolver,
placeholders: List<AnnotatedString.Range<Placeholder>>,
softWrap: Boolean,
): ParagraphIntrinsics = SdlParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
): ParagraphIntrinsics = NativeParagraphIntrinsics(text, style, density.density, annotations.filterSpanStyles())
// AnnotatedString.Annotation is a sealed interface with subtypes SpanStyle,
// ParagraphStyle, LinkAnnotation, StringAnnotation, TtsAnnotation, etc. Only
// SpanStyle contributes to glyph rendering — the paint path knows how to
// interpret it via drawNativeText's `inSpans` argument.
// AnnotatedString.Annotation is a sealed interface (SpanStyle, ParagraphStyle,
// LinkAnnotation, StringAnnotation, TtsAnnotation, …). Only SpanStyle affects
// glyph layout/painting.
@Suppress("UNCHECKED_CAST")
private fun List<AnnotatedString.Range<out AnnotatedString.Annotation>>.filterSpanStyles():
List<AnnotatedString.Range<SpanStyle>> =
@@ -91,7 +120,7 @@ actual fun Paragraph(
width: Float,
density: Density,
resourceLoader: Font.ResourceLoader,
): Paragraph = SdlParagraph(text, style, width, maxLines, density.density, spanStyles)
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles)
actual fun Paragraph(
text: String,
@@ -103,7 +132,7 @@ actual fun Paragraph(
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
ellipsis: Boolean,
): Paragraph = SdlParagraph(text, style, width, maxLines, density.density, spanStyles)
): Paragraph = makeSkiaParagraph(text, style, width, maxLines, ellipsis, density.density, spanStyles)
actual fun Paragraph(
text: String,
@@ -115,7 +144,7 @@ actual fun Paragraph(
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
ellipsis: Boolean,
): Paragraph = SdlParagraph(text, style, widthFrom(constraints), maxLines, density.density, spanStyles)
): Paragraph = makeSkiaParagraph(text, style, widthFrom(constraints), maxLines, ellipsis, density.density, spanStyles)
actual fun Paragraph(
text: String,
@@ -127,7 +156,9 @@ actual fun Paragraph(
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
overflow: TextOverflow,
): Paragraph = SdlParagraph(text, style, widthFrom(constraints), maxLines, density.density, spanStyles)
): Paragraph = makeSkiaParagraph(
text, style, widthFrom(constraints), maxLines, overflow == TextOverflow.Ellipsis, density.density, spanStyles,
)
actual fun Paragraph(
paragraphIntrinsics: ParagraphIntrinsics,
@@ -135,8 +166,8 @@ actual fun Paragraph(
ellipsis: Boolean,
width: Float,
): Paragraph {
val vI = paragraphIntrinsics as SdlParagraphIntrinsics
return SdlParagraph(vI.paragraphText, vI.paragraphStyle, width, maxLines, vI.density, vI.spanStyles)
val i = paragraphIntrinsics as NativeParagraphIntrinsics
return makeSkiaParagraph(i.paragraphText, i.paragraphStyle, width, maxLines, ellipsis, i.density, i.spanStyles)
}
actual fun Paragraph(
@@ -145,8 +176,8 @@ actual fun Paragraph(
maxLines: Int,
ellipsis: Boolean,
): Paragraph {
val vI = paragraphIntrinsics as SdlParagraphIntrinsics
return SdlParagraph(vI.paragraphText, vI.paragraphStyle, widthFrom(constraints), maxLines, vI.density, vI.spanStyles)
val i = paragraphIntrinsics as NativeParagraphIntrinsics
return makeSkiaParagraph(i.paragraphText, i.paragraphStyle, widthFrom(constraints), maxLines, ellipsis, i.density, i.spanStyles)
}
actual fun Paragraph(
@@ -155,6 +186,9 @@ actual fun Paragraph(
maxLines: Int,
overflow: TextOverflow,
): Paragraph {
val vI = paragraphIntrinsics as SdlParagraphIntrinsics
return SdlParagraph(vI.paragraphText, vI.paragraphStyle, widthFrom(constraints), maxLines, vI.density, vI.spanStyles)
val i = paragraphIntrinsics as NativeParagraphIntrinsics
return makeSkiaParagraph(
i.paragraphText, i.paragraphStyle, widthFrom(constraints), maxLines,
overflow == TextOverflow.Ellipsis, i.density, i.spanStyles,
)
}
@@ -0,0 +1,305 @@
package androidx.compose.ui.text
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import kotlin.math.floor
// ==================
// MARK: SkiaParagraph — nativeMain Paragraph over a skiko-backed ops seam
// ==================
//
// `Paragraph` is a sealed interface whose actual is declared in nativeMain, so
// its implementers must live here (skiko-tied code in the child skikoRenderer
// source set counts as a "different module" for the sealed check). This class
// therefore holds no skiko types: it drives the real skiko skparagraph through
// [NativeParagraphOps] (impl in skikoRendererMain), exchanging plain data
// ([TextBoxData] / [LineMetricData] / [RectData]). Cursor/selection/line-metric
// math is adapted from upstream `SkiaParagraph.skiko.kt` (@ CMP core v1.12.x).
/** Plain mirror of a skiko `TextBox` (a glyph-run rect + its direction). */
internal class TextBoxData(
val left: Float, val top: Float, val right: Float, val bottom: Float, val isRtl: Boolean,
)
/** Plain mirror of a skiko paragraph `LineMetrics`. */
internal class LineMetricData(
val startIndex: Int,
val endIndex: Int,
val endExcludingWhitespaces: Int,
val endIncludingNewline: Int,
val isHardBreak: Boolean,
val ascent: Double,
val descent: Double,
val baseline: Double,
val left: Double,
val right: Double,
val width: Double,
val height: Double,
val lineNumber: Int,
)
/** Plain mirror of a Compose/skiko rect. */
internal class RectData(val left: Float, val top: Float, val right: Float, val bottom: Float) {
fun toComposeRect() = Rect(left, top, right, bottom)
}
/**
* Skiko-free view of one laid-out skiko paragraph. Implemented in
* skikoRendererMain (SkiaParagraphOps) where skiko is on the classpath; all
* exchange types are plain so this interface can live in nativeMain beside the
* sealed [Paragraph]. [rebuildAndPaint] re-lays-out with paint-time overrides
* (Compose paints text with the resolved colour/shadow/decoration) then draws.
*/
internal interface NativeParagraphOps {
val height: Float
val lineNumber: Int
val minIntrinsicWidth: Float
val maxIntrinsicWidth: Float
val didExceedMaxLines: Boolean
val alphabeticBaseline: Float
val defaultAscentPx: Float // positive: -fontMetrics.ascent
val defaultDescentPx: Float
fun lineMetrics(): List<LineMetricData>
fun placeholderRects(): List<RectData?>
fun getRectsForRange(start: Int, end: Int, useMaxHeight: Boolean): List<TextBoxData>
fun glyphPositionAtCoordinate(x: Float, y: Float): Int
fun wordBoundary(offset: Int): IntArray // [start, end]
fun rebuildAndPaint(canvas: Canvas, color: Color, shadow: Shadow?, decoration: TextDecoration?)
}
/** Bridge to the skiko ops impl (actual in skikoRendererMain). */
internal expect fun buildParagraphOps(
text: String,
style: TextStyle,
width: Float,
maxLines: Int,
ellipsize: Boolean,
density: Float,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
): NativeParagraphOps
internal class SkiaParagraph(
private val text: String,
private val style: TextStyle,
private val widthConstraint: Float,
private val ops: NativeParagraphOps,
) : Paragraph {
private val textDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr
override val width: Float
get() = if (widthConstraint.isFinite()) widthConstraint else ops.maxIntrinsicWidth
override val height: Float get() = ops.height
override val minIntrinsicWidth: Float get() = ops.minIntrinsicWidth
override val maxIntrinsicWidth: Float get() = ops.maxIntrinsicWidth
override val firstBaseline: Float get() = lineMetrics.firstOrNull()?.baseline?.toFloat() ?: 0f
override val lastBaseline: Float get() = lineMetrics.lastOrNull()?.baseline?.toFloat() ?: 0f
override val didExceedMaxLines: Boolean get() = ops.didExceedMaxLines
override val lineCount: Int
get() = if (text == "" || ops.lineNumber < 1) 1 else ops.lineNumber
override val placeholderRects: List<Rect?>
get() = ops.placeholderRects().map { it?.toComposeRect() }
override fun getPathForRange(start: Int, end: Int): Path {
val path = Path()
for (b in ops.getRectsForRange(start, end, useMaxHeight = true)) {
path.addRect(Rect(b.left, b.top, b.right, b.bottom))
}
return path
}
override fun getCursorRect(offset: Int): Rect {
val horizontal = getHorizontalPosition(offset, true)
val line = lineMetricsForOffset(offset) ?: return Rect(horizontal, 0f, horizontal, ops.defaultDescentPx)
val isNewEmptyLine = offset - 1 == line.startIndex && offset == text.length
val asc = line.ascent.let { if (isNewEmptyLine) it.coerceAtMost(ops.defaultAscentPx.toDouble()) else it }
val desc = line.descent.let { if (isNewEmptyLine) it.coerceAtMost(ops.defaultDescentPx.toDouble()) else it }
return Rect(horizontal, (line.baseline - asc).toFloat(), horizontal, (line.baseline + desc).toFloat())
}
override fun getLineLeft(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.left?.toFloat() ?: 0f
override fun getLineRight(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.right?.toFloat() ?: 0f
override fun getLineTop(lineIndex: Int): Float =
lineMetrics.getOrNull(lineIndex)?.let { floor((it.baseline - it.ascent).toFloat()) } ?: 0f
override fun getLineBottom(lineIndex: Int): Float =
lineMetrics.getOrNull(lineIndex)?.let { floor((it.baseline + it.descent).toFloat()) } ?: 0f
override fun getLineBaseline(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.baseline?.toFloat() ?: 0f
override fun getLineHeight(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.height?.toFloat() ?: 0f
override fun getLineWidth(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.width?.toFloat() ?: 0f
override fun getLineStart(lineIndex: Int): Int = lineMetrics.getOrNull(lineIndex)?.startIndex ?: 0
override fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int {
val metrics = lineMetrics.getOrNull(lineIndex) ?: return 0
return if (visibleEnd) {
if (lineIndex > 0 && metrics.startIndex < lineMetrics[lineIndex - 1].endIndex) metrics.endIndex
else if (metrics.startIndex < text.length && text[metrics.startIndex] == '\n') metrics.startIndex
else metrics.endExcludingWhitespaces
} else metrics.endIndex
}
override fun isLineEllipsized(lineIndex: Int): Boolean = false
override fun getLineForOffset(offset: Int): Int = when {
offset < 0 -> 0
offset > text.length -> lineCount - 1
else -> lineMetricsForOffset(offset)?.lineNumber ?: 0
}
override fun getLineForVerticalPosition(vertical: Float): Int =
lineMetricsForVerticalPosition(vertical)?.lineNumber ?: 0
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float {
val prevBox = getBoxBackwardByOffset(offset)
val nextBox = getBoxForwardByOffset(offset)
val isRtl = textDirection == ResolvedTextDirection.Rtl
return when {
prevBox == null && nextBox == null -> getAlignedStartingPosition(isRtl)
prevBox == null -> nextBox!!.cursorHorizontalPosition(true)
nextBox == null -> prevBox.cursorHorizontalPosition()
nextBox.isRtl == prevBox.isRtl -> nextBox.cursorHorizontalPosition(true)
!isRtl && !prevBox.isRtl -> nextBox.cursorHorizontalPosition(opposite = true)
isRtl && prevBox.isRtl -> nextBox.cursorHorizontalPosition(opposite = true)
usePrimaryDirection -> prevBox.cursorHorizontalPosition()
else -> nextBox.cursorHorizontalPosition(true)
}
}
private fun getAlignedStartingPosition(isRtl: Boolean): Float = when (style.textAlign) {
TextAlign.Left -> 0f
TextAlign.Right -> width
TextAlign.Center -> width / 2
TextAlign.Start -> if (isRtl) width else 0f
TextAlign.End -> if (isRtl) 0f else width
else -> 0f
}
override fun getParagraphDirection(offset: Int): ResolvedTextDirection = textDirection
override fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
if (getBoxForwardByOffset(offset)?.isRtl == true) ResolvedTextDirection.Rtl else ResolvedTextDirection.Ltr
override fun getOffsetForPosition(position: Offset): Int {
val glyphPosition = ops.glyphPositionAtCoordinate(position.x, position.y)
val expectedLine = lineMetricsForVerticalPosition(position.y) ?: return glyphPosition
if (position.x > expectedLine.left && position.x < expectedLine.right) return glyphPosition
val isNotEmptyLine = expectedLine.startIndex < expectedLine.endIndex
val rects = if (isNotEmptyLine) ops.getRectsForRange(
expectedLine.startIndex,
if (expectedLine.isHardBreak) expectedLine.endIndex else expectedLine.endIndex - 1,
useMaxHeight = false,
) else null
val leftX = rects?.firstOrNull()?.left ?: expectedLine.left.toFloat()
val rightX = rects?.lastOrNull()?.right ?: expectedLine.right.toFloat()
if (leftX == rightX) return glyphPosition
return when {
position.x <= leftX -> ops.glyphPositionAtCoordinate(leftX + 1f, position.y)
position.x >= rightX -> ops.glyphPositionAtCoordinate(rightX - 1f, position.y)
else -> glyphPosition
}
}
override fun getRangeForRect(rect: Rect, granularity: TextGranularity, inclusionStrategy: TextInclusionStrategy): TextRange =
TextRange.Zero
override fun getBoundingBox(offset: Int): Rect {
val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)
return box?.let { Rect(it.left, it.top, it.right, it.bottom) } ?: Rect(0f, 0f, 0f, 0f)
}
override fun fillBoundingBoxes(range: TextRange, array: FloatArray, arrayStart: Int) {
// TODO(CMP-720): not supported by skiko paragraph yet.
}
override fun getWordBoundary(offset: Int): TextRange {
if (offset < text.length && text[offset].isWhitespace() || offset == text.length) {
return if (offset > 0 && !text[offset - 1].isWhitespace()) {
val b = ops.wordBoundary(offset - 1); TextRange(b[0], b[1])
} else TextRange(offset, offset)
}
val b = ops.wordBoundary(offset)
return TextRange(b[0], b[1])
}
override fun paint(canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?) {
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
}
override fun paint(
canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?,
drawStyle: DrawStyle?, blendMode: BlendMode,
) {
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
}
override fun paint(
canvas: Canvas, brush: Brush, alpha: Float, shadow: Shadow?, textDecoration: TextDecoration?,
drawStyle: DrawStyle?, blendMode: BlendMode,
) {
val color = (brush as? SolidColor)?.value?.let {
if (alpha.isFinite()) it.copy(alpha = it.alpha * alpha) else it
} ?: style.color
ops.rebuildAndPaint(canvas, color, shadow ?: style.shadow, textDecoration ?: style.textDecoration)
}
// ============
// Line-metric helpers
private val lineMetrics: List<LineMetricData> by lazy { ops.lineMetrics() }
private fun lineMetricsForOffset(offset: Int): LineMetricData? =
if (offset in 0..text.length) lineMetrics.binarySearchFirstMatchingOrLast { offset < it.endIncludingNewline } else null
private fun lineMetricsForVerticalPosition(vertical: Float): LineMetricData? =
lineMetrics.binarySearchFirstMatchingOrLast { vertical < it.baseline + it.descent }
private fun getBoxForwardByOffset(offset: Int): TextBoxData? {
if (offset !in 0..text.length) return null
var to = offset + 1
while (to <= text.length) {
ops.getRectsForRange(offset, to, useMaxHeight = false).firstOrNull()?.let { return it }
to += 1
}
return null
}
private fun getBoxBackwardByOffset(offset: Int, end: Int = offset): TextBoxData? {
if (offset !in 0..text.length) return null
var from = offset - 1
while (from >= 0) {
val box = ops.getRectsForRange(from, end, useMaxHeight = false).firstOrNull()
when {
box == null -> from -= 1
text[from] == '\n' -> {
val bottom = box.bottom + box.bottom - box.top
return TextBoxData(0f, box.bottom, 0f, bottom, box.isRtl)
}
else -> return box
}
}
return null
}
}
private fun TextBoxData.cursorHorizontalPosition(opposite: Boolean = false): Float =
if (isRtl) (if (opposite) right else left) else (if (opposite) left else right)
private inline fun <T> List<T>.binarySearchFirstMatchingOrLast(crossinline predicate: (T) -> Boolean): T? {
if (isEmpty()) return null
val index = binarySearch { if (predicate(it)) 1 else -1 }
return this[(-index - 1).coerceAtMost(lastIndex)]
}
@@ -10,7 +10,7 @@ package androidx.compose.ui.text.font
* that-isn't-a-FontListFontFamily" adapter. Upstream's skiko actual routes
* `Default`, `SansSerif`, `Serif`, `Monospace`, `Cursive`, and `LoadedFontFamily`
* through `SkiaFontLoader` to produce a real `SkTypeface`. Our text renderer
* (SdlParagraph / SkiaParagraph) reads `TextStyle.fontFamily` and
* (SkiaParagraph) reads `TextStyle.fontFamily` and
* [com.compose.sdl.text.NamedFont] directly to look the family up
* against its registered font table, so the resolver's typeface is never
* consulted. A no-op adapter (returns an [TypefaceResult.Immutable] wrapping
@@ -64,4 +64,4 @@ actual class Locale actual constructor(languageTag: String) {
// The OS's most-preferred locale (SDL); en-US before SDL init or on a host that
// reports none. Cheap: the tag list is cached after the first non-empty read.
private fun systemCurrentLocale(): Locale =
com.compose.sdl.text.systemPreferredLocaleTags().firstOrNull()?.let(::Locale) ?: Locale("en-US")
com.compose.sdl.res.preferredLocaleTagsProvider?.invoke()?.firstOrNull()?.let(::Locale) ?: Locale("en-US")
@@ -17,7 +17,7 @@ internal actual fun createPlatformLocaleDelegate(): PlatformLocaleDelegate =
object : PlatformLocaleDelegate {
override val current: LocaleList
get() {
val tags = com.compose.sdl.text.systemPreferredLocaleTags()
val tags = com.compose.sdl.res.preferredLocaleTagsProvider?.invoke().orEmpty()
return if (tags.isEmpty()) LocaleList(listOf(Locale("en-US")))
else LocaleList(tags.map(::Locale))
}
@@ -0,0 +1,88 @@
package com.compose.sdl.renderer.skia
import androidx.compose.ui.text.font.FontVariation as ComposeFontVariation
import com.compose.sdl.icons.IconFont
import com.compose.sdl.res.composeResourceReader
import org.jetbrains.skia.Data
import org.jetbrains.skia.FontMgr
import org.jetbrains.skia.FontVariation as SkiaFontVariation
import org.jetbrains.skia.Typeface
import org.jetbrains.skia.paragraph.FontCollection
import org.jetbrains.skia.paragraph.TypefaceFontProvider
// ==================
// MARK: SkiaFonts — font-resolution bridge for the skiko paragraph engine
// ==================
/**
* Turns the port's `(family-name, variable-axis)` font model into skiko
* [Typeface]s for the paragraph engine. Families come from
* [com.compose.sdl.text.NamedFont.projectFontName] (a bundled default, an
* [IconFont]-registered text/icon family, or `generic:*`); variable axes
* (Material Symbols `FILL`/`wght`/… and paragraph `FontWeight`→`wght`) are
* applied via [Typeface.makeClone].
*
* Every resolved typeface is registered in a [TypefaceFontProvider] under a
* UNIQUE alias, and [resolve] hands that alias back so the caller sets both
* `TextStyle.typeface` AND `TextStyle.fontFamilies = [alias]`. That matters:
* skiko's shaper maps codepoints→glyphs through `fontFamilies` (the
* FontCollection), so an icon font MUST be reachable by its alias — otherwise a
* bare `typeface` is ignored and private-use icon codepoints render as tofu.
* (Mirrors upstream FontCache: register alias + set fontFamilies + typeface.)
*/
internal object SkiaFonts {
private const val DEFAULT_ALIAS = "Noto Sans"
private val fontMgr = FontMgr.default
private val provider = TypefaceFontProvider()
/** Bundled default (NotoSans from data.kres); the fallback for every unresolved family. */
val defaultTypeface: Typeface? =
composeResourceReader?.invoke("font/NotoSans.ttf")
?.let { fontMgr.makeFromData(Data.makeFromBytes(it), 0) }
?.also { provider.registerTypeface(it, DEFAULT_ALIAS) }
/** Shared collection: the alias provider first (bundled + icon + varied fonts),
then the system FontMgr for glyph fallback (CJK/emoji where present). */
val fontCollection: FontCollection = FontCollection().apply {
setDefaultFontManager(fontMgr)
setAssetFontManager(provider)
}
// family name (null = default) -> base typeface
private val baseCache = mutableMapOf<String?, Typeface?>()
// resolve key -> (typeface, registered alias)
private val resolveCache = mutableMapOf<String, Pair<Typeface?, String>>()
private val registered = mutableSetOf(DEFAULT_ALIAS)
private fun baseTypeface(family: String?): Typeface? =
baseCache.getOrPut(family) {
val bytes = family?.let { IconFont.bytesFor(it) }
if (bytes != null) fontMgr.makeFromData(Data.makeFromBytes(bytes), 0) else defaultTypeface
}
private fun variationsKey(variations: List<ComposeFontVariation.Setting>): String =
variations.sortedBy { it.axisName }.joinToString(",") { "${it.axisName}=${it.toVariationValue(null)}" }
/** Resolve a family + optional variable-axis settings to a concrete typeface AND
a provider alias (register-on-first-use). Set `fontFamilies = [alias]` and
`typeface = first` on the skiko TextStyle. */
fun resolve(family: String?, variations: List<ComposeFontVariation.Setting>?): Pair<Typeface?, String> {
if (family == null && variations.isNullOrEmpty()) return defaultTypeface to DEFAULT_ALIAS
val varKey = variations?.takeUnless { it.isEmpty() }?.let { variationsKey(it) } ?: ""
val key = "${family ?: ""}#$varKey"
return resolveCache.getOrPut(key) {
val base = baseTypeface(family) ?: defaultTypeface
val typeface = if (varKey.isEmpty() || base == null) base
else runCatching {
base.makeClone(variations!!.map { SkiaFontVariation(it.axisName, it.toVariationValue(null)) }.toTypedArray())
}.getOrDefault(base)
val alias = if (family == null && varKey.isEmpty()) DEFAULT_ALIAS else "cdn-font:$key"
if (typeface != null && alias !in registered) {
provider.registerTypeface(typeface, alias)
registered += alias
}
typeface to alias
}
}
}
@@ -0,0 +1,256 @@
package androidx.compose.ui.text
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.skiaCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.isSpecified
import com.compose.sdl.renderer.skia.SkiaFonts
import com.compose.sdl.text.projectFontName
import com.compose.sdl.text.projectFontVariations
import org.jetbrains.skia.Font as SkFont
import org.jetbrains.skia.FontStyle as SkFontStyle
import org.jetbrains.skia.paragraph.Alignment as SkAlignment
import org.jetbrains.skia.paragraph.DecorationLineStyle as SkDecorationLineStyle
import org.jetbrains.skia.paragraph.DecorationStyle as SkDecorationStyle
import org.jetbrains.skia.paragraph.Direction as SkDirection
import org.jetbrains.skia.paragraph.Paragraph as SkParagraph
import org.jetbrains.skia.paragraph.ParagraphBuilder as SkParagraphBuilder
import org.jetbrains.skia.paragraph.ParagraphStyle
import org.jetbrains.skia.paragraph.RectHeightMode
import org.jetbrains.skia.paragraph.RectWidthMode
import org.jetbrains.skia.paragraph.Shadow as SkShadow
import org.jetbrains.skia.paragraph.TextStyle as SkTextStyle
// ==================
// MARK: SkiaParagraphOps — skiko skparagraph impl behind the nativeMain seam
// ==================
//
// Lives in the skiko source set (skiko on classpath) and implements the
// skiko-free [NativeParagraphOps] the nativeMain [SkiaParagraph] drives. Builds
// a skiko `org.jetbrains.skia.paragraph` (HarfBuzz shaping + skunicode bidi +
// FontCollection fallback) from Compose style/spans and exposes plain-typed
// queries. Compose style/span -> skiko conversion is a reduced local version of
// upstream ParagraphBuilder.skiko.kt, keeping the port's family/variable-axis
// font model (SkiaFonts) so icons / monospace / custom fonts are unchanged.
private const val INTRINSIC_WIDTH = 100_000f
internal class SkiaParagraphOps(
private val text: String,
private val style: TextStyle,
private val widthConstraint: Float,
private val maxLines: Int,
private val density: Float,
private val spanStyles: List<AnnotatedString.Range<SpanStyle>>,
private val ellipsize: Boolean,
) : NativeParagraphOps {
private val fontPx: Float =
((if (style.fontSize.isSpecified) style.fontSize.value else 14f) * density).coerceAtLeast(1f)
private val baseFamily: String? = style.fontFamily.projectFontName()
// Icon families (Material Symbols) carry explicit axes on the family; ordinary
// text derives only the wght axis from the paragraph FontWeight.
private val baseVariations: List<FontVariation.Setting>? =
style.fontFamily.projectFontVariations()
?: style.fontWeight?.let { listOf(FontVariation.weight(it.weight)) }
private val baseTypeface = SkiaFonts.resolve(baseFamily, baseVariations).first
private val defaultFont = SkFont(baseTypeface ?: SkiaFonts.defaultTypeface, fontPx)
private val layoutWidth: Float =
if (widthConstraint.isFinite() && widthConstraint > 0f) widthConstraint else INTRINSIC_WIDTH
private var paragraph: SkParagraph = build(style.color, style.shadow, style.textDecoration)
// ============
// NativeParagraphOps
override val height: Float get() = paragraph.height
override val lineNumber: Int get() = paragraph.lineNumber
override val minIntrinsicWidth: Float get() = paragraph.minIntrinsicWidth
override val maxIntrinsicWidth: Float get() = paragraph.maxIntrinsicWidth
override val didExceedMaxLines: Boolean get() = paragraph.didExceedMaxLines()
override val alphabeticBaseline: Float get() = paragraph.alphabeticBaseline
override val defaultAscentPx: Float get() = -defaultFont.metrics.ascent
override val defaultDescentPx: Float get() = defaultFont.metrics.descent
override fun lineMetrics(): List<LineMetricData> {
val metrics = if (text.isEmpty()) emptyList() else paragraph.lineMetrics.toList()
if (metrics.isEmpty()) {
val a = -defaultFont.metrics.ascent.toDouble()
val d = defaultFont.metrics.descent.toDouble()
return listOf(
LineMetricData(0, 0, 0, 0, true, a, d, paragraph.alphabeticBaseline.toDouble().takeIf { it > 0 } ?: a,
0.0, 0.0, 0.0, a + d, 0),
)
}
return metrics.map {
LineMetricData(
startIndex = it.startIndex, endIndex = it.endIndex,
endExcludingWhitespaces = it.endExcludingWhitespaces, endIncludingNewline = it.endIncludingNewline,
isHardBreak = it.isHardBreak, ascent = it.ascent, descent = it.descent, baseline = it.baseline,
left = it.left, right = it.left + it.width, width = it.width, height = it.height,
lineNumber = it.lineNumber,
)
}
}
override fun placeholderRects(): List<RectData?> =
paragraph.rectsForPlaceholders.map { RectData(it.rect.left, it.rect.top, it.rect.right, it.rect.bottom) }
override fun getRectsForRange(start: Int, end: Int, useMaxHeight: Boolean): List<TextBoxData> =
paragraph.getRectsForRange(
start, end,
if (useMaxHeight) RectHeightMode.MAX else RectHeightMode.STRUT,
RectWidthMode.TIGHT,
).map { TextBoxData(it.rect.left, it.rect.top, it.rect.right, it.rect.bottom, it.direction == SkDirection.RTL) }
override fun glyphPositionAtCoordinate(x: Float, y: Float): Int =
paragraph.getGlyphPositionAtCoordinate(x, y).position
override fun wordBoundary(offset: Int): IntArray =
paragraph.getWordBoundary(offset).let { intArrayOf(it.start, it.end) }
override fun rebuildAndPaint(canvas: Canvas, color: Color, shadow: Shadow?, decoration: TextDecoration?) {
paragraph = build(color, shadow, decoration)
paragraph.paint(canvas.skiaCanvas, 0f, 0f)
}
// ============
// Compose style/span -> skiko build
private fun build(color: Color, shadow: Shadow?, decoration: TextDecoration?): SkParagraph {
val baseStyle = makeTextStyle(baseFamily, baseVariations, fontPx, color, style.fontStyle, decoration, shadow)
val pStyle = ParagraphStyle().apply {
alignment = style.textAlign.toSkAlignment()
direction = if (style.textDirection == androidx.compose.ui.text.style.TextDirection.Rtl) SkDirection.RTL else SkDirection.LTR
textStyle = baseStyle
if (maxLines != Int.MAX_VALUE) {
maxLinesCount = maxLines
ellipsis = if (ellipsize) "…" else ""
}
}
val pb = SkParagraphBuilder(pStyle, SkiaFonts.fontCollection)
if (spanStyles.isEmpty()) {
pb.pushStyle(baseStyle)
pb.addText(text)
pb.popStyle()
} else {
appendWithSpans(pb, color, shadow, decoration)
}
return pb.build().also { it.layout(layoutWidth) }
}
private fun appendWithSpans(pb: SkParagraphBuilder, color: Color, shadow: Shadow?, decoration: TextDecoration?) {
val points = buildList {
add(0); add(text.length)
spanStyles.forEach { add(it.start.coerceIn(0, text.length)); add(it.end.coerceIn(0, text.length)) }
}.distinct().sorted()
for (i in 0 until points.size - 1) {
val segStart = points[i]
val segEnd = points[i + 1]
if (segStart >= segEnd) continue
val active = spanStyles.filter { it.start <= segStart && it.end >= segEnd }
pb.pushStyle(segmentStyle(color, shadow, decoration, active))
pb.addText(text.substring(segStart, segEnd))
pb.popStyle()
}
}
private fun segmentStyle(
color: Color, shadow: Shadow?, decoration: TextDecoration?,
active: List<AnnotatedString.Range<SpanStyle>>,
): SkTextStyle {
var family = baseFamily
var variations = baseVariations
var size = fontPx
var segColor = color
var fontStyle = style.fontStyle
var deco = decoration
active.forEach { range ->
val sp = range.item
if (sp.color.isSpecified) segColor = sp.color
sp.fontWeight?.let { variations = listOf(FontVariation.weight(it.weight)) }
if (sp.fontSize.isSpecified) size = (sp.fontSize.value * density).coerceAtLeast(1f)
sp.fontStyle?.let { fontStyle = it }
sp.textDecoration?.let { deco = it }
sp.fontFamily.projectFontName()?.let { family = it }
sp.fontFamily.projectFontVariations()?.let { variations = it }
}
return makeTextStyle(family, variations, size, segColor, fontStyle, deco, shadow)
}
private fun makeTextStyle(
family: String?, variations: List<FontVariation.Setting>?, sizePx: Float,
color: Color, fontStyle: FontStyle?, decoration: TextDecoration?, shadow: Shadow?,
): SkTextStyle {
val ts = SkTextStyle()
val argb = (if (color.isSpecified) color else Color.Black).toArgb()
ts.color = argb
ts.fontSize = sizePx
// Register + resolve to a provider alias so skiko's shaper maps codepoints
// through the exact typeface (icon fonts would otherwise fall back to
// Noto Sans and render private-use glyphs as tofu).
val (tf, alias) = SkiaFonts.resolve(family, variations)
tf?.let { ts.typeface = it }
ts.fontFamilies = arrayOf(alias)
if (fontStyle == FontStyle.Italic) ts.fontStyle = SkFontStyle.ITALIC
decoration?.takeUnless { it == TextDecoration.None }?.let {
ts.decorationStyle = SkDecorationStyle(
it.contains(TextDecoration.Underline), false, it.contains(TextDecoration.LineThrough),
false, argb, SkDecorationLineStyle.SOLID, 1f,
)
}
val letter = style.letterSpacing
if (letter.isSpecified) {
ts.letterSpacing = if (letter.isEm) sizePx * letter.value else letter.value * density
}
shadow?.takeUnless { it == Shadow.None }?.let {
ts.addShadow(SkShadow(it.color.toArgb(), it.offset.x, it.offset.y, it.blurRadius.toDouble()))
}
return ts
}
}
private fun TextAlign.toSkAlignment(): SkAlignment = when (this) {
TextAlign.Left -> SkAlignment.LEFT
TextAlign.Right -> SkAlignment.RIGHT
TextAlign.Center -> SkAlignment.CENTER
TextAlign.Justify -> SkAlignment.JUSTIFY
TextAlign.End -> SkAlignment.END
else -> SkAlignment.START
}
// ==================
// MARK: factory actuals (skikoRendererMain)
// ==================
/** Actual for the nativeMain `expect fun buildParagraphOps`; runs in the skiko
* source set (compiled into both the official-skiko and fork siblings). */
internal actual fun buildParagraphOps(
text: String,
style: TextStyle,
width: Float,
maxLines: Int,
ellipsize: Boolean,
density: Float,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
): NativeParagraphOps = SkiaParagraphOps(text, style, width, maxLines, density, spanStyles, ellipsize)
/** Actual for `expect fun paragraphIntrinsicWidths` — [min, max] from an
* unbounded layout. */
internal actual fun paragraphIntrinsicWidths(
text: String,
style: TextStyle,
density: Float,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
): FloatArray {
val ops = SkiaParagraphOps(text, style, Float.POSITIVE_INFINITY, Int.MAX_VALUE, density, spanStyles, false)
return floatArrayOf(ops.minIntrinsicWidth, ops.maxIntrinsicWidth)
}
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-tooling-preview>
// Library unique name: <com.bitsycore.compose.ui:ui-tooling-preview>
open annotation class androidx.compose.ui.tooling.preview/AndroidUiMode : kotlin/Annotation { // androidx.compose.ui.tooling.preview/AndroidUiMode|null[0]
constructor <init>() // androidx.compose.ui.tooling.preview/AndroidUiMode.<init>|<init>(){}[0]
}
+1 -1
View File
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-unit>
// Library unique name: <com.bitsycore.compose.ui:ui-unit>
open annotation class androidx.compose.ui.unit/ExperimentalUnitApi : kotlin/Annotation { // androidx.compose.ui.unit/ExperimentalUnitApi|null[0]
constructor <init>() // androidx.compose.ui.unit/ExperimentalUnitApi.<init>|<init>(){}[0]
}
+1 -1
View File
@@ -5,7 +5,7 @@
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.compose.sdl:ui-util>
// Library unique name: <com.bitsycore.compose.ui:ui-util>
open annotation class androidx.compose.ui/ExperimentalComposeUiApi : kotlin/Annotation { // androidx.compose.ui/ExperimentalComposeUiApi|null[0]
constructor <init>() // androidx.compose.ui/ExperimentalComposeUiApi.<init>|<init>(){}[0]
}
File diff suppressed because it is too large Load Diff
+51 -176
View File
@@ -1,25 +1,23 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// :core — the renderer-agnostic Compose base + both renderer pipelines.
// :ui — the Compose core + the Skia rendering pipeline + the SDL bridges
// (all native targets). androidx.compose.ui.graphics.* / .text.* were split out
// to :ui-graphics / :ui-text, and the sdl3 cinterop now lives in :sdl-core;
// :ui depends on both.
//
// Source-set hierarchy:
// commonMain
// └── nativeMain (vendored .native.kt + project SDL3 wrappers)
// ├── skikoRendererMain (Skia drawing pipeline; Skiko on classpath)
// │ ├── skikoRendererMacosMain (macOS-only Skia actuals — Metal bridge)
// │ └── skikoRendererLinuxMain (Linux-only Skia actuals — OpenGL)
// │ attached: macosArm64Main / linuxX64Main / linuxArm64Main
// │ only when Skia path is active (default on macOS/Linux)
// └── sdlRendererMain (SDL3 drawing pipeline + TTF/IMG/FreeType)
// ├── sdlRendererMacosMain (macOS-only SDL3 driver hint)
// ├── sdlRendererLinuxMain (Linux-only SDL3 driver hint)
// └── sdlRendererMingwMain (mingwX64-only SDL3 driver hint)
// attached: mingwX64Main always; macOS/Linux when -Prenderer=sdl3
// └── nativeMain (vendored .native.kt + project SDL3 wrappers)
// ├── skikoRendererMain (Skia pipeline; OFFICIAL Skiko — macOS/Linux)
// │ ├── skikoRendererMacosMain (macOS Metal bridge) → macosArm64
// │ └── skikoRendererLinuxMain (Linux OpenGL) → linuxX64/Arm64
// └── skikoRendererMingwSharedMain (Skia pipeline; the bitsycore skiko FORK —
// └── skikoRendererMingwMain mingwX64 has no official Skiko klib) → mingwX64
//
// `-Prenderer=sdl3` flips macOS/Linux targets onto the SDL3 path. The
// `:renderer-skia` and `:renderer-sdl3` sibling modules are gone — their code
// lives here. `:window` depends only on `:core`; `createRenderBackend` comes
// from whichever of the renderer source sets is active for the target.
// SDL3 stays as the windowing / input / platform layer (the single `sdl3`
// cinterop, now in :sdl-core). Every target renders through Skia.
// `:desktop-native-window` depends only on this module and calls createRenderBackend() /
// rendererPreferredGpuMode(), which resolve to the Skia actuals.
plugins {
alias(libs.plugins.kotlin.multiplatform)
@@ -27,39 +25,6 @@ plugins {
alias(libs.plugins.compose.multiplatform)
}
// -Prenderer=sdl3 flips macOS/Linux targets onto sdlRendererMain (Skiko-free build).
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
// All targets pull headers from the in-repo static build tree at <repo>/libs,
// populated by scripts/build-sdl/build-all.py (optionally one library at a time).
// Same paths on macOS / Linux / Windows — the .def files are pathless and
// rely on these -I injections. Cross-target cinterop indexing under
// kotlin.mpp.enableCInteropCommonization is fine: clang silently ignores -I
// dirs that don't exist, and the libs/ tree only has headers for the host
// that built them (they aren't cross-installed).
val vLibs = "${rootDir.invariantSeparatorsPath}/libs"
val vHostSdlInclude: String = "$vLibs/SDL3/include"
val vHostFtInclude: String = "$vLibs/FreeType/include/freetype2"
val vHostTtfInclude: String = "$vLibs/SDL3_ttf/include"
val vHostImageInclude: String = "$vLibs/SDL3_image/include"
// Static-archive dirs for cinterop's `staticLibraries = …` directive. Same
// paths on every host — cinterop only reads the one for the target being
// built, so cross-target indexing on a host that hasn't built libs/ for a
// foreign target is fine (cinterop skips the archive lookup).
val vSdlLibDir: String = "$vLibs/SDL3/lib"
val vFtLibDir: String = "$vLibs/FreeType/lib"
val vTtfLibDir: String = "$vLibs/SDL3_ttf/lib"
val vImageLibDir: String = "$vLibs/SDL3_image/lib"
// Renderer assignment per target. mingwX64 is always SDL3; macOS / Linux
// default to Skia, switch to SDL3 under -Prenderer=sdl3.
fun isSkiaTarget(targetName: String): Boolean = when (targetName) {
"mingwX64" -> false
"macosArm64", "linuxX64", "linuxArm64" -> !useSdl3Everywhere
else -> false
}
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
@@ -71,88 +36,16 @@ kotlin {
applyDefaultHierarchyTemplate()
targets.withType<KotlinNativeTarget>().all {
val vTargetName = name
// Whether this target compiles the SDL3 drawing pipeline. mingwX64 is
// always SDL3 (no Skiko on Windows); macOS / Linux switch to it only
// under -Prenderer=sdl3. SDL3 on macOS/Linux is a DEBUG target we don't
// ship in releases, so the default release build (Skia) skips the
// sdl3_ttf / sdl3_image / freetype cinterops entirely — one fewer set
// of system headers to install in CI, and a smaller klib footprint.
val vSdlRenderer = vTargetName == "mingwX64" ||
((vTargetName == "macosArm64" || vTargetName == "linuxX64" || vTargetName == "linuxArm64") && useSdl3Everywhere)
// Path to the sdl3 cinterop output klib for this target — used to
// wire `depends = sdl3` in sdl3_ttf / sdl3_image / freetype below.
// Gradle does NOT auto-add the sdl3 klib to the dependent cinterop
// tasks' -library list, so cinterop generates its own SDL_Surface /
// SDL_Color inside sdl3_image / sdl3_ttf instead of reusing the sdl3
// ones. Passing the path explicitly via extraOpts forces the link.
val vSdl3Klib = layout.buildDirectory.dir(
"classes/kotlin/$vTargetName/main/cinterop/ui-cinterop-sdl3"
).get().asFile.absolutePath
compilations["main"].cinterops {
// sdl3 stays for every target — :window uses the SDL3 main-loop
// types (SDL_Window / SDL_Event / SDL_GetBasePath / …) regardless
// of the renderer choice.
create("sdl3") {
defFile(project.file("src/nativeInterop/cinterop/sdl3.def"))
packageName("sdl3")
extraOpts("-compiler-options", "-I$vHostSdlInclude")
extraOpts("-libraryPath", vSdlLibDir)
}
if (vSdlRenderer) {
create("sdl3_ttf") {
defFile(project.file("src/nativeInterop/cinterop/sdl3_ttf.def"))
packageName("sdl3_ttf")
extraOpts("-library", vSdl3Klib)
extraOpts("-compiler-options", "-I$vHostSdlInclude")
extraOpts("-compiler-options", "-I$vHostTtfInclude")
extraOpts("-libraryPath", vTtfLibDir)
}
create("sdl3_image") {
defFile(project.file("src/nativeInterop/cinterop/sdl3_image.def"))
packageName("sdl3_image")
extraOpts("-library", vSdl3Klib)
extraOpts("-compiler-options", "-I$vHostSdlInclude")
extraOpts("-compiler-options", "-I$vHostImageInclude")
extraOpts("-libraryPath", vImageLibDir)
}
// FreeType powers variable-font axis rendering (FILL / wght /
// GRAD / opsz) on Material Symbols icons in the SDL3 path.
create("freetype") {
defFile(project.file("src/nativeInterop/cinterop/freetype.def"))
packageName("freetype")
extraOpts("-compiler-options", "-I$vHostFtInclude")
extraOpts("-libraryPath", vFtLibDir)
}
}
}
// Wire the task graph so cinteropSdl3_ttf/_image*Target run AFTER
// cinteropSdl3*Target — the -library reference above only points at
// the klib path; without a task dependency Gradle might run the
// dependent cinterop first and the path wouldn't exist yet.
if (vSdlRenderer) {
val vT = vTargetName.replaceFirstChar { it.uppercase() }
tasks.matching { it.name == "cinteropSdl3_ttf$vT" || it.name == "cinteropSdl3_image$vT" }
.configureEach { dependsOn("cinteropSdl3$vT") }
}
}
sourceSets {
commonMain {
// Files vendored VERBATIM from upstream Compose by
// scripts/compose-fork/sync.sh. Kept in their own folder so it's
// obvious they are generated — never hand-edit; re-run sync instead.
// scripts/compose-fork/sync.sh. Never hand-edit; re-run sync.
kotlin.srcDir("src/vendor/common/kotlin")
dependencies {
// Split-out lower ui artifacts (CMP layout). The graphics/text/framework
// vendored code + renderers here import ui.util / ui.geometry / ui.unit /
// ui.backhandler from these modules.
api(project(":ui-util"))
api(project(":ui-geometry"))
api(project(":ui-graphics"))
api(project(":ui-text"))
api(project(":ui-unit"))
api(project(":ui-backhandler"))
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
@@ -163,11 +56,6 @@ kotlin {
api("androidx.savedstate:savedstate-compose:1.5.0")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0")
api("androidx.lifecycle:lifecycle-runtime-compose:2.11.0")
// Navigation 3 runtime — the backstack / NavEntry / scene model. Published for
// all our K/N targets (mingwX64 + macos/linux) and pulls only runtime / lifecycle /
// savedstate / collection / serialization (no androidx.compose.ui). The NavDisplay
// UI (androidx.navigation3:navigation3-ui) has NO K/N desktop artifact, so it's not
// here — a display is hand-rolled on the project's own ui/animation.
api("androidx.navigation3:navigation3-runtime:1.1.4")
api("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0")
implementation(libs.kotlinx.coroutines.core)
@@ -178,74 +66,61 @@ kotlin {
// Vendored platform `actual`s + project SDL3 wrappers / Compose native code.
nativeMain {
kotlin.srcDir("src/vendor/native/kotlin")
dependencies {
// The sdl3 cinterop now lives in :sdl-core; expose it (api) so :ui's
// SDL platform + renderer code — and :desktop-native-window downstream — see sdl3.*
// and inherit SDL3's static-lib + linker-opt propagation.
api(project(":sdl-core"))
}
}
// ============
// Renderer roots. Each is a child of nativeMain; per-platform
// intermediates below attach to one of these. Only the renderer
// source sets that will actually be attached are created, so Gradle
// doesn't warn about unused source sets when the build is asymmetric
// (e.g. -Prenderer=sdl3 wouldn't use any skikoRenderer* sets).
// Skia renderer. macOS/Linux use the OFFICIAL Skiko; mingwX64 uses the
// bitsycore skiko FORK (no official mingw klib), in a separate tree so
// only mingw gets the fork coord.
val sdlRendererMain = create("sdlRendererMain") {
val skikoRendererMain = create("skikoRendererMain") {
dependsOn(nativeMain.get())
// src/vendor/sdlRenderer/kotlin holds files vendored verbatim
// from upstream's skikoMain that are SDL3-friendly (no Skia refs)
// or whose SDL3 actual we provide; same "never hand-edit" rule
// as the other vendor srcDirs.
kotlin.srcDir("src/vendor/sdlRenderer/kotlin")
// SDL3_ttf / SDL3_image / freetype cinterop bindings come from
// the per-target cinterop block above; no separate Gradle deps.
// src/vendor/skikoRenderer/kotlin — upstream `skikoMain` files
// (Skia-tied actuals like BlendMode.skiko.kt) vendored verbatim.
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
dependencies {
implementation(libs.skiko)
}
}
// mingwX64 is SDL3 always — attach its intermediate only when the
// target itself was declared (host is Windows). Non-Windows hosts skip
// both the target and its source-set wiring.
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 sdlRendererMingwMain = create("sdlRendererMingwMain") { dependsOn(sdlRendererMain) }
mingwX64Main.get().dependsOn(sdlRendererMingwMain)
}
val macosArm64Main = get("macosArm64Main")
val linuxX64Main = get("linuxX64Main")
val linuxArm64Main = get("linuxArm64Main")
if (useSdl3Everywhere) {
// macOS / Linux flip to SDL3 — create the sdl intermediates.
val sdlRendererMacosMain = create("sdlRendererMacosMain") { dependsOn(sdlRendererMain) }
val sdlRendererLinuxMain = create("sdlRendererLinuxMain") { dependsOn(sdlRendererMain) }
macosArm64Main.dependsOn(sdlRendererMacosMain)
linuxX64Main.dependsOn(sdlRendererLinuxMain)
linuxArm64Main.dependsOn(sdlRendererLinuxMain)
} else {
// Default: macOS / Linux use Skia. Create the skiko tree.
val skikoRendererMain = create("skikoRendererMain") {
// Route 1a: mingwX64 Skia leg on the fork. Two levels so the shared
// PlatformGpu expect (Shared) has its mingw actual (Mingw), mirroring
// the macos/linux split. Depends on the fork ROOT coord (not the
// platform artifact) so KMP variant-resolution exposes api-elements.
val skikoRendererMingwSharedMain = create("skikoRendererMingwSharedMain") {
dependsOn(nativeMain.get())
// src/vendor/skikoRenderer/kotlin holds upstream's `skikoMain`
// files (Skia-tied actuals / helpers like BlendMode.skiko.kt)
// vendored verbatim. Same "never hand-edit" rule.
kotlin.srcDir("src/skikoRendererMain/kotlin")
kotlin.srcDir("src/vendor/skikoRenderer/kotlin")
dependencies {
implementation(libs.skiko)
// Published by the fork's CI to GitHub Packages; version
// overridable via -PskikoMingwVersion=.
implementation("com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse("0.150.1-mingw.1")}")
}
}
val skikoRendererMacosMain = create("skikoRendererMacosMain") { dependsOn(skikoRendererMain) }
val skikoRendererLinuxMain = create("skikoRendererLinuxMain") { dependsOn(skikoRendererMain) }
macosArm64Main.dependsOn(skikoRendererMacosMain)
linuxX64Main.dependsOn(skikoRendererLinuxMain)
linuxArm64Main.dependsOn(skikoRendererLinuxMain)
val skikoRendererMingwMain = create("skikoRendererMingwMain") { dependsOn(skikoRendererMingwSharedMain) }
get("mingwX64Main").dependsOn(skikoRendererMingwMain)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xcollection-literals",
// Silence the expect/actual-classes Beta warning that vendored files
// (e.g. ImageBitmap / Paint / Canvas / Path / ClipEntry) tripped. See
// Silence the expect/actual-classes Beta warning vendored files trip.
// https://youtrack.jetbrains.com/issue/KT-61573.
"-Xexpect-actual-classes",
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
// Vendored code is upstream foundation/ui, which their build compiles with these
// module-level opt-ins (e.g. lazy-layout prefetch uses ExperimentalFoundationApi).
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.ui.InternalComposeUiApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
-224
View File
@@ -406,230 +406,6 @@ skikoMain/kotlin/androidx/compose/ui/skiko/RecordDrawRectRenderDecorator.skiko.k
!skikoMain/kotlin/androidx/compose/ui/window/WindowExceptionHandler.kt
skikoMain/kotlin/androidx/compose/ui/window/WindowExceptionHandler.kt -> src/vendor/common/kotlin/androidx/compose/ui/window/WindowExceptionHandler.kt
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
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 (125)
# | androidMain/kotlin/androidx/compose/ui/Actual.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/Actual.android.kt
@@ -1,17 +0,0 @@
package com.compose.sdl.graphics
// ==================
// MARK: NativeFinishableCanvas
// ==================
/**
A renderer canvas that must be flushed once drawing into it is done — SDL batches
geometry and only submits on flush; an offscreen SDL canvas must flush before its
backing texture is read. Implemented by Sdl3Canvas (real flush) and 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()
}
@@ -1,33 +0,0 @@
package com.compose.sdl.graphics
import androidx.compose.ui.layout.ContentScale
import com.compose.sdl.res.ResourceKind
// ==================
// MARK: NativePainterCanvas bridge
// ==================
/**
Image drawing bridge. Renderer Canvas backends (Sdl3Canvas / SkiaBackedCanvas)
implement this interface to paint a bundled resource by
path + kind. [ResourcePainter] (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), so the resource render path is driven end-to-end by the
vendored upstream engine.
contentScale is FillBounds when called through PainterModifier (which
pre-scales the size); other values are supported for direct callers.
*/
interface NativePainterCanvas {
fun drawNativePainter(
inResourcePath: String,
inKind: ResourceKind,
inX: Float,
inY: Float,
inWidth: Float,
inHeight: Float,
inContentScale: ContentScale,
inAlpha: Float,
)
}
@@ -6,7 +6,7 @@ import androidx.compose.ui.draganddrop.DragAndDropManager
// MARK: DragAndDropPlatform — SDL event pump hooks
// ==================
/** Platform hooks the :window layer's event loop uses to feed SDL_EVENT_DROP_*
/** Platform hooks the :desktop-native-window layer's event loop uses to feed SDL_EVENT_DROP_*
* into a per-window DragAndDropManager. The manager instance is created here
* (native actual: Sdl3DragAndDropOwner over the SDL drop stream); each dropX
* extension casts to the concrete platform implementation and no-ops if the
@@ -8,7 +8,7 @@ import androidx.compose.ui.node.LayoutNode
// ==================
/** Composition applier over the vendored upstream LayoutNode.
internal because LayoutNode is internal to :core; the :window layer drives
internal because LayoutNode is internal to :core; the :desktop-native-window layer drives
composition through the public ComposeRootHost facade (which upcasts this to
Applier<*>). */
internal class NodeApplier(root: LayoutNode) : AbstractApplier<LayoutNode>(root) {
@@ -1,234 +0,0 @@
package com.compose.sdl.text
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString.Range
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import kotlin.math.roundToInt
// ==================
// MARK: Per-line style runs (renderer helper)
// ==================
/* Project render glue with no official Compose equivalent — the text renderers
turn an AnnotatedString's spans into per-line style runs (colour + weight +
italic + background + decoration + size). Lives in
`com.compose.sdl.text` rather than `androidx.compose.ui.text`. */
/** A run of one style within a wrapped line, line-local [start, end) cols.
`weight` is the OpenType wght axis (100..900, 400 = default), `italic` =
FontStyle.Italic (paragraph base or span), `background` = SpanStyle.background
(Unspecified = none), `underline`/`lineThrough` = TextDecoration flags
(paragraph base or span), `fontSize` = the span's size (Unspecified = the
paragraph size; Em scales it, Sp resolves through the density).
Measurement/wrapping is still base-style, so a bold/resized run's glyphs
paint into a line box wrapped at base metrics; that's a known simplification
(real fidelity needs a re-wrap per style run). Paint-time advances DO use
the run's own style, so runs push each other over correctly. */
class ColorRun(
val start: Int,
val end: Int,
val color: Color,
val weight: Int = 400,
val italic: Boolean = false,
val background: Color = Color.Unspecified,
val underline: Boolean = false,
val lineThrough: Boolean = false,
val fontSize: TextUnit = TextUnit.Unspecified,
)
/** Style runs for a single wrapped line, given the AnnotatedString spans (whose
start/end index the ORIGINAL text) and this line's start offset. Gaps use
the base style; overlapping spans compose colour/background/size last-wins,
weight max-wins (bold beats regular), italic/underline/lineThrough OR-wins
(also OR'd with the paragraph-level base flags — a span's explicit
TextDecoration.None can't clear a base decoration; simplification).
Spans are assumed sorted by start with non-decreasing end (true for the
tokenizers and for buildAnnotatedString appended in order). That lets us
binary-search to the first span reaching this line and stop at the first span
past it, so a visible line of a 20k-line highlighted body touches only its
own handful of spans instead of scanning the whole (tens-of-thousands) list
every frame — the difference between smooth and janky scrolling. */
fun lineColorRuns(
inLine: String,
inLineStart: Int,
inSpans: List<Range<SpanStyle>>,
inDefault: Color,
inBaseItalic: Boolean = false,
inBaseUnderline: Boolean = false,
inBaseLineThrough: Boolean = false,
): List<ColorRun> {
val vN = inLine.length
if (vN == 0) return emptyList()
val vLineEnd = inLineStart + vN
// Per-column style. Nullable arrays let us distinguish "explicit default" from
// "no span touched me" — the coalesce step uses `inDefault` when a column stayed null.
val vCols = arrayOfNulls<Color>(vN)
val vWgt = IntArray(vN)
val vIt = BooleanArray(vN)
val vBg = arrayOfNulls<Color>(vN)
val vUl = BooleanArray(vN)
val vSt = BooleanArray(vN)
val vSz = arrayOfNulls<TextUnit>(vN)
// First span whose end reaches into this line (skip the prefix before it).
var vIdx = firstSpanReaching(inSpans, inLineStart)
while (vIdx < inSpans.size) {
val vS = inSpans[vIdx]
// Sorted by start → once a span starts at/after the line end, so do all
// the rest; nothing more can overlap.
if (vS.start >= vLineEnd) break
vIdx++
val vColor = vS.item.color
val vSpanWeight = vS.item.fontWeight?.weight ?: 0
val vSpanItalic = vS.item.fontStyle == FontStyle.Italic
val vSpanBg = vS.item.background
val vDeco = vS.item.textDecoration
val vSpanUl = vDeco != null && vDeco.contains(TextDecoration.Underline)
val vSpanSt = vDeco != null && vDeco.contains(TextDecoration.LineThrough)
val vSpanSz = vS.item.fontSize
if (vColor == Color.Unspecified && vSpanWeight == 0 && !vSpanItalic &&
vSpanBg == Color.Unspecified && !vSpanUl && !vSpanSt && vSpanSz == TextUnit.Unspecified
) continue
val vA = vS.start - inLineStart
val vB = vS.end - inLineStart
if (vB <= 0 || vA >= vN) continue
var i = if (vA < 0) 0 else vA
val vEnd = if (vB > vN) vN else vB
while (i < vEnd) {
if (vColor != Color.Unspecified) vCols[i] = vColor
if (vSpanWeight > vWgt[i]) vWgt[i] = vSpanWeight
if (vSpanItalic) vIt[i] = true
if (vSpanBg != Color.Unspecified) vBg[i] = vSpanBg
if (vSpanUl) vUl[i] = true
if (vSpanSt) vSt[i] = true
if (vSpanSz != TextUnit.Unspecified) vSz[i] = vSpanSz
i++
}
}
// Coalesce equal-style columns into runs. Two adjacent columns coalesce
// only when every dimension matches.
val vRuns = ArrayList<ColorRun>()
var i = 0
while (i < vN) {
val vC = vCols[i] ?: inDefault
val vW = if (vWgt[i] == 0) 400 else vWgt[i]
val vI = vIt[i] || inBaseItalic
val vB = vBg[i] ?: Color.Unspecified
val vU = vUl[i] || inBaseUnderline
val vS2 = vSt[i] || inBaseLineThrough
val vZ = vSz[i] ?: TextUnit.Unspecified
var j = i + 1
while (j < vN &&
(vCols[j] ?: inDefault) == vC &&
(if (vWgt[j] == 0) 400 else vWgt[j]) == vW &&
(vIt[j] || inBaseItalic) == vI &&
(vBg[j] ?: Color.Unspecified) == vB &&
(vUl[j] || inBaseUnderline) == vU &&
(vSt[j] || inBaseLineThrough) == vS2 &&
(vSz[j] ?: TextUnit.Unspecified) == vZ
) j++
vRuns.add(ColorRun(i, j, vC, vW, vI, vB, vU, vS2, vZ))
i = j
}
return vRuns
}
// ==================
// MARK: Style-aware metrics helpers
// ==================
// Shared by BOTH the renderers (paint-time advances) and SdlParagraph
// (layout-time widths/heights) so painted glyph runs land exactly inside the
// measured box — per-run fontSize / fontWeight change metrics, not just paint.
/** Resolves a run's SpanStyle.fontSize to pixels: Em scales the paragraph's
base size, Sp resolves through the density the base size was resolved with,
Unspecified inherits the base. */
fun resolveRunPx(inRun: ColorRun, inBasePx: Int, inDensity: Float): Int = when (inRun.fontSize.type) {
TextUnitType.Em -> (inBasePx * inRun.fontSize.value).roundToInt().coerceAtLeast(1)
TextUnitType.Sp -> (inRun.fontSize.value * inDensity).roundToInt().coerceAtLeast(1)
else -> inBasePx
}
/** A run's font axes: its own weight when set (400 = "no run weight"), else
the paragraph's base axes. */
fun runVariations(
inRun: ColorRun,
inBaseVariations: List<FontVariation.Setting>?,
): List<FontVariation.Setting>? =
if (inRun.weight != 400) listOf(FontVariation.weight(inRun.weight)) else inBaseVariations
/** Whether any span carries a metric-affecting style (size / weight) — the
gate for the styled measurement paths, so plain/colour-only text keeps the
cheap single-measure route. */
fun spansAffectMetrics(inSpans: List<Range<SpanStyle>>?): Boolean =
inSpans != null && inSpans.any {
it.item.fontSize != TextUnit.Unspecified || it.item.fontWeight != null
}
/** Width of a text slice, span-aware: sums each style run's advance at its
resolved size/weight through [inMeasurer]. `inGlobalStart` maps the slice
into the spans' index space. */
fun styledSliceWidth(
inSlice: String,
inGlobalStart: Int,
inSpans: List<Range<SpanStyle>>,
inBasePx: Int,
inDensity: Float,
inMeasurer: TextMeasurer,
inFontFamily: String?,
inBaseVariations: List<FontVariation.Setting>?,
): Float {
if (inSlice.isEmpty()) return 0f
var vW = 0f
for (vRun in lineColorRuns(inSlice, inGlobalStart, inSpans, Color.Unspecified)) {
val vPx = resolveRunPx(vRun, inBasePx, inDensity)
val vVars = runVariations(vRun, inBaseVariations)
vW += inMeasurer.measure(
inSlice.substring(vRun.start, vRun.end), vPx, Int.MAX_VALUE, inFontFamily, vVars,
).width
}
return vW
}
/** Tallest run cell height on a line — the line's box height when spans carry
their own sizes; the base cell height when they don't (or the line is empty). */
fun styledLineCellHeight(
inLine: String,
inGlobalStart: Int,
inSpans: List<Range<SpanStyle>>,
inBasePx: Int,
inDensity: Float,
inMeasurer: TextMeasurer,
inFontFamily: String?,
inBaseVariations: List<FontVariation.Setting>?,
): Float {
var vH = inMeasurer.lineHeight(inBasePx, inFontFamily, inBaseVariations)
for (vRun in lineColorRuns(inLine, inGlobalStart, inSpans, Color.Unspecified)) {
val vPx = resolveRunPx(vRun, inBasePx, inDensity)
if (vPx != inBasePx) {
val vRunH = inMeasurer.lineHeight(vPx, inFontFamily, runVariations(vRun, inBaseVariations))
if (vRunH > vH) vH = vRunH
}
}
return vH
}
/** Index of the first span whose end is past inLineStart — i.e. the first that
can reach into a line starting there. Binary search; relies on spans being
sorted with non-decreasing end (see lineColorRuns). */
private fun firstSpanReaching(inSpans: List<Range<SpanStyle>>, inLineStart: Int): Int {
var vLo = 0
var vHi = inSpans.size
while (vLo < vHi) {
val vMid = (vLo + vHi) ushr 1
if (inSpans[vMid].end <= inLineStart) vLo = vMid + 1 else vHi = vMid
}
return vLo
}
@@ -1,108 +0,0 @@
package com.compose.sdl.text
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.text.AnnotatedString.Range
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
// ==================
// MARK: Native text drawing bridge (B5)
// ==================
/**
Bridges commonMain text drawing to the platform renderer without a hard
dependency on it. A renderer's `Canvas` (Sdl3Canvas / SkiaBackedCanvas)
implements [NativeTextCanvas]; the text [DrawModifierNode] gets the live Canvas via
`drawIntoCanvas` and, if it's a NativeTextCanvas, asks it to paint the glyphs. Layout
sizing still goes through `currentTextMeasurer` so bounds match the drawn text.
This keeps text as a real upstream DrawModifierNode in the modifier chain — it draws
through the coordinator pipeline like any other draw node.
*/
interface NativeTextCanvas {
fun drawNativeText(
inText: String,
inSpans: List<Range<SpanStyle>>?,
inX: Float,
inY: Float,
inBoxWidth: Float,
inBoxHeight: Float,
inColor: Color,
inFontSizePx: Int,
inTextAlign: TextAlign,
inSoftWrap: Boolean,
inFontFamily: String?,
inFontVariations: List<FontVariation.Setting>?,
// Paragraph-level TextStyle.fontStyle == Italic + merged textDecoration
// (underline / line-through). Per-run overrides ride in via inSpans.
inBaseItalic: Boolean = false,
inTextDecoration: TextDecoration? = null,
// The paragraph's per-line band height in px (TextStyle.lineHeight when the
// style specifies one — SdlParagraph passes it so glyph rows stack exactly
// where layout put the line boxes). <= 0 → derive from font metrics (legacy
// behaviour; icon callers and TextDrawNode don't pass it).
inLineHeightPx: Float = 0f,
// Compat-trim (raw styles): the FIRST line keeps the tight font cell. False
// for M3's LineHeightStyle(Trim.None) — every line is the full band.
inTrimFirstLine: Boolean = true,
)
}
// Modifier element carrying the text paint params onto a LayoutNode's chain.
// Public (not internal) so :foundation's IconText.kt can reach it — same
// package, different module.
data class TextDrawElement(
val text: String,
val spans: List<Range<SpanStyle>>?,
val color: Color,
val fontSizePx: Int,
val textAlign: TextAlign,
val softWrap: Boolean,
val fontFamily: String?,
val fontVariations: List<FontVariation.Setting>?,
) : ModifierNodeElement<TextDrawNode>() {
override fun create(): TextDrawNode =
TextDrawNode(text, spans, color, fontSizePx, textAlign, softWrap, fontFamily, fontVariations)
override fun update(node: TextDrawNode) {
node.text = text
node.spans = spans
node.color = color
node.fontSizePx = fontSizePx
node.textAlign = textAlign
node.softWrap = softWrap
node.fontFamily = fontFamily
node.fontVariations = fontVariations
}
}
// DrawModifierNode that paints the text via the renderer Canvas at the node's origin.
class TextDrawNode(
var text: String,
var spans: List<Range<SpanStyle>>?,
var color: Color,
var fontSizePx: Int,
var textAlign: TextAlign,
var softWrap: Boolean,
var fontFamily: String?,
var fontVariations: List<FontVariation.Setting>?,
) : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() {
drawContent()
drawIntoCanvas { vCanvas ->
(vCanvas as? NativeTextCanvas)?.drawNativeText(
text, spans, 0f, 0f, size.width, size.height,
color, fontSizePx, textAlign, softWrap, fontFamily, fontVariations,
)
}
}
}

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