Phosphor icons: resolve by name via a generated map (T-314)

Replace the 49 hand-maintained named consts with one generated
label→codepoint map (phosphor_glyphs.g.dart, 1512 glyphs from the glyph
table via tool/gen_phosphor_glyphs.dart). Feature code now references
glyphs by their exact kebab-case name — PhosphorIcons.byName('folder') —
with no raw codepoints; this also lets a Lua extension name an icon
without crossing the FFI boundary with a codepoint.

byName is total: an unknown name degrades to the `placeholder` box so the
bug is visible (it's a real error), while phosphor_glyphs_test asserts
every byName('...') literal in lib/ resolves — recovering the typo check a
const gave. Migrated the 89 call sites. Adds EmptyIconPainter for an
intentional blank that still reserves the icon box; ClideFilterBox gains
showIcon to keep the slot aligned when blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:46:51 +02:00
co-authored by Claude Opus 4.8
parent b8d75230e8
commit 3833a41a9e
38 changed files with 1779 additions and 163 deletions
@@ -0,0 +1,47 @@
/// Guards the Phosphor label→codepoint indirection (T-314). The named consts
/// are gone; code resolves glyphs by string via [PhosphorIcons.byName]. A
/// string key loses the compiler's typo check, so this test recovers it: every
/// `byName('…')` string literal in `lib/` must exist in the generated map —
/// across all call sites, executed or not.
library;
import 'dart:io';
import 'package:clide/widgets/src/icons/phosphor_glyphs.g.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('the generated glyph map covers the full Phosphor set', () {
expect(kPhosphorGlyphs.length, greaterThanOrEqualTo(1500));
expect(kPhosphorGlyphs.containsKey('folder'), isTrue);
// The fallback glyph must itself resolve, or byName would throw on a miss.
expect(kPhosphorGlyphs.containsKey(PhosphorIcons.fallbackName), isTrue);
});
test('every byName(\'\') literal in lib/ resolves to a real glyph', () {
final call = RegExp(r"byName\(\s*'([a-z0-9-]+)'\s*\)");
final missing = <String, String>{}; // label -> first file it appears in
for (final entity in Directory('lib').listSync(recursive: true)) {
if (entity is! File || !entity.path.endsWith('.dart')) continue;
for (final m in call.allMatches(entity.readAsStringSync())) {
final label = m.group(1)!;
if (!kPhosphorGlyphs.containsKey(label)) {
missing.putIfAbsent(label, () => entity.path);
}
}
}
expect(missing, isEmpty, reason: 'unknown Phosphor glyph name(s) in byName(): $missing');
});
test('byName resolves a known name to its codepoint', () {
expect(PhosphorIcons.byName('folder').codePoint, kPhosphorGlyphs['folder']);
expect(PhosphorIcons.byName('caret-line-left').codePoint, kPhosphorGlyphs['caret-line-left']);
});
test('byName degrades an unknown name to the placeholder fallback (never throws)', () {
final fallback = kPhosphorGlyphs[PhosphorIcons.fallbackName];
expect(PhosphorIcons.byName('definitely-not-a-real-glyph').codePoint, fallback);
expect(PhosphorIcons.fallbackName, 'placeholder');
});
}