Skip to content

Spec: Text ‐ API Design

Matt Carroll edited this page Feb 25, 2024 · 2 revisions

Text Widget API:

class Text extends StatefulWidget {
    /// Creates a [Text] widget that automatically attempts to localize the given
    /// [localizedTextKey].
    const Text(this.localizedTextKey, {
      this.tableName,
      this.bundle,
      this.comment,
      this.accessibility = defaultTextAccessibility,
    }) : assert(localizedString == null), assert(text == null);
    
    const Text.fromLocalizedString(this.localizedString, {
      this.accessibility = defaultTextAccessibility,
    }) : assert(localizedTextKey == null), assert(text == null);
    
    /// Creates a [Text] widget that displays the given [text] without any localization.
    const Text.verbatim(String text, {
      this.accessibility = defaultTextAccessibility,
    }) : assert(localizedTextKey == null), assert(localizedString == null);

    /// The name of a localized text key, which is used to lookup the actual content that's
    /// displayed by this [Text] widget.
    ///
    /// If no corresponding localized content can be found, the [localizedTextKey] value is
    /// displayed literally as text content.
    final String? localizedTextKey;
    
    /// The localization table name to use for [localizedTextKey] lookup - uses the default
    /// table if not provided.
    // TODO: how does this relate to Flutter's localization system?
    final String? tableName;
    
    /// The bundle containing the strings file for localization - defaults to the main bundle
    /// if not provided.
    // TODO: how does this relate to Flutter's localization system?
    final String? bundle;
    
    /// Contextual information about the combination of the [localizedTextKey] and its
    /// localized value.
    final String? comment;
    
    /// Fully configured, localized text to display as the text content within this [Text] widget.
    final LocalizedStringResource? localizedString;
    
    /// Non-localized text to display as content in this [Text] widget.
    final String? text;
    
    /// The accessibility configuration for this [Text] widget.
    final TextAccessibility accessibility;
    
    final ShapeStyle foregroundStyle;
    final ShapeStyle background;
    
    final SwiftUiFont? font;
    final FontWeights? fontWeight;
    final TextDecorations? decoration;
    final FontDesigns? fontDesign;
    final FontWidths? fontWidth;
    
    final TextStyles? style;
    final DynamicTextSizes? dynamicTextSize;
    final TextScale? scale;
    final double? minimumScaleFactor;
    final TruncationModes? truncationMode;
    final double? kerning;
    final bool allowsTightening;
    
    final double? baselineOffset;
    
    final int? lineLimit;
    final double? lineSpacing;
    final MultilineTextAlignments? multilineTextAlignment;
}

Typography classes and methods, which the Text widget requires:

class Font extends StatelessWidget {
    const Font(this.font);

    final SwiftUiFont font;
}

class SwiftUiFont {
    static const largeTitle = //
    static const title = //
    static const title2 = //
    static const title3 = //
    static const headline = //
    static const body = //
    static const callout = //
    static const subheadline = //
    static const footnote = //
    static const caption = //
    static const caption2 = //

    const SwiftUiFont.system({...});
    
    const SwiftUiFont.custom({...});
    
    final String? family;
    final FontDesigns? design;
    /// The size of the font, in points, which may change based on the system's
    /// Dynamic Type settings.
    final Points? size; // Note the typedef for Points
    /// The font, relative to which this font will apply [size] when accounting
    /// for Dynamic Type settings.
    ///
    /// If [relativeTo] is `null`, it defaults to the [body] font.
    final SwiftUiFont? relativeTo;
    /// The size of the font, in points, which remains the same regardless of
    /// the system's Dynamic Type settings.
    final Points? fixedSize;
    final FontWeightValue? weight;
    
    // TODO:
}

typedef Points = int;

class FontWeight extends StatelessWidget {
    const FontWeight(this.weight);

    final FontWeights weight;
}

// FIXME: I'm making enums plural so that we can have a modifier widget with the
//        same name. I'm not a fan of this, but I haven't found a less bad
//        alternative.
enum FontWeights {
    ultraLight,
    thin,
    light,
    regular,
    medium,
    semibold,
    bold,
    heavy,
    black;
    
    /// Returns the font-rendering weight number associated with the given 
    /// [FontWeights], e.g., `400` for [regular].
    int get asWeightNumber;
}

class FontDesign extends StatelessWidget {
    const FontDesign(this.design);

    final FontDesigns design;
}

enum FontDesigns {
    default,
    rounded,
    serif,
    monospaced;
}

class FontWidth extends StatelessWidget {
    const FontWidth(this.width);

    final FontWidths width;
}

enum FontWidths {
    compressed,
    condensed,
    standard,
    expanded;
}

enum DynamicTextSizes {
    xSmall,
    small,
    medium,
    large,
    xLarge,
    xxLarge,
    xxxLarge,
    ax1,
    ax2,
    ax3,
    ax4,
    ax5;
}

class Bold extends StatelessWidget {
    const Bold();
}

class Italic extends StatelessWidget {
    const Italic();
}

class Monospaced extends StatelessWidget {
    const Monospaced();
}

class MonospacedDigit extends StatelessWidget {
    const MonospacedDigit();
}

class Uppercase extends StatelessWidget {
    const Uppercase();
}

class Lowercase extends StatelessWidget {
    const Lowercase();
}

enum TextStyles {
    bold,
    italic,
    monospaced,
    monospacedDigit,
    uppercase,
    lowercase;
}

class Underline extends StatelessWidget {
    const Underline();
}

class Strikethrough extends StatelessWidget {
    const Strikethrough();
}

enum TextDecorations {
    underline,
    strikethrough;
}

class TruncationMode extends StatelessWidget {
    const TruncationMode(this.mode);
    
    final TruncationModes mode;
}

enum TruncationModes {
    tail,
    middle,
    head;
}

class AllowsTightening extends StatelessWidget {
    const AllowsTightening(this.allowsTightening);

    final bool allowsTightening;
}

class TextScale extend StatelessWidget {
    const TextScale(this.scale);

    final TextScales scale;
}

enum TextScales {
    default,
    secondary;
}

class MinimumScaleFactor extends StatelessWidget {
    const MinimumScaleFactor(this.scaleFactor);

    final double scaleFactor;
}

class BaselineOffset extends StatelessWidget {
    const BaselineOffset(this.offset);

    final double offset;
}

class Kerning extends StatelessWidget {
    const Kerning(this.kerning);

    final double kerning;
}

class Tracking extends StatelessWidget {
    const Tracking(this.tracking);

    final double tracking;
}

class MultilineTextAlignment extends StatelessWidget {
    const MultilineTextAlignment(this.alignment);
    
    final MultilineTextAlignments alignment;
}

enum MultilineTextAlignments {
    leading,
    center,
    trailing;
}

class LineLimit extends StatelessWidget {
    const LineLimit(this.lineLimit);

    final int? lineLimit;
}

class LineSpacing extends StatelessWidget {
    const LineSpacing(this.spacing);
    
    final double spacing;
}

class ForegroundStyle extends StatelessWidget {
    const ForegroundStyle(this.style);
    
    final ShapeStyle style;
}

class Background extends StatelessWidget {
    const Background(this.style);
    
    final ShapeStyle style;
}

abstract interface class ShapeStyle {
    // TODO: we probably need to define this method in terms of a Canvas, along
    //       with the Paragraph that needs to be painted. TextStyle probably
    //       doesn't cover enough out-of-the-box capabilities.
    TextStyle applyToText();
    
    Widget applyToBackground();
}
class Color implements ShapeStyle {
    static const black = Color(0xFF000000);
    
    const Color(this.rgba);
    
    final int rgba;

    TextStyle applyToText();
    
    Widget applyToBackground();
}

class LinearGradient implements ShapeStyle {
    TextStyle applyToText();
    
    Widget applyToBackground();
}

class AngularGradient implements ShapeStyle {
    TextStyle applyToText();
    
    Widget applyToBackground();
}

class ConicGradient implements ShapeStyle {
    TextStyle applyToText();
    
    Widget applyToBackground();
}

class EllipticalGradient implements ShapeStyle {
    TextStyle applyToText();
    
    Widget applyToBackground();
}

class RadialGradient implements ShapeStyle {
    TextStyle applyToText();
    
    Widget applyToBackground();
}

TODO: Add an explicit definition for default system fonts. On iOS, the environment sets a default system font. Typically, the default is San Francisco, which includes different families: SF Pro, SF Pro Rounded, SF Mono, etc. We need this font to change, as expected, when moving from iOS to iPadOS to macOS, etc. We also need the concept of an Apple/iOS default to play nice on other platforms. Also, we have a licensing issue - SF font can only be used on Apple devices.

Classes and methods for localization, which the Text widget requires.

class LocalizedStringResource {
    const LocalizedStringResource(this.key, {
      this.defaultValue,
      required this.locale,
      this.table,
      this.bundle,
      this.comment,
    });

    /// The name of a localized text key, which is used to lookup the actual content associated
    /// with this [LocalizedStringResource].
    ///
    /// If no corresponding localized content can be found, the [defaultValue] is used. If no
    /// [defaultValue] is provided, the [key], itself, is used as the localized value.
    final String key;
    
    /// The value to be used for this string when [key] doesn't map to any value in the
    /// given localization table.
    final String? defaultValue;
    
    /// The locale to use for localization lookup for this string.
    final Locale locale;
    
    /// The localization table name to use for [localizedTextKey] lookup - uses the default
    /// table if not provided.
    // TODO: how does this relate to Flutter's localization system?
    final String? table;
    
    /// The bundle containing the strings file for localization - defaults to the main bundle
    /// if not provided.
    // TODO: how does this relate to Flutter's localization system?
    final String? bundle;
    
    /// Contextual information about the combination of the [localizedTextKey] and its
    /// localized value.
    final String? comment;
}

/// DIRECTION: Create an `AttributedString` class using `AttributedText` for implementation.

Accessibility classes and functions, which the Text widget requires:

/// The default [TextAccessibility] configuration for a [Text] widget when no accessibility
/// configuration is provided.
const defaultTextAccessibility = TextAccessibility();

class TextAccessibility {
    const TextAccessibility({/*...*/});

    final double speechAdjustedPitch;
    final bool speechAlwaysIncludesPunctuation;
    final bool speechSpellsOutCharacters;
    final AccessibilityHeading accessibilityHeading;
    final Set<AccessibilityTrait> accessibilityTraits;
    final String accessibilityLabel;
    
    // DIRECTION: Include equality override to avoid needless widget rebuilds
}
Clone this wiki locally