simpmusic-icons
GitHub指导SimpMusic项目中Material Symbols图标的添加、替换及规范使用。涵盖从Google服务生成Kotlin文件、参数配置、代码重构到避免RTL镜像陷阱等全流程,确保图标风格统一并解决ImageVector类型错误。
Trigger Scenarios
Install
npx skills add maxrave-dev/SimpMusic --skill simpmusic-icons -g -y
SKILL.md
Frontmatter
{
"name": "simpmusic-icons",
"description": "Add or change an icon in SimpMusic. Every icon is a generated Material Symbols ImageVector under ui\/icon, addressed as SimpIcons.<Name> — there is no material-icons dependency and no XML icon drawable. Use when adding a new icon, replacing one, or hitting ImageVector\/Painter type errors."
}
SimpMusic Icons
Icons live in composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/icon/, one file per
icon, each declaring val SimpIcons.<Name>: ImageVector.
Two things are deliberately absent and must not come back:
- the
material-icons-core/material-icons-extendeddependency (Icons.Rounded.*) - XML icon drawables in
composeResources/drawableused throughpainterResource(...)
Adding an icon
Google's font service generates the Compose file directly — no conversion tool needed:
curl -sfL --compressed \
"https://fonts.gstatic.com/render/v1/Material+Symbols+Rounded/24dp/<symbol_name>.kt?var=opsz,wght,FILL,GRAD,ROND@24,400,1,0,50" \
-o <PascalName>.kt
Keep these axes identical for every icon, or the set stops looking like one set:
Rounded, opsz 24, wght 400, GRAD 0, ROND 50, FILL=1.
Use FILL=0 only for the "off" half of a state pair — FavoriteBorder,
AddCircleOutline, DownloadForOfflineOutlined. At FILL=1 the off state renders identically to
the on state and the distinction is lost.
Then edit the downloaded file:
package com.example.test→package com.maxrave.simpmusic.ui.iconpublic val <symbol_name>: ImageVector→val SimpIcons.<PascalName>: ImageVector- Rename the backing field
_<symbol_name>→_<PascalName>, andname = "<symbol_name>"→"<PascalName>" - Never add
autoMirror = truetoImageVector.Builder(...): in right-to-left languages only text flips, icons keep their shape (owner's rule, 2026-10-14).ArrowForwardIos,QueueMusic,Sort,TrendingUp,VolumeOffandVolumeUphad it and drew mirrored until it was removed. - Replace the closing
.build()with.buildSimpIcon()(inSimpIcons.kt): it isbuild()plus the fit into Material's 20×20 live area described under Traps.SimpIconsFitTestfails on a file that still ends with.build().
Using an icon
import com.maxrave.simpmusic.ui.icon.PlayArrow // per-icon import, required
import com.maxrave.simpmusic.ui.icon.SimpIcons
Icon(SimpIcons.PlayArrow, contentDescription = null)
The project's own icon buttons take an ImageVector:
RippleIconButton(imageVector = …), LiquidGlassIconButton(imageVector = …),
ActionButton(icon = …).
Traps
Every icon needs its own import. val SimpIcons.X is an extension property; importing the
SimpIcons object alone leaves it unresolved. This is the same mechanism that lets R8 strip unused
icons, so never "simplify" the set into a map or a when — that would ship all of them.
ImageVector is not a Painter. Icon and Image have overloads for both, so those look
fine. These do not, and need rememberVectorPainter(SimpIcons.X):
AsyncImage(placeholder = …, error = …)- anything drawn inside a
DrawScope(with(painter) { draw(...) }) - custom composables whose parameter is typed
Painter
Do not text-replace blindly. A regex swap of Res.drawable.X → SimpIcons.X produced 40
compile errors in one pass: it also hit Painter parameters, and it landed inside multi-line
painterResource(\n Res.drawable.X,\n) calls, leaving the syntactically valid but wrong
painterResource(SimpIcons.X).
Do not replace an icon whose colour carries meaning. These stay as resources:
| File | Why |
|---|---|
baseline_downloaded.xml |
#FF00A0CB — the blue is the "downloaded" state |
baseline_favorite_24.xml |
#D10000 — the red is the "liked" state |
mono.xml, monochrome.xml |
SimpMusic logos (#0AEAF8 / black) |
app_icon.png, circle_app_icon.png, holder*.png |
bitmaps, not icons |
If a state needs that colour with a shared symbol, pass it explicitly — e.g. ActionButton's
iconColor = Color(0xFF00A0CB) for the downloaded state.
Verify the symbol name exists before assuming a mapping, against
google/material-design-icons → variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].codepoints.
Legacy names such as favorite_border, add_circle_outline and thumb_up_alt do still exist;
person_add_alt_1 does not (use person_add).
Glyphs that spill past Material's 20×20 live area are fitted into it (2026-10-18). Material's
icon grid keeps a glyph inside the centred 20×20 square of the 24 box, with 2dp of padding, and
balances the glyphs inside it by keyline shapes of about equal area. 24 of our 111 Symbols spill
into that padding anyway: Lyrics' note and Groups reach the box edge, ThumbsUpDown spans the
full width. At one size those read bigger than everything beside them. buildSimpIcon() measures
how far each glyph reaches from the centre along either axis (walking its path nodes, 16 samples
per curve) and shrinks one that passes 10 of 24 about the centre until it fits: Lyrics, Groups
and ThumbsUpDown ×0.83, the other 21 ×0.86–0.996. A glyph inside the square is left exactly as Google
drew it, nothing is enlarged, and anything it cannot measure (an arc, a group, a fit tighter than
×0.6) keeps the glyph as generated and logs under SimpIcons. SimpIconsFitTest (jvmTest) fails if
a file skips buildSimpIcon() or any icon ends up outside the square.
- Do not fit glyphs into a circle instead. That was tried first: a circle judges a glyph by its one
farthest point, so
Lyricsshrank to ×0.72 to pull its note's tip inside and then read far smaller thanQueueMusicbeside it.
A small round button cuts a glyph that fills the live area. IconButton clips its content to
its shape (a circle by default), and a button sized 24dp makes that a 24dp circle, 12dp from the
centre. A glyph that fills the 20×20 square reaches 14.1 at its corners — comment reaches 13.3 —
so its corners are cut while round or open glyphs beside it are not. The icon file is not the
problem; the button is. Check the farthest glyph point before putting an icon in one: Classic's
Comments button clips to 28dp with requiredSize(28.dp) inside the same 24dp slot.
Icons in one row take one size. Tuning each glyph by eye is what made the Apple Music row read big-small (23 and 24dp beside 22dp dock buttons, the title-row star 32dp beside 24dp glyphs). Only a deliberate hierarchy, such as a Play button over Previous/Next, gets a different size.
The response is gzipped even when the request asks for identity — curl --compressed, or
decompress by magic bytes (\x1f\x8b) when fetching from a script.
Version History
-
a81563e
Current 2026-10-18 18:38
修正了关于RTL(从右至左)语言下图标镜像的规则,明确禁止对图标使用autoMirror=true,以符合2026年10月14日的维护者规定。
- 973fbc8 2026-08-04 18:59


