COSMIC Text

repository·main·Indexed 24 days ago

https://github.com/pop-os/cosmic-text

A pure Rust library for advanced multi-line text handling, providing high-level abstractions for shaping, layout, and rendering. It supports bidirectional text, RTL, ligatures, color emoji, and font fallback with per-line and per-character granularity. The library is compatible with Linux, macOS, and Windows, and offers support for no_std environments.

Tokens
11.6K
Snippets
20
Records
66
Agent score
84%

What's inside cosmic-text

  1. Overview of COSMIC Text

    main

    COSMIC Text is a pure Rust library for advanced multi-line text handling. It provides a unified abstraction for text shaping, layout, and rendering.

    Key capabilities include:

    • Shaping: Powered by HarfRust, supporting advanced shaping operations and RTL (Right-to-Left) text.
    • Rendering: Powered by swash, supporting ligatures and color emoji.
    • Layout: Custom implementation in safe Rust that supports bidirectional text and simple wrapping.
    • Font Fallback: Custom implementation with per-line and per-character granularity, utilizing logic similar to Chromium and Firefox.
    • Text Editing: Supports text selection and bidirectional selection.
    • Platform Support: Full feature set available on Linux, macOS, and Windows. Other platforms may require manual implementation of font fallback.
  2. Use COSMIC Text in no_std environments

    main

    COSMIC Text supports no_std environments. To use it in a no_std context, you must disable default features in your Cargo.toml.

    Note that while shaping and layout are supported in no_std, font loading and rendering currently require standard library features.

  3. Shape and layout a `BufferLine`

    main

    BufferLine uses a caching mechanism for expensive operations like shaping and layout. You must explicitly call shape and layout to populate these caches.

    1. Shaping

    Call shape to convert text and attributes into a ShapeLine. This caches the result. If the text or attributes change, the cache is invalidated.

    2. Layout

    Call layout to convert the shaped line into a collection of LayoutLine objects. This requires a FontSystem and parameters like font size, width, and wrapping rules. This also caches the result.

    3. Rendering

    Use layout_runs to get an iterator of visible layout runs, which is useful for rendering the line within a specific height and line height.

    Checking Cache Validity

    You can check if the line needs to be re-processed using needs_reshaping(), which returns true if either the shaping or layout caches have been invalidated.

  4. Configure text alignment

    main

    When calling layout_to_buffer, you can specify an Align value to control how lines are positioned within the available width. If None is provided, the alignment defaults to Align::Left for LTR text and Align::Right for RTL text.

    Supported alignment modes:

    • Align::Left: Aligns text to the left (or right in RTL).
    • Align::Right: Aligns text to the right (or left in RTL).
    • Align::Center: Centers the text within the line width.
    • Align::End: Aligns text to the end of the line.
    • Align::Justified: Distributes extra space between words to stretch the line to the full width (note: the last line of a paragraph is typically not justified).
  5. Manage undo/redo using Change and ChangeItem

    main

    Text modifications are encapsulated in Change and ChangeItem structures. This allows for grouping multiple atomic operations into a single logical undoable unit.

    • ChangeItem: Represents a single atomic change. It contains the start and end cursors, the text involved, and an insert boolean (true for insertion, false for deletion).
    • Change: A collection of ChangeItems representing a single logical transaction.

    Both ChangeItem and Change provide a reverse() method to invert the operation, which is essential for implementing undo functionality.

    #[derive(Clone, Debug)]
    pub struct ChangeItem {
        pub start: Cursor,
        pub end: Cursor,
        pub text: String,
        pub insert: bool,
    }
    
    #[derive(Clone, Debug, Default)]
    pub struct Change {
        pub items: Vec<ChangeItem>,
    }
    
    impl ChangeItem {
        pub fn reverse(&mut self) {
            self.insert = !self.insert;
        }
    }
    
    impl Change {
        pub fn reverse(&mut self) {
            self.items.reverse();
            for item in &mut self.items {
                item.reverse();
            }
        }
    }
  6. How font fallback works in COSMIC Text

    main

    When a requested font cannot render certain characters, cosmic-text uses a fallback mechanism to find suitable alternatives. The search order is generally:

    1. Default Families: The system first attempts to use the requested font families (including Monospace if applicable).
    2. Script-Specific Fallbacks: If the character belongs to a specific script, the system checks the script_fallback list provided by the Fallback implementation.
    3. Common Fallbacks: If script-specific fallbacks fail, the system iterates through the common_fallback list.
    4. Other/Forbidden Fallbacks: Finally, it may check other available fonts, provided they are not in the forbidden_fallback list.

    For Monospace fonts, the system also uses MonospaceFallbackInfo to prioritize fonts that minimize font_weight_diff and codepoint_non_matches (the number of characters in a word that the font cannot support).

  7. Set the text base direction

    main

    The Direction enum determines the paragraph-level base direction used during shaping:

    • Direction::Auto (Default): Detects the base direction from the first strong character in the paragraph.
    • Direction::LeftToRight: Forces a left-to-right base direction.
    • Direction::RightToLeft: Forces a right-to-left base direction.
    pub enum Direction {
        #[default]
        Auto,
        LeftToRight,
        RightToLeft,
    }
  8. Choose a text shaping strategy

    main

    The Shaping enum defines how text is converted into glyphs. You can choose between two strategies:

    1. Shaping::Basic: A very cheap strategy with no font fallback. It will not handle complex scripts properly or find missing glyphs in system fonts. Use this only if you have complete control over the text and the font.
    2. Shaping::Advanced: Provides advanced text shaping and font fallback. This is required for complex scripts, fonts requiring specific shaping features, or when multiple fonts are needed to display all glyphs in a text run.
    pub enum Shaping {
        /// Basic shaping with no font fallback.
        #[cfg(feature = "swash")]
        Basic,
        /// Advanced text shaping and font fallback.
        Advanced,
    }
  9. Use BufferRef to manage buffer ownership

    main

    The BufferRef enum provides a way to handle a Buffer regardless of whether it is owned, borrowed, or shared via an Arc. This abstraction is used by the Edit trait to allow different editor implementations to manage their underlying data consistently.

    #[derive(Debug)]
    pub enum BufferRef<'buffer> {
        Owned(Buffer),
        Borrowed(&'buffer mut Buffer),
        Arc(Arc<Buffer>),
    }
  10. How to use Buffer with BorrowedWithFontSystem

    main

    The BorrowedWithFontSystem wrapper is a convenience pattern that manages the relationship between a Buffer and a FontSystem. It ensures that whenever you perform layout-dependent tasks (like hit, layout_runs, or cursor_motion), the buffer is automatically shaped and up-to-date.

    Key methods available on the wrapper:

    • shape_until_scroll(prune): Shapes lines until the scroll position.
    • layout_runs(): Returns an iterator over visible runs (automatically shapes).
    • hit(x, y): Performs hit detection (automatically shapes).
    • cursor_motion(...): Moves the cursor (uses the internal font system).
    • set_metrics(...), set_size(...), etc.: Configures the underlying buffer.
  11. Configure text wrapping and ellipsizing

    main

    The layout_to_buffer method supports several strategies for wrapping text and handling overflow via ellipsizing.

    Wrapping Strategies (Wrap)

    • Wrap::None: No wrapping; text continues on a single line.
    • Wrap::Word: Breaks lines at word boundaries.
    • Wrap::WordOrGlyph: Breaks at word boundaries, but falls back to breaking at glyph boundaries if a single word is wider than the available width.
    • Wrap::Glyph: Breaks lines at any glyph boundary.

    Ellipsizing Strategies (Ellipsize)

    Ellipsizing can be applied to limit the number of lines or the total height:

    • Ellipsize::Start(limit): Ellipsizes the beginning of the text.
    • Ellipsize::Middle(limit): Ellipsizes the middle of the text.
    • Ellipsize::End(limit): Ellipsizes the end of the text.

    Limits can be defined by:

    • EllipsizeHeightLimit::Lines(usize): A maximum number of lines.
    • EllipsizeHeightLimit::Height(f32): A maximum total height in pixels.
  12. Quickstart: Basic text shaping and layout with COSMIC Text

    main

    To use COSMIC Text, follow this general workflow:

    1. Create a FontSystem to access system fonts (create one per application).
    2. Create a SwashCache to store rasterized glyphs (create one per application).
    3. Define Metrics for font size and line height.
    4. Create a Buffer using the FontSystem and Metrics (create one per text widget).
    5. Use Attrs to specify font choices.
    6. Set the buffer size and text using set_size and set_text.
    7. Inspect the layout via layout_runs() or draw the buffer using draw().

    Note: For high-performance rendering, it is recommended to use SwashCache directly rather than the buffer.draw() convenience method.

    use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping};
    
    // A FontSystem provides access to detected system fonts, create one per application
    let mut font_system = FontSystem::new();
    
    // A SwashCache stores rasterized glyphs, create one per application
    let mut swash_cache = SwashCache::new();
    
    // Text metrics indicate the font size and line height of a buffer
    let metrics = Metrics::new(14.0, 20.0);
    
    // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
    let mut buffer = Buffer::new(&mut font_system, metrics);
    
    // Borrow buffer together with the font system for more convenient method calls
    let mut buffer = buffer.borrow_with(&mut font_system);
    
    // Attributes indicate what font to choose
    let attrs = Attrs::new();
    
    // Set size and text
    buffer.set_size(Some(80.0), Some(25.0));
    buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced, None);
    
    // Inspect the output runs
    for run in buffer.layout_runs() {
        for glyph in run.glyphs.iter() {
            println!("{:#?}", glyph);
        }
    }
    
    // Create a default text color
    let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
    
    // Draw the buffer (for performance, instead use SwashCache directly)
    buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
        // Fill in your code here for drawing rectangles
    });