72bef059f2
Material Symbol icons were allocating a fresh Font wrapper on every composition (Font(resource = ...) returns a new instance each call), which invalidated the remember(font) cache in rememberMaterialSymbolsFontFamily and forced a fresh TextMeasurer per call site. Tint was also baked into the TextStyle passed to measure(), so any tint change (selected / unselected reaction icons, hover states) produced a cache miss. Hoist the FontFamily + TextMeasurer to CompositionLocals provided once at the root (AmethystTheme on Android, MaterialTheme wrapper on desktop) via ProvideMaterialSymbols. Every icon in the subtree now reuses: - One FontFamily (Skia typeface cache hit for every subsequent glyph) - One TextMeasurer with a 64-entry LRU, shared across all call sites so its measurement cache is hit across 300+ MaterialSymbols usages - Tint applied via drawText(layout, color = tint) instead of TextStyle, so the measured TextLayoutResult is reused across tint variations Icon(symbol = ...) now delegates to Material3's Icon(painter = ...), restoring proper sizing/semantics without the custom Box+drawBehind workaround. autoMirror handled inside the painter's onDraw. The previously-unused MaterialSymbolPainter is now the single render path. The weight parameter is dropped from the Icon/Painter APIs since the app uses a single weight globally (MaterialSymbolsDefaults.WEIGHT). A local fallback path keeps @Preview composables that don't wrap in the theme renderable (per-composition allocation, same cost as before). https://claude.ai/code/session_01V9esGUFH72ujCQNWCAnjKN