Pure Rust multi-line text handling

Overview

COSMIC Text

crates.io docs.rs license Rust workflow

Pure Rust multi-line text handling.

COSMIC Text provides advanced text shaping, layout, and rendering wrapped up into a simple abstraction. Shaping is provided by rustybuzz, and supports a wide variety of advanced shaping operations. Rendering is provided by swash, which supports ligatures and color emoji. Layout is implemented custom, in safe Rust, and supports bidirectional text. Font fallback is also a custom implementation, reusing some of the static fallback lists in browsers such as Chromium and Firefox. Linux, macOS, and Windows are supported with the full feature set. Other platforms may need to implement font fallback capabilities.

Screenshots

Arabic translation of Universal Declaration of Human Rights Arabic screenshot

Hindi translation of Universal Declaration of Human Rights Hindi screenshot

Simplified Chinese translation of Universal Declaration of Human Rights Simplified Chinses screenshot

Roadmap

The following features must be supported before this is "ready":

  • Font loading (using fontdb)
    • Preset fonts
    • System fonts
  • Text styles (bold, italic, etc.)
    • Per-buffer
    • Per-span
  • Font shaping (using rustybuzz)
    • Cache results
    • RTL
    • Bidirectional rendering
  • Font fallback
    • Choose font based on locale to work around "unification"
    • Per-line granularity
    • Per-character granularity
  • Font layout
    • Click detection
    • Simple wrapping
    • Wrapping with indentation
    • No wrapping
    • Ellipsize
  • Font rendering (using swash)
    • Cache results
    • Font hinting
    • Ligatures
    • Color emoji
  • Text editing
    • Performance improvements
    • Text selection
    • Can automatically recreate https://unicode.org/udhr/ without errors (see below)
    • Bidirectional selection
    • Copy/paste
  • no_std support (with default-features = false)
    • no_std font loading
    • no_std shaping
    • no_std layout
    • no_std rendering

The UDHR (Universal Declaration of Human Rights) test involves taking the entire set of UDHR translations (almost 500 languages), concatenating them as one file (which ends up being 8 megabytes!), then via the editor-test example, automatically simulating the entry of that file into cosmic-text per-character, with the use of backspace and delete tested per character and per line. Then, the final contents of the buffer is compared to the original file. All of the 106746 lines are correct.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Comments
  • Fix word wrapping in `ShapeLine::layout`

    Fix word wrapping in `ShapeLine::layout`

    Playing around with the editor-libcosmic example I encountered a crash when using word wrapping, because the first word was larger than the line. This is caused by this line https://github.com/pop-os/cosmic-text/blob/1bc198fd6306923e797dcf022439faf44c343af0/src/shape.rs#L782, because i is 0.

    ~After fixing that I also noticed that if a word is larger than the line, it will cause an overflow. I think almost everywhere word wrapping implies wrapping at the glyph level, if the word does not fit the line, so I believe users will expect that when using Wrap::Word.~

    ~Therefore I changed the ShapeLine::layout method to wrap at the glyph level (Edit: see https://github.com/pop-os/cosmic-text/pull/98#issuecomment-1456340645 and other comments in this PR), if the wrapped word does not fit the new line~.

    I noticed some more issues with the current implementation:

    • https://github.com/pop-os/cosmic-text/blob/1bc198fd6306923e797dcf022439faf44c343af0/src/shape.rs#L707 In the incongruent directions case this must be i + 1.
    • https://github.com/pop-os/cosmic-text/blob/1bc198fd6306923e797dcf022439faf44c343af0/src/shape.rs#L782 In both direction cases this ignores the fact that the current word could be the first in the line. However, it could be that this might work regardless, but that doesn't seem obvious.
    • https://github.com/pop-os/cosmic-text/blob/1bc198fd6306923e797dcf022439faf44c343af0/src/shape.rs#L738 This should be (i, 0).

    ~My implementation works by moving the Wrap::Glyph wrapping part behind the Wrap::Word case and executing it unless continue is used to skip it. Because the Wrap::None case is handled before the loop, we only have to deal with Wrap::Word and Wrap::Glyph so that should work.~

    I've also renamed the local word_size variable to word_width for more consistency and because I think it's a more precise name.

    I replaced the fetching of the previous word with a local trailing_space_width variable, which keeps track of the width of the last processed word in the current line. It is only Some(_) if we already processed a word in the current line and that word was blank.

    opened by geieredgar 15
  • example `editor-libcosmic` does not properly apply scale

    example `editor-libcosmic` does not properly apply scale

    Running the editor-libcosmic example, I get this:

    image

    There is no text rendered anywhere. Clicking on the big textfield and typing does nothing. Opening a file changes nothing. The pulldown menus and toggle buttons respond, but do nothing.

    opened by WhyNotHugo 10
  • Text is blurry

    Text is blurry

    I'm on macOS 13.2.1, with M1. When I run the editor-libcosmic example, the text looks blurry compared to native text. Some time ago I head this issue with nuklear. It was related to antialiasing. (Notice also on the screenshot some symbols aren't rendered, but I'm not sure whether it's related to cosmic-text.) Screenshot 2023-03-08 at 17 39 40

    opened by ales-tsurko 8
  • Make `FontSystem` not self-referencing and update `fontdb` and `rustybuzz`

    Make `FontSystem` not self-referencing and update `fontdb` and `rustybuzz`

    This makes the std implementation of FontSystem not self-referencing, which in turn allows defining a db_mut method for mutable access to the underlying fontdb::Database (requested in #75).

    The high level changes are:

    • Adds a new FontKey struct that stores the fontdb::ID and (if the swash feature is enabled) the swash key data (u32, swash::CacheKey).
      • A FontKey can be obtained via the new FontSystem::get_font_key(id).
      • FontKeys are cached instead of caching Fonts.
    • FontSystem::get_font now takes a FontKey instead of fontdb::ID and creates a Font on the fly, reusing the key data.
    • Removes FontMatches.
      • FontSystem::get_font_matches now returns a Arc<Vec<FontKey>> instead of Arc<FontMatches<'a>>.
      • FontFallbackIter borrows fontdb::Database and creates the font on the fly using Font::from_key(db, key).
    opened by geieredgar 7
  • 96.8MB memory usage in rich-text example

    96.8MB memory usage in rich-text example

    Originally I found this issue in the advanced text branch for iced. My system reports that 96.8MB is used when running. It seems like a bit much to use that much memory for just displaying some text. Iced gave memory usage of around 100MB for the checkbox example on the advanced-text branch.

    I ran dhat from valgrind for iced and it seems that some parts of rustybuzz appears to be the cause of 12MB of memory usage.

    I am running on Linux on aarch64.

    opened by i509VCB 6
  • FontSystem::new() takes a significant amount of time

    FontSystem::new() takes a significant amount of time

    On my system (Xubuntu), running FontSystem::new() takes a significant amount of time. On release, it takes around a second to run. On debug, it takes up to ten seconds.

    There should probably be some measures taken to make sure FontSystem runs quickly. However, it should also be noted somewhere that FontSystem takes a while to load.

    opened by notgull 5
  • Use `f32` instead of `i32` for lengths

    Use `f32` instead of `i32` for lengths

    This allows users to use logical coordinates instead of physical ones.

    These changes seem to fit naturally since fewer conversions are necessary during layouting. I left the editor module largely unchanged because I am not familiar with it, but I imagine it can be ported to logical units as well.

    opened by hecrj 5
  • Bidi reordering and wrapping

    Bidi reordering and wrapping

    Previously we were calling unicode_bidi::bidi_info::visual_run to reorder the embedded bidi text. This was working well for texts on one line, but wrapping text would break the algorithm.

    I had to move the reordering algorithm into cosmic_text to be able to adapt it. Also I had to change the ShapeLine::layout function to create the LayoutLine at the end, after reordering.

    Currently I can not do wrap_simple because I am using a Vec<span_index, word_range> to represent a line. But to be able to do wrap_simple I need to know where in a word the line break happens.

    More detail: So with this PR, if we have a big chunk of LTR text that needs multiple visual lines. It would become one single Span with index 0 and the visual lines would be [ [(0, 0..15)], [(0, 15..34)], ..] (which is a Vec<Vec<usize, Range>>). Now if we have two small chunks of text, one 5 words LTR, one 4 words RTL and it would fit on one line. It would generate two Spans (indices 0,1) and the Visual line would be [[(0, 0..5), (1, 0..4)]].

    TODO:

    • [x] Correct BiDi ordering per visual line
    • [x] wrap_simple
    • [x] Skip first space on a new line
    • [x] A long sequence of whitespaces is considered a word and causes issues with wrapping
    • [x] Words longer than the line_width break the layout (in the main branch too).
    opened by hojjatabdollahi 5
  • Fix a few bugs in layout causing the text to overflow or disappear

    Fix a few bugs in layout causing the text to overflow or disappear

    While testing #98 I noticed a few layouting issues. These issues are in the main branch too.

    One issue was in BiDi text, sometimes there was an overflow like the word "Left" in this picture:

    Screenshot from 2023-03-12 21-47-01

    This issue sometimes causes the last word to repeat or to disappear.

    Another issue was in the LTR-only lines (or RTL-only), the space before the last word would disappear if the linewidth was a specific size. Like in the image below:

    Screenshot from 2023-03-12 21-52-05

    In this PR I fix these issues by changing the layout algorithm a little. Previously we used to create word_ranges and later put them together in a visual_line but now the visual_line is created while iterating over the words. This simplifies the algorithm and fixes the issue mentioned above.

    This PR supersedes #98. I tried to apply these changes to @geieredgar 's PR, but the algorithm they used for detecting trailing spaces would not work in specific situations in BiDi. So, I decided to open a new PR.

    opened by hojjatabdollahi 4
  • feature request: Add text justification

    feature request: Add text justification

    I am currently developing a cosmic-text-based implementation of the piet text API, see here. The only bits from piet that are currently unimplemented are variable font sizes (already raised in #64) and text justification.

    piet expects there to be utilities for text justification, with start (default), right, center and justified alignments. See this enum for more information.

    Are there any plans to add text alignment to cosmic-text?

    opened by notgull 4
  • Add line width field to `LayoutLine`

    Add line width field to `LayoutLine`

    This PR introduces a w field to LayoutLine and a line_w to LayoutRun.

    These are useful to avoid iterating through all the glyphs just to figure out the line width (e.g. for alignment purposes).

    opened by hecrj 4
  • Ability to obtain baseline Y offset

    Ability to obtain baseline Y offset

    I try to utilize cosmic-text to draw text to a bitmap and to obtain a tight bounding box around the result. Right now, I am confused by the calculation of the vertical offset of the text. To elaborate further, let us assume that we draw a single glyph, i.e. the letter "g", using a Buffer. For the sake of simplicity, we set line_height == font_size == 72.0 (i.e. we do not apply leading aka additional distance between the lines). When we call Buffer::draw(), the code first creates a LayoutRunIter whose line_y property is initialized as follows:

    https://github.com/pop-os/cosmic-text/blob/bfb5eefbfa869915e47824877af68a5307cf301c/src/buffer.rs#L215

    In our case, y_offset() evaluates to 0.0 (no leading, see above) and therefore, our run starts at a vertical offset of 0.0. Now, we call next() on the iterator to draw the first run and observe that line_y is incremented by the line height which is equal to the font size (aka 72.0):

    https://github.com/pop-os/cosmic-text/blob/bfb5eefbfa869915e47824877af68a5307cf301c/src/buffer.rs#L241

    Finally, we obtain the glyph image from `swash' and draw it to the given offset:

    https://github.com/pop-os/cosmic-text/blob/bfb5eefbfa869915e47824877af68a5307cf301c/src/buffer.rs#L718-L720

    run.line_y still evaluates to 72.0 in our example. However, if I understand correctly, the swash image is positioned relatively to the baseline of the glyph. I did not find explicit confirmation for this assumption in the swash documentation, but the swash_demo repo provides an extensive sample for this. When drawing its layout, it incorporates the baseline into the vertical offset as seen here:

    https://github.com/dfrg/swash_demo/blob/55aedbc8604201f1b4925e2b986db59b838dc062/src/main.rs#L493

    In opposition, cosmic-text effectively places the baseline at the bottom of the line. When I try to draw my "g" glyph to the top-left corner of a bitmap and set my bounding box height to 72 pix, I end up with the following result:

    g

    How to fix this? I am not adept in typography, but for my understanding, the vertical offset must be corrected by the descent of the font as displayed here:

    glyph

    Indeed, to draw my glyphs to the bitmap, I tried to calculate the baseline of the font by myself to apply this correction. This approach works, but cosmic-text makes it pretty hard to implement it. Basically, I have to layout my line, obtain the FontID from the glyphs, query the Font from the FontSystem and do something like this:

    let metrics = font.as_swash().metrics(&[]);
    let total_height = metrics.ascent + metrics.descent;
    let baseline = font_size * metrics.ascent / total_height;
    

    In fact, this is about the same calculation that is done in the swash demo, see here:

    https://github.com/dfrg/swash_demo/blob/d08ced5f1e92b62cd637415c1f66c440d737b31b/src/layout/line_breaker.rs#L324-L326

    By using baseline instead of font_size (resp. the total line height) as vertical offset, I finally get the correct result. I can live with this approach, but it would be nice if cosmic-text made it easier for me to access the baseline offsets of the layout lines.

    Sorry for the long explanation. I hope the issue has been made clear - and thanks for your awesome work on this crate!

    opened by JayTee42 1
  • `Shaping` strategy selection

    `Shaping` strategy selection

    This PR introduces a Shaping enum that allows users to control the shaping strategy used.

    A new Basic shaping strategy is also added which naively positions glyphs and skips rustybuzz completely.

    opened by hecrj 0
  • Panic with strange file

    Panic with strange file

    Modifying editor-test

        let text = if let Some(arg) = env::args().nth(1) {
            fs::read_to_string(&arg).expect("failed to open file")
        } else {
            #[cfg(feature = "mono")]
            let default_text = include_str!("../../../sample/a.txt");
            #[cfg(not(feature = "mono"))]
            let default_text = include_str!("../../../sample/a.txt");
            default_text.to_string()
        };
    

    and putting to sample file - ss.txt.zip

    crash app with info

    thread 'main' panicked at 'assertion failed: `(left == right)`
      left: `Cursor { line: 0, index: 11747, affinity: After }`,
     right: `Cursor { line: 0, index: 11743, affinity: After }`', examples/editor-test/src/main.rs:99:21
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    opened by qarmin 0
  • Font fallback fails if main font's properties don't match fallback font

    Font fallback fails if main font's properties don't match fallback font

    It seems like font fallback generally works pretty well, but if a certain property, such as italics are not available for the fallback font, the font is not chosen at all and tofus are rendered instead.

    Here's an example where Calibri is the main font. Calibri by itself does not support Hangul, so it falls back to a different font (seems to be Malgun Gothic). This works fine until you choose Calibri Italic, at which point it can't find an italic version of Malgun Gothic and shows tofus.

    image

    This may or may not be related to #58. I'm surprised the fallback is rejected if found in the first place though, so it may not actually be related.

    opened by CryZe 0
  • BIDI Layout is

    BIDI Layout is "random"

    Depending on whether it's a mixed layout or not, the arabic characters are either reversed or not:

    https://i.imgur.com/WNUdgIy.png

    This is with the latest master.

    Here you see a debugger view of just the arabic characters:

    https://i.imgur.com/scFc9kv.png

    This may or may not be intended, but is super weird to deal with. To properly handle this I need to properly mix the shape line's BIDI level with the shape span's BIDI level and then depending on what combination it is, iterate the glyphs in reverse or not.

    I don't even understand why cosmic-text (sometimes) reverses what rustybuzz spits out in the first place, as I need to unreverse it to draw the glyphs in the proper order. That seems like a lot of wasted work that cosmic-text does.

    opened by CryZe 12
Releases(0.8.0)
  • 0.8.0(Apr 3, 2023)

    Added

    • FontSystem::new_with_fonts helper
    • Alignment and justification
    • FontSystem::db_mut provides mutable access to fontdb database
    • rustybuzz is re-exported

    Fixed

    • Fix some divide by zero panics
    • Redox now uses std FontSystem
    • Layout system improvements
    • BufferLinke::set_text has been made more efficient
    • Fix potential panic on window resize

    Changed

    • Use f32 instead of i32 for lengths
    • FontSystem no longer self-referencing
    • SwashCash no longer keeps reference to FontSystem

    Removed

    • Attrs::monospaced is removed, use Family::Monospace instead
    Source code(tar.gz)
    Source code(zip)
Owner
Pop!_OS
An Operating System by System76
Pop!_OS
A library that creates a terminal-like window with feature-packed drawing of text and easy input handling. MIRROR.

BearLibTerminal provides a pseudoterminal window with a grid of character cells and a simple yet powerful API for flexible textual output and uncompli

Tommy Ettinger 43 Oct 31, 2022
Application microframework with command-line option parsing, configuration, error handling, logging, and shell interactions

Abscissa is a microframework for building Rust applications (either CLI tools or network/web services), aiming to provide a large number of features w

iqlusion 524 Dec 26, 2022
An implementation of Piet's text interface using cosmic-text

piet-cosmic-text Implements piet's Text interface using the cosmic-text crate. License piet-cosmic-text is free software: you can redistribute it and/

John Nunley 7 Mar 12, 2023
Rust CLI utility library. Error handling, status reporting, and exit codes.

narrate This library provides CLI application error and status reporting utilities. The coloured output formatting aims to be similar to Cargo. Error

Christopher Morton 5 Nov 2, 2022
Schemars is a high-performance Python serialization library, leveraging Rust and PyO3 for efficient handling of complex objects

Schemars Introduction Schemars is a Python package, written in Rust and leveraging PyO3, designed for efficient and flexible serialization of Python c

Michael Gendy 7 Nov 21, 2023
Rustato: A powerful, thread-safe global state management library for Rust applications, offering type-safe, reactive state handling with an easy-to-use macro-based API.

Rustato State Manager A generical thread-safe global state manager for Rust Introduction • Features • Installation • Usage • Advanced Usage • Api Refe

BiteCraft 8 Sep 16, 2024
Building blocks for handling potentially unsafe statics.

Grounded Building blocks for handling potentially unsafe statics. This crate aims to provide useful and sound components that serve as building blocks

James Munns 3 Nov 28, 2023
plonky2 recursion framework handling different circuits in unified way.

generic_recursion Version: 0.1.0 generic_recursion is a crate that allows to easily aggregate an unlimited amount of plonky2 proofs, generated with a

null 6 Mar 4, 2024
Ergonomic and precise error handling provided by error sets. Inspired by Zig's error set type.

Error Set Error Set simplifies error management by providing a streamlined method for defining errors and easily converting between them. Resultingly,

Henry 61 Jul 24, 2024
ergonomic and precise error handling built atop type-level set arithmetic

terrors - the Rust error handling library Handling errors means taking a set of possible error types, removing the ones that are locally addressible,

Komora 190 Jul 27, 2024
colorStyle is a library of styles for command-line text write in Rust.

Colorstyle colorStyle is a library of styles for command-line text. Inspired by flylog/colorstyle (golang) Example let text = colorstyle::green("gre

Code Translation 6 Nov 12, 2022
A command-line utility which aligns a block of text within the terminal (or a specified number of columns), written in Rust.

align: a command line utility for aligning text. ⭐ Overview Aligns text within the terminal (or a specified number of columns). The text is treated as

Khalil Ouali 6 Aug 11, 2023
hj is a command line tool to convert HTTP/1-style text into JSON

hj hj is a command line tool to convert HTTP/1-style text into JSON. This command is inspired by yusukebe/rj, which is a standalone HTTP client that s

FUJI Goro 10 Aug 21, 2022
A robust, customizable, blazingly-fast, efficient and easy-to-use command line application to uwu'ify your text!

uwuifyy A robust, customizable, blazingly-fast, efficient and easy-to-use command line application to uwu'ify your text! Logo Credits: Jade Nelson Tab

Hamothy 43 Dec 12, 2022
Small command-line tool to switch monitor inputs from command line

swmon Small command-line tool to switch monitor inputs from command line Installation git clone https://github.com/cr1901/swmon cargo install --path .

William D. Jones 5 Aug 20, 2022
Checkline: checkbox line picker for stdin line input

checkline is a Unix command line interface (CLI) terminal user interface (TUI) that prompts you to check each line of stdin, to pick each line to output to stdout

SixArm 4 Dec 4, 2022
Command-line HTTP client for sending a POST request to specified URI on each stdin line.

line2httppost Simple tool to read lines from stdin and post each line as separate POST request to a specified URL (TCP connection is reused though). G

Vitaly Shukela 3 Jan 3, 2023
Show unused code from multi-crate Rust projects

Warnalyzer Remove unused code from multi-crate Rust projects. The dead_code lint family of rustc is limited to one crate only and thus can't tell whet

null 75 Dec 27, 2022