Ellipsize (#467)

* feat: add Ellipsize enum

* chore: API changes needed for ellipsize

Decided not to change "layout()" function for now to avoid breaking the
interface. For now.

* chore: shape ellipsis

* feat: Ellipsize::Start

Since it can only have 1 line, it's easier to implement.

* DROPME: temporarily change rich-text for testing

* test(ellipsize): Testing Ellipsize::Start

Long text in small buffer should produce ellipsis glyphs

* fix: do not need font_system anymore

We moved ellipsis shaping elsewhere so no need to pass font_system to
layout function (which also was recreating a new one in the tests every
time making them take forever).

* feat: Ellipsize::End

* improv(ellipsize): use a single ellipsis shape

* improv: Ellipsie::End && Wrap::None

There is no need to layout the whole line if it's not going to fit.

* fix: mixed bidi text when Ellipsize::End && Wrap::None

* chore: clean up and simplify when line.RTL==span.RTL

* fix(ellipsize): last word is not (word_count -1) if iter().rev()

* refactor(layout): extract the layout algorithm to make it more readable

* improv(ellipsize): Ellipsize::Start && Wrap::None

we iterate in reverse and only layout what's going to be visible

* Revert: delete the previous approach of post processing ellipsis

* doc: explain the interaction between Ellipsize and Wrap

* chore: lower the scope

* feat: Ellipsize the last line of a paragraph

For now only the number of lines is supported

* fix: clear ellipsized field on visual lines

This was causing ellipsis to show on random lines

* chore: remove old tests

will add better tests soon

* chore: clean up changes from previous attempt

* fix: consider the ellipsis width when doing alignment

* feat(ellipsize): add `Height` limit to `Ellipsize`

* fix: ellipsize the start of the last line correctly

* fix: ellipsize at the start of mixed bidi lines

* feat: Ellipsize::Middle

* fix: consider ellipsize::middle when calculating alignment correction

* refactor: improve readability

* refactor: deduplicate "fit_glyphs"

* refactor: combine backward and forward layout into one (wip)

* fix: Backward works in the unified layout_spans

* chore: clean up

* fix: Ellipsize::Middle

* fix: handle large words in bidi boundaries

* chore: clean up and some refactoring

* fix: ellipsis is now the same level as the surrounding text

* fix: try to fit more when ellipsizing::middle

* improv: ellipsis now have the same level as the neighbors

This makes ellipsized RTL text inside a LTR line more readable.

before:

Hello سلام...خوبی؟
Hello خولی؟...سلام

* fix: some extra words were being rendered in Ellipsize::Middle

This was causing the last word (if it's not the same level as the rest)
to be rendered outside the buffer.

* test: a few test cases for ellipsize

* fix: assign the correct byte range to ellipsis

this should fix the panic when selecting or clicking on or near the
ellipsis in the editor.
This commit is contained in:
Hojjat Abdollahi 2026-02-19 09:11:22 -07:00 committed by GitHub
parent 4fd11f0e5e
commit 4819bc30fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1568 additions and 313 deletions

View file

@ -1,8 +1,8 @@
use std::path::PathBuf;
use cosmic_text::{
fontdb::Database, Attrs, AttrsOwned, Buffer, Color, Family, FontSystem, Metrics, Shaping,
SwashCache,
fontdb::Database, Align, Attrs, AttrsOwned, Buffer, Color, Ellipsize, Family, FontSystem,
Metrics, Shaping, SwashCache, Wrap,
};
use tiny_skia::{Paint, Pixmap, Rect, Transform};
@ -29,6 +29,9 @@ pub struct DrawTestCfg {
line_height: f32,
canvas_width: u32,
canvas_height: u32,
wrap: Wrap,
ellipsize: Ellipsize,
alignment: Option<Align>,
}
impl Default for DrawTestCfg {
@ -42,6 +45,9 @@ impl Default for DrawTestCfg {
line_height: 20.0,
canvas_width: 300,
canvas_height: 300,
wrap: Wrap::WordOrGlyph,
ellipsize: Ellipsize::None,
alignment: None,
}
}
}
@ -76,6 +82,21 @@ impl DrawTestCfg {
self
}
pub fn wrap(mut self, wrap: Wrap) -> Self {
self.wrap = wrap;
self
}
pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self {
self.ellipsize = ellipsize;
self
}
pub fn alignment(mut self, alignment: Option<Align>) -> Self {
self.alignment = alignment;
self
}
pub fn validate_text_rendering(self) {
let repo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
// Create a db with just the fonts in our fonts dir to make sure we only test those
@ -88,11 +109,18 @@ impl DrawTestCfg {
let mut buffer = Buffer::new(&mut font_system, metrics);
let mut buffer = buffer.borrow_with(&mut font_system);
let margins = 5;
buffer.set_wrap(self.wrap);
buffer.set_ellipsize(self.ellipsize);
buffer.set_size(
Some((self.canvas_width - margins * 2) as f32),
Some((self.canvas_height - margins * 2) as f32),
);
buffer.set_text(&self.text, &self.font.as_attrs(), Shaping::Advanced, None);
buffer.set_text(
&self.text,
&self.font.as_attrs(),
Shaping::Advanced,
self.alignment,
);
buffer.shape_until_scroll(true);
// Black

View file

@ -0,0 +1,161 @@
use common::DrawTestCfg;
use cosmic_text::{Align, Attrs, Ellipsize, EllipsizeHeightLimit, Family, Wrap};
mod common;
#[test]
fn test_ellipsize_ltr_end_single_line() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_end_single_line")
.font_size(20., 26.)
.font_attrs(attrs)
.text("The quick brown fox jumps over the lazy dog.")
.wrap(Wrap::None)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.canvas(180, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_ltr_end_single_line_aligned_right() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_end_single_line_aligned_right")
.font_size(20., 26.)
.font_attrs(attrs)
.text("The quick brown fox jumps over the lazy dog.")
.wrap(Wrap::None)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.alignment(Some(Align::Right))
.canvas(180, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_rtl_end_single_line() {
let attrs = Attrs::new().family(Family::Name("Noto Sans"));
DrawTestCfg::new("ellipsize_rtl_end_single_line")
.font_size(22., 28.)
.font_attrs(attrs)
.text("توانا بود هرکه دانا بود.")
.wrap(Wrap::None)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.canvas(180, 55)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_mixed_end_single_line() {
let attrs = Attrs::new().family(Family::Name("Noto Sans"));
DrawTestCfg::new("ellipsize_mixed_end_single_line")
.font_size(20., 26.)
.font_attrs(attrs)
.text("Hello سلام mixed RTL/LTR world with extra words")
.wrap(Wrap::None)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.canvas(190, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_ltr_start_single_line() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_start_single_line")
.font_size(20., 26.)
.font_attrs(attrs)
.text("The quick brown fox jumps over the lazy dog.")
.wrap(Wrap::None)
.ellipsize(Ellipsize::Start(EllipsizeHeightLimit::Lines(1)))
.canvas(180, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_ltr_middle_single_line() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_middle_single_line")
.font_size(20., 26.)
.font_attrs(attrs)
.text("The quick brown fox jumps over the lazy dog.")
.wrap(Wrap::None)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(1)))
.canvas(180, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_ltr_end_two_lines() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_end_two_lines")
.font_size(18., 24.)
.font_attrs(attrs)
.text("Pack my box with five dozen liquor jugs. Sphinx of black quartz, judge my vow.")
.wrap(Wrap::Word)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(2)))
.canvas(200, 80)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_mixed_middle_single_line() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_mixed_middle_single_line")
.font_size(20., 26.)
.font_attrs(attrs)
.text("Hello سلام mixed RTL/LTR world with extra words")
.wrap(Wrap::None)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(1)))
.canvas(180, 50)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_mixed_ltr_rtl_middle_two_lines() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_mixed_ltr_rtl_middle_two_lines")
.font_size(20., 26.)
.font_attrs(attrs)
.text("First line is LTR خط دوم از راست به چپ")
.wrap(Wrap::WordOrGlyph)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(2)))
.canvas(180, 80)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_mixed_rtl_ltr_middle_two_lines() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_mixed_rtl_ltr_middle_two_lines")
.font_size(20., 26.)
.font_attrs(attrs)
.text("خط اول از راست به چپ Second line is LTR and has more words")
.wrap(Wrap::WordOrGlyph)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(2)))
.canvas(210, 80)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_ltr_single_word_middle_two_lines() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_ltr_single_word_middle_two_lines")
.font_size(20., 26.)
.font_attrs(attrs)
.text("AVeryLongWordThatExceedsTheWidth")
.wrap(Wrap::WordOrGlyph)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(2)))
.canvas(180, 80)
.validate_text_rendering();
}
#[test]
fn test_ellipsize_mixed_ltr_rtl_ltr_middle_three_lines() {
let attrs = Attrs::new().family(Family::Name("Inter"));
DrawTestCfg::new("ellipsize_mixed_ltr_rtl_ltr_middle_three_lines")
.font_size(20., 26.)
.font_attrs(attrs)
.text("This is some LTR text that keeps و یه مشت متن فارسیی.zippy")
.wrap(Wrap::WordOrGlyph)
.ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(3)))
.canvas(200, 100)
.validate_text_rendering();
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0de01fa40e15779c52c39652369f6c636ce0c018c3ef488d99cc7cfcd0d53eb4
size 4957

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb2f9fccbe5dd17854cc9975cba81a8357ca299d1970585dac025d81beaa6a29
size 5099

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab08b3a7061db4e32ece8aec5bc6c4a32939fa91186c31f76eb901adcdbdd0fc
size 12218

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:181b48bf70119b2df56521c287b2cd1b60ab1677ebaa03ea5f01dee522693370
size 5412

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fb2a94c8892736ae1e05ef5d42e541eff7eae8826cae6e19fb34727b1ff6ee6
size 12179

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2a705f931aae72c693a2a1ae56ad2455ee3a8f7f2309ca99864eedbf7c59d40
size 5518

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6438bf5df040bfe61566af722823f2f78d519e4719547c766fbe19e2dc4a07f6
size 5985

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cf481f25cf06518eddfc8794322bb13998f39709076a2d07ece2e2528d7364e
size 16096

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36156e31793901ede07b4db53ddf671b5c134afe82a8328fd7eacad1cebcd339
size 8960

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88bf081f80aac508baa685718fce15e3acf888b43c199aa902494a2c47ed22e3
size 5501

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56fcff35d50673ec790dfacc82b55513f643628d6745a7eec2a7082b4ced04f7
size 12004

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e9d984a59003766fda15d1bb35069d6f5a9638b00c0560f2fad3431c0283aa9
size 5585