solar_icons_flutter 1.0.0 copy "solar_icons_flutter: ^1.0.0" to clipboard
solar_icons_flutter: ^1.0.0 copied to clipboard

The complete Solar Icon Set: 7,759 icons in all six styles (linear, outline, bold, broken, and both duotones) as tree-shakeable icon fonts. No dependencies.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// The gallery needs every icon by name, so it imports the opt-in index library.
// Apps that reference icons directly (`SolarIconsBold.home`) should import
// `package:solar_icons_flutter/solar_icons_flutter.dart` instead, which lets
// Flutter tree-shake the fonts down to just the glyphs they use.
import 'package:solar_icons_flutter/solar_icons_index.dart';

void main() => runApp(const SolarGalleryApp());

class SolarGalleryApp extends StatefulWidget {
  const SolarGalleryApp({super.key});

  @override
  State<SolarGalleryApp> createState() => _SolarGalleryAppState();
}

class _SolarGalleryAppState extends State<SolarGalleryApp> {
  ThemeMode _themeMode = ThemeMode.light;

  @override
  Widget build(BuildContext context) {
    ThemeData theme(Brightness brightness) => ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: const Color(0xFF5B5BD6),
            brightness: brightness,
          ),
          useMaterial3: true,
        );

    return MaterialApp(
      title: 'Solar Icons',
      debugShowCheckedModeBanner: false,
      theme: theme(Brightness.light),
      darkTheme: theme(Brightness.dark),
      themeMode: _themeMode,
      home: GalleryPage(
        onToggleTheme: () => setState(() {
          _themeMode =
              _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
        }),
      ),
    );
  }
}

const double _controlsHeight = 126;

/// One row of the gallery: a Solar name plus whichever icon data the current
/// style provides for it.
class _Entry {
  const _Entry(this.name, this.icon, this.duotone);

  final String name;
  final IconData icon;
  final SolarDuotoneIconData? duotone;
}

List<_Entry> _entriesFor(SolarIconStyle style) {
  if (style.isDuotone) {
    final map = style == SolarIconStyle.boldDuotone
        ? solarBoldDuotoneIcons
        : solarLineDuotoneIcons;
    return <_Entry>[
      for (final e in map.entries) _Entry(e.key, e.value.primary, e.value),
    ];
  }
  final map = switch (style) {
    SolarIconStyle.linear => solarLinearIcons,
    SolarIconStyle.outline => solarOutlineIcons,
    SolarIconStyle.broken => solarBrokenIcons,
    _ => solarBoldIcons,
  };
  return <_Entry>[for (final e in map.entries) _Entry(e.key, e.value, null)];
}

class GalleryPage extends StatefulWidget {
  const GalleryPage({required this.onToggleTheme, super.key});

  final VoidCallback onToggleTheme;

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  SolarIconStyle _style = SolarIconStyle.boldDuotone;
  String _query = '';
  double _size = 32;

  // Recomputed only when the style or query changes. Dragging the size slider
  // rebuilds every frame, and re-filtering 1,300 icons each time would jank.
  late List<_Entry> _all = _entriesFor(_style);
  late List<_Entry> _visible = _all;

  void _setStyle(SolarIconStyle style) {
    if (style == _style) return;
    setState(() {
      _style = style;
      _all = _entriesFor(style);
      _visible = _filter(_all, _query);
    });
  }

  void _setQuery(String query) {
    if (query == _query) return;
    setState(() {
      _query = query;
      _visible = _filter(_all, query);
    });
  }

  static List<_Entry> _filter(List<_Entry> all, String query) {
    final q = query.toLowerCase().trim();
    if (q.isEmpty) return all;
    return all.where((e) => e.name.contains(q)).toList();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final entries = _visible;
    final isDark = theme.brightness == Brightness.dark;

    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            title: const Text('Solar Icons'),
            actions: <Widget>[
              IconButton(
                tooltip: isDark ? 'Light mode' : 'Dark mode',
                onPressed: widget.onToggleTheme,
                icon: Icon(isDark ? SolarIconsBold.sun : SolarIconsBold.moon),
              ),
              const SizedBox(width: 8),
            ],
            // The controls ride along as part of the app bar rather than as a
            // second pinned sliver, which would have to account for the app
            // bar's overlap itself.
            bottom: PreferredSize(
              preferredSize: const Size.fromHeight(_controlsHeight),
              child: _Controls(
                style: _style,
                size: _size,
                count: entries.length,
                onStyle: _setStyle,
                onQuery: _setQuery,
                onSize: (s) => setState(() => _size = s),
              ),
            ),
          ),
          if (entries.isEmpty)
            SliverFillRemaining(
              hasScrollBody: false,
              child: _Empty(query: _query, style: _style),
            )
          else
            SliverPadding(
              padding: const EdgeInsets.fromLTRB(12, 12, 12, 32),
              sliver: SliverGrid.builder(
                gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                  maxCrossAxisExtent: _size * 3.6,
                  mainAxisSpacing: 4,
                  crossAxisSpacing: 4,
                  childAspectRatio: 0.82,
                ),
                itemCount: entries.length,
                itemBuilder: (context, i) => _IconTile(
                  entry: entries[i],
                  style: _style,
                  size: _size,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _Controls extends StatelessWidget {
  const _Controls({
    required this.style,
    required this.size,
    required this.count,
    required this.onStyle,
    required this.onQuery,
    required this.onSize,
  });

  final SolarIconStyle style;
  final double size;
  final int count;
  final ValueChanged<SolarIconStyle> onStyle;
  final ValueChanged<String> onQuery;
  final ValueChanged<double> onSize;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Material(
      color: theme.colorScheme.surface,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          SizedBox(
            height: 48,
            child: ListView(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 16),
              children: <Widget>[
                for (final s in SolarIconStyle.values) ...<Widget>[
                  ChoiceChip(
                    label: Text(s.label),
                    selected: s == style,
                    onSelected: (_) => onStyle(s),
                  ),
                  const SizedBox(width: 8),
                ],
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: SizedBox(
                    height: 44,
                    child: TextField(
                      onChanged: onQuery,
                      decoration: InputDecoration(
                        isDense: true,
                        hintText: 'Search $count icons…',
                        prefixIcon:
                            const Icon(SolarIconsLinear.magnifier, size: 20),
                        border: const OutlineInputBorder(),
                        contentPadding:
                            const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                Icon(SolarIconsLinear.textCircle,
                    size: 18, color: theme.hintColor),
                SizedBox(
                  width: 120,
                  child: Slider(
                    value: size,
                    min: 16,
                    max: 64,
                    onChanged: onSize,
                  ),
                ),
                SizedBox(
                  width: 34,
                  child: Text('${size.round()}',
                      style: theme.textTheme.labelMedium),
                ),
              ],
            ),
          ),
          Divider(height: 1, color: theme.dividerColor),
        ],
      ),
    );
  }
}

class _IconTile extends StatelessWidget {
  const _IconTile(
      {required this.entry, required this.style, required this.size});

  final _Entry entry;
  final SolarIconStyle style;
  final double size;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final duotone = entry.duotone;
    return InkWell(
      borderRadius: BorderRadius.circular(10),
      onTap: () => _showDetail(context, entry, style),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          if (duotone != null)
            SolarDuotoneIcon(duotone,
                size: size, color: theme.colorScheme.primary)
          else
            Icon(entry.icon, size: size, color: theme.colorScheme.primary),
          const SizedBox(height: 6),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 4),
            child: Text(
              entry.name,
              maxLines: 2,
              textAlign: TextAlign.center,
              overflow: TextOverflow.ellipsis,
              style:
                  theme.textTheme.labelSmall?.copyWith(color: theme.hintColor),
            ),
          ),
        ],
      ),
    );
  }
}

String _dartName(String solarName) {
  const overrides = <String, String>{'case': 'caseIcon', '4k': 'icon4k'};
  if (overrides.containsKey(solarName)) return overrides[solarName]!;
  final parts =
      solarName.split(RegExp(r'[^A-Za-z0-9]+')).where((p) => p.isNotEmpty);
  return parts.first.toLowerCase() +
      parts.skip(1).map((p) => p[0].toUpperCase() + p.substring(1)).join();
}

String _snippetFor(_Entry entry, SolarIconStyle style) {
  final cls =
      'SolarIcons${style.name[0].toUpperCase()}${style.name.substring(1)}';
  final ref = '$cls.${_dartName(entry.name)}';
  return style.isDuotone ? 'SolarDuotoneIcon($ref)' : 'Icon($ref)';
}

void _showDetail(BuildContext context, _Entry entry, SolarIconStyle style) {
  final snippet = _snippetFor(entry, style);
  showModalBottomSheet<void>(
    context: context,
    showDragHandle: true,
    builder: (context) {
      final theme = Theme.of(context);
      return Padding(
        padding: const EdgeInsets.fromLTRB(24, 0, 24, 40),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                for (final s in <double>[20, 32, 48, 72])
                  if (entry.duotone case final d?)
                    SolarDuotoneIcon(d,
                        size: s, color: theme.colorScheme.primary)
                  else
                    Icon(entry.icon, size: s, color: theme.colorScheme.primary),
              ],
            ),
            const SizedBox(height: 24),
            Text(entry.name,
                textAlign: TextAlign.center,
                style: theme.textTheme.titleMedium),
            Text(
              style.label,
              textAlign: TextAlign.center,
              style:
                  theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
            ),
            const SizedBox(height: 20),
            OutlinedButton.icon(
              onPressed: () {
                Clipboard.setData(ClipboardData(text: snippet));
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Copied  $snippet')),
                );
                Navigator.pop(context);
              },
              icon: const Icon(SolarIconsLinear.copy, size: 18),
              label: Text(snippet, overflow: TextOverflow.ellipsis),
            ),
          ],
        ),
      );
    },
  );
}

class _Empty extends StatelessWidget {
  const _Empty({required this.query, required this.style});

  final String query;
  final SolarIconStyle style;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          SolarDuotoneIcon(SolarIconsLineDuotone.magnifier,
              size: 56, color: theme.hintColor),
          const SizedBox(height: 12),
          Text('No ${style.label} icon matches “$query”',
              style:
                  theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
        ],
      ),
    );
  }
}
1
likes
160
points
71
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

The complete Solar Icon Set: 7,759 icons in all six styles (linear, outline, bold, broken, and both duotones) as tree-shakeable icon fonts. No dependencies.

Repository (GitHub)
View/report issues

Topics

#icons #solar #ui #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on solar_icons_flutter