Files
Claude a87222c51f refactor: split Compose UI out of commons into a new commonsUI module
`:commons` is on the CLI classpath, yet it declared Compose UI, Coil, Compose
resources, markdown and desktop Compose as dependencies, dragging ~40 MB of
UI/Skiko jars into every `amy` distribution. This moves every
Compose-dependent file into a new KMP module, `:commonsUI`, that
`api`-depends on `:commons`; `:commons` keeps only the Compose runtime
(stability annotations + snapshot state) and lifecycle-viewmodel.

Files keep their `com.vitorpamplona.amethyst.commons.*` packages, so the split
is a build-graph boundary and no consumer import changed. 236 files were
`git mv`'d (composables, icons, robohash, theme, Coil fetchers, the
`@Composable` relay-client entry points, `composeResources`, and the tests
that exercise them). Two headless files needed surgery instead of a move:
`GalleryParser` lost a vestigial foundation `@OptIn`, and the
`LocalPrivacyLockState`/`lockStateFor` CompositionLocal accessor moved out of
`PrivacyLockState` into its own commonsUI file. The feed DAL under `ui/feeds`
and `ui/note/ParentNote`+`ReplyContext` stay in `commons` because ViewModels
depend on them.

`amethyst`, `desktopApp`, `nappletHost` (NappletWebContract serves the shell
from composeResources) and `benchmark` now depend on `:commonsUI`; `cli`,
`geode` and `marmotBench` do not. commons' androidMain gains an explicit
androidx.core KTX dep it previously got transitively through Compose UI.

CI, crowdin, the icon-font tools and the escaping hook point at the new
composeResources location; CLAUDE.md, commons/ARCHITECTURE.md, a new
commonsUI/ARCHITECTURE.md, CONTRIBUTING, BUILDING and the affected skills
document the boundary. A plan doc under commons/plans records the
classification method and follow-ups.

Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug
compiles for nappletHost and amethyst; commons/commonsUI/cli JVM test suites;
both verifyKmpPurity gates; the cli runtime classpath no longer resolves
Compose UI, material3, Skiko or Coil.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 16:26:59 +00:00
..

Amethyst icon font

Builds amethyst_icons.ttf from the Kotlin ImageVector icons in commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/.

Why

Icon(imageVector = …) calls rememberVectorPainter, and a VectorPainter rasterises its paths into a cached graphics layer per instance. A feed therefore re-rasterised the same handful of glyphs once for every card scrolled in. A font glyph is a blit from the shared text atlas instead, shared across every call site in the app for free — no CompositionLocal plumbing, no per-screen scoping.

Measured on the uniform-corpus macrobenchmark (SM-T220, three arms, 0.2% noise floor):

approach frame P90 overrun P90 artwork
one shared VectorPainter per icon 8.2% 14.2% unchanged
MaterialSymbols glyph substitutes 10.4% 16.0% changes
this font 10.7% 17.4% unchanged
ceiling: draw no icons at all 12.7% 22.9% n/a

Usage

pip install fonttools
python3 tools/icon-font/build_icon_font.py <icons-dir> <out.ttf> <out.kt>

See the "Amethyst's own icons are also a font" section of .claude/CLAUDE.md for the mandatory regeneration step and why both outputs must be committed together.

How it works

The ImageVector builder DSL maps 1:1 onto SVG path commands (moveToM, curveToRelativec, …; none of the icons use arcTo), so the script extracts the path data, emits an SVG d string, and draws it into a TrueType glyph via fontTools — converting cubics to quadratics and flipping the y axis, since SVG grows downward and font outlines grow upward from the baseline.

Font metrics deliberately mirror the bundled material_symbols_outlined.ttf (unitsPerEm 960, ascent 1056, descent 96, advance 960) so the glyphs align with existing MaterialSymbols call sites and Icon() sizing. Generated outlines land within a few units of Google's own: our Like spans (78,94)(882,851), their heart (80,120)(880,854).

An icon whose path data the parser cannot reach is reported and skipped rather than silently emitted empty; it must keep using its ImageVector.