* 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.
172 lines
5.5 KiB
Rust
172 lines
5.5 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use cosmic_text::{
|
|
fontdb::Database, Align, Attrs, AttrsOwned, Buffer, Color, Ellipsize, Family, FontSystem,
|
|
Metrics, Shaping, SwashCache, Wrap,
|
|
};
|
|
use tiny_skia::{Paint, Pixmap, Rect, Transform};
|
|
|
|
/// The test configuration.
|
|
/// The text in the test will be rendered as image using the one of the fonts found under the
|
|
/// `fonts` directory in this repository.
|
|
/// The image will then be compared to an image with the name `name` under the `tests/images`
|
|
/// directory in this repository.
|
|
/// If the images do not match the test will fail.
|
|
/// NOTE: if an environment variable `GENERATE_IMAGES` is set, the test will create and save
|
|
/// the images instead.
|
|
#[derive(Debug)]
|
|
pub struct DrawTestCfg {
|
|
/// The name of the test.
|
|
/// Will be used for the image name under the `tests/images` directory in this repository.
|
|
name: String,
|
|
/// The text to render to image
|
|
text: String,
|
|
/// The name, details of the font to be used.
|
|
/// Expected to be one of the fonts found under the `fonts` directory in this repository.
|
|
font: AttrsOwned,
|
|
|
|
font_size: f32,
|
|
line_height: f32,
|
|
canvas_width: u32,
|
|
canvas_height: u32,
|
|
wrap: Wrap,
|
|
ellipsize: Ellipsize,
|
|
alignment: Option<Align>,
|
|
}
|
|
|
|
impl Default for DrawTestCfg {
|
|
fn default() -> Self {
|
|
let font = Attrs::new().family(Family::Serif);
|
|
Self {
|
|
name: "default".into(),
|
|
font: AttrsOwned::new(&font),
|
|
text: "".into(),
|
|
font_size: 16.0,
|
|
line_height: 20.0,
|
|
canvas_width: 300,
|
|
canvas_height: 300,
|
|
wrap: Wrap::WordOrGlyph,
|
|
ellipsize: Ellipsize::None,
|
|
alignment: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DrawTestCfg {
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn text(mut self, text: impl Into<String>) -> Self {
|
|
self.text = text.into();
|
|
self
|
|
}
|
|
|
|
pub fn font_attrs(mut self, attrs: Attrs) -> Self {
|
|
self.font = AttrsOwned::new(&attrs);
|
|
self
|
|
}
|
|
|
|
pub fn font_size(mut self, font_size: f32, line_height: f32) -> Self {
|
|
self.font_size = font_size;
|
|
self.line_height = line_height;
|
|
self
|
|
}
|
|
|
|
pub fn canvas(mut self, width: u32, height: u32) -> Self {
|
|
self.canvas_width = width;
|
|
self.canvas_height = height;
|
|
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
|
|
let fonts_path = PathBuf::from(&repo_dir).join("fonts");
|
|
let mut font_db = Database::new();
|
|
font_db.load_fonts_dir(fonts_path);
|
|
let mut font_system = FontSystem::new_with_locale_and_db("En-US".into(), font_db);
|
|
let mut swash_cache = SwashCache::new();
|
|
let metrics = Metrics::new(self.font_size, self.line_height);
|
|
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,
|
|
self.alignment,
|
|
);
|
|
buffer.shape_until_scroll(true);
|
|
|
|
// Black
|
|
let text_color = Color::rgb(0x00, 0x00, 0x00);
|
|
|
|
let mut pixmap = Pixmap::new(self.canvas_width, self.canvas_height).unwrap();
|
|
pixmap.fill(tiny_skia::Color::WHITE);
|
|
|
|
buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
|
let mut paint = Paint {
|
|
anti_alias: true,
|
|
..Paint::default()
|
|
};
|
|
paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a());
|
|
let rect = Rect::from_xywh(
|
|
(x + margins as i32) as f32,
|
|
(y + margins as i32) as f32,
|
|
w as f32,
|
|
h as f32,
|
|
)
|
|
.unwrap();
|
|
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
|
|
});
|
|
|
|
let image_name = format!("{}.png", self.name);
|
|
let reference_image_path = PathBuf::from(&repo_dir)
|
|
.join("tests")
|
|
.join("images")
|
|
.join(image_name);
|
|
|
|
let generate_images = std::env::var("GENERATE_IMAGES")
|
|
.map(|v| {
|
|
let val = v.trim().to_ascii_lowercase();
|
|
["t", "true", "1"].iter().any(|&v| v == val)
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if generate_images {
|
|
pixmap.save_png(reference_image_path).unwrap();
|
|
} else {
|
|
let reference_image_data = std::fs::read(reference_image_path).unwrap();
|
|
let image_data = pixmap.encode_png().unwrap();
|
|
assert_eq!(
|
|
reference_image_data, image_data,
|
|
"rendering failed of {self:?}"
|
|
)
|
|
}
|
|
}
|
|
}
|