GardenMobile & interfaces

Custom icon fonts and styled text in Flutter

An earlier SVG-to-font workflow and three small text/controller experiments.

An icon font maps selected glyphs to code points. The original workflow imported SVG paths into IcoMoon, generated a font, and registered the resulting .ttf in Flutter.

Prepare and register the glyphs

The source used Sketch’s Convert to Outlines when an exported SVG was rejected, then exported paths again. Tool dialogs vary; the important boundary is whether the font generator receives the intended vector geometry.

The earlier registration and usage notes were:

flutter:  uses-material-design: true  assets:    - assets/base/icons/    - assets/base/fonts/    - assets/app/icons/    - assets/app/fonts/fonts:    - family: icomoon      fonts:        - asset: assets/app/fonts/icomoon/icomoon.ttf
Text('hello \ue901', style: normalTextStyle(13, color: Colors.white, fontFamily: 'icomoon'),),# or with IconDatastatic const IconData home = IconData(0xe901, fontFamily: 'icomoon', color: Colors.white);

Those fragments lost line boundaries in the source, and the IconData example places a color argument at the wrong layer. They are preserved as text, not a validated configuration. A code point only works when its registered font matches the intended mapping.

Text and controller experiments

Read the styled-text and editing examples
import 'package:nft/services/safety/base_stateful.dart';
import 'package:flutter/material.dart';

/// Use
/// const WChatText(text: 'hello:heart.86ac068::smile.48f82f2::fire.9f77c64:'),
class WChatText extends StatefulWidget {
  const WChatText({Key key, @required this.text}) : super(key: key);

  final String text;

  @override
  _WChatTextState createState() => _WChatTextState();
}

class _WChatTextState extends BaseStateful<WChatText> {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container(
      child: RichText(
        text: _buildEmojiTextSpan(_standardizeText(widget.text)),
      ),
    );
  }

  // Standardize text
  // hello:heart.86ac068::smile.48f82f2::fire.9f77c64:
  // =>
  // hello\ue901\ue902\ue900
  String _standardizeText(String text) {
    text = text?.replaceAll(RegExp(r':fire.\w+:'), '\ue900');
    text = text?.replaceAll(RegExp(r':heart.\w+:'), '\ue901');
    text = text?.replaceAll(RegExp(r':smile.\w+:'), '\ue902');
    return text;
  }

  // Get emoji color from code
  Color _getEmojiColor(int emojiCode) {
    switch (emojiCode) {
      case 0xe900:
        return const Color(0xFFFFD243);
      case 0xe901:
        return const Color(0xFFFF39AF);
      case 0xe902:
        return const Color(0xFF2CD7FF);
    }
    return null;
  }

  // Build text span
  TextSpan _buildEmojiTextSpan(String text) {
    if (text == null) {
      return const TextSpan();
    }
    final List<TextSpan> children = <TextSpan>[];
    // integer Unicode code points
    final Runes runes = text.runes;

    final List<int> chunk = <int>[];
    for (int i = 0; i < runes.length; i++) {
      final int code = runes.elementAt(i);
      // print('code ${code.toRadixString(16)}');

      // we assume that everything that is not
      // in Extended-ASCII set is an emoji...
      final bool isEmoji = code > 255;
      if (!isEmoji) {
        chunk.add(code);
      }
      if (isEmoji || i == runes.length - 1) {
        children.add(
          TextSpan(
            text: String.fromCharCodes(chunk),
            style: const TextStyle(),
          ),
        );
        chunk.clear();
        if (isEmoji) {
          children.add(
            TextSpan(
              text: String.fromCharCode(code),
              style: TextStyle(
                fontFamily: 'nft',
                color: _getEmojiColor(code),
              ),
            ),
          );
        }
      }
    }
    return TextSpan(children: children);
  }
}
class AppTextEditingController extends TextEditingController {
  @override
  TextSpan buildTextSpan({TextStyle style, bool withComposing}) {
    final TextSpan textSpan =
        super.buildTextSpan(style: style, withComposing: withComposing);

    if (text != null) {
      return _buildEmojiTextSpan(text);
    }
    return textSpan;
  }

  // Get emoji color from code
  Color _getEmojiColor(int emojiCode) {
    switch (emojiCode) {
      case 0xe900:
        return const Color(0xFFFFD243);
      case 0xe901:
        return const Color(0xFFFF39AF);
      case 0xe902:
        return const Color(0xFF2CD7FF);
    }
    return null;
  }

  // Build text span
  TextSpan _buildEmojiTextSpan(String text) {
    if (text == null) {
      return const TextSpan();
    }
    final List<TextSpan> children = <TextSpan>[];
    // integer Unicode code points
    final Runes runes = text.runes;

    final List<int> chunk = <int>[];
    for (int i = 0; i < runes.length; i++) {
      final int code = runes.elementAt(i);
      // print('code ${code.toRadixString(16)}');

      // we assume that everything that is not
      // in Extended-ASCII set is an emoji...
      final bool isEmoji = code > 255;
      if (!isEmoji) {
        chunk.add(code);
      }
      if (isEmoji || i == runes.length - 1) {
        children.add(
          TextSpan(
            text: String.fromCharCodes(chunk),
            style: const TextStyle(),
          ),
        );
        chunk.clear();
        if (isEmoji) {
          children.add(
            TextSpan(
              text: String.fromCharCode(code),
              style: TextStyle(
                fontFamily: 'nft',
                color: _getEmojiColor(code),
              ),
            ),
          );
        }
      }
    }
    return TextSpan(children: children);
  }
}
// Insert text to TextField
  void insertText(String insert, TextEditingController controller) {
    final int cursorPos = controller.selection.base.offset;
    controller.value = controller.value.copyWith(
        text: controller.text.replaceRange(max(cursorPos, 0), max(cursorPos, 0), insert),
        selection: TextSelection.fromPosition(TextPosition(offset: max(cursorPos, 0) + insert.length))
    );
  }

Sources: styled text revision, controller revision, and insertion revision. Selection, composing text, fallback fonts, and controller lifetime need review. No font or application was generated for this revision.

← Back to the garden