Skip to content

Commit

Permalink
Merge pull request #167 from w0rm/support-glyphs-with-negative-left-s…
Browse files Browse the repository at this point in the history
…ide-bearing

Offset the line to fit the glyph with negative side bearing
  • Loading branch information
bugadani committed Apr 29, 2024
2 parents f49d524 + e6b14d6 commit 805e78a
Show file tree
Hide file tree
Showing 6 changed files with 451 additions and 11 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
0.7.1 (2024-04-28)
==================

- [#167] Offset the line to fit the glyph with negative side bearing

[#167]: https://github.com/embedded-graphics/embedded-text/pull/167

0.7.0 (2023-11-03)
==================

Expand Down
6 changes: 5 additions & 1 deletion src/rendering/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
line_iter::{ElementHandler, LineElementParser, LineEndType},
},
style::TextBoxStyle,
utils::str_width,
utils::{str_width, str_width_and_left_offset},
};
use embedded_graphics::{
draw_target::DrawTarget,
Expand Down Expand Up @@ -105,6 +105,10 @@ where
str_width(self.text_renderer, st)
}

fn measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
str_width_and_left_offset(self.text_renderer, st)
}

fn whitespace(&mut self, st: &str, _space_count: u32, width: u32) -> Result<(), Self::Error> {
if width > 0 {
self.text_renderer
Expand Down
77 changes: 70 additions & 7 deletions src/rendering/line_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub trait ElementHandler {
/// Returns the width of the given string in pixels.
fn measure(&self, st: &str) -> u32;

/// Returns the left offset in pixels.
fn measure_width_and_left_offset(&self, _st: &str) -> (u32, u32);

/// A whitespace block with the given width.
fn whitespace(&mut self, _st: &str, _space_count: u32, _width: u32) -> Result<(), Self::Error> {
Ok(())
Expand Down Expand Up @@ -136,13 +139,12 @@ where
handler: &E,
w: &'a str,
) -> (&'a str, &'a str) {
let mut width = 0;
for (idx, c) in w.char_indices() {
let char_width = handler.measure(unsafe {
let width = handler.measure(unsafe {
// SAFETY: we are working on character boundaries
w.get_unchecked(idx..idx + c.len_utf8())
w.get_unchecked(0..idx + c.len_utf8())
});
if !self.cursor.fits_in_line(width + char_width) {
if !self.cursor.fits_in_line(width) {
unsafe {
if w.is_char_boundary(idx) {
return w.split_at(idx);
Expand All @@ -151,7 +153,6 @@ where
}
}
}
width += char_width;
}

(w, "")
Expand Down Expand Up @@ -321,7 +322,18 @@ where
}

Token::Word(w) => {
let width = handler.measure(w);
let width = if self.empty {
// If this is the first word on the line, offset the line by
// the word's left negative boundary to make sure it is not clipped.
let (width, offset) = handler.measure_width_and_left_offset(w);
if offset > 0 && self.move_cursor_forward(offset).is_ok() {
handler.whitespace("", 0, offset).ok();
};
width
} else {
handler.measure(w)
};

let (word, remainder) = if self.move_cursor_forward(width).is_ok() {
// We can move the cursor here since `process_word()`
// doesn't depend on it.
Expand Down Expand Up @@ -452,14 +464,18 @@ where

#[cfg(test)]
pub(crate) mod test {
use core::fmt::Debug;
use std::convert::Infallible;

use super::*;
use crate::{
plugin::{NoPlugin, PluginMarker as Plugin, PluginWrapper},
rendering::{cursor::Cursor, space_config::SpaceConfig},
style::TabSize,
utils::{str_width, test::size_for},
utils::{
str_width, str_width_and_left_offset,
test::{size_for, TestFont},
},
};
use embedded_graphics::{
geometry::{Point, Size},
Expand Down Expand Up @@ -511,6 +527,10 @@ pub(crate) mod test {
str_width(&self.style, st)
}

fn measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
str_width_and_left_offset(&self.style, st)
}

fn whitespace(&mut self, _string: &str, count: u32, width: u32) -> Result<(), Self::Error> {
self.elements
.push(RenderElement::Space(count, (width > 0) as bool));
Expand Down Expand Up @@ -781,4 +801,47 @@ pub(crate) mod test {

assert_line_elements(&mut parser, 2, &[RenderElement::string("So", 12)], &mw);
}

#[test]
fn negative_left_side_bearing_of_the_first_glyph_sets_left_offset() {
let text = "just a jet";
let mut parser = Parser::parse(text);
let plugin = PluginWrapper::new(NoPlugin::<Rgb888>::new());
let style = TestFont::new(BinaryColor::On.into(), BinaryColor::Off.into());

let size = style
.measure_string(text, Point::zero(), embedded_graphics::text::Baseline::Top)
.bounding_box
.size;
let config = SpaceConfig::new(str_width(&style, " "), None);
let cursor = Cursor::new(
Rectangle::new(Point::zero(), size),
style.line_height(),
LineHeight::Percent(100),
TabSize::Spaces(4).into_pixels(&style),
)
.line();

let text_box_style = TextBoxStyle::default();

let mut handler = TestElementHandler::new(style);
let mut line1 =
LineElementParser::new(&mut parser, &plugin, cursor, config, &text_box_style);

line1.process(&mut handler).unwrap();

// 'j' occupies 1 pixel because of the negative left side bearing -2 (its width is 3 pixels)
// each additional glyph in a word occupies 5 pixels (4 for the glyph and 1 for letter spacing)
assert_eq!(
handler.elements,
&[
RenderElement::Space(0, true), // 2 pixels, to compensate for the negative left side bearing
RenderElement::string("just", 16), // 1 for j + 5 for each additional glyph
RenderElement::Space(1, true),
RenderElement::string("a", 4),
RenderElement::Space(1, true),
RenderElement::string("jet", 11), // 1 for j + 5 for each additional glyph
]
);
}
}
140 changes: 139 additions & 1 deletion src/rendering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ pub mod test {
use crate::{
alignment::HorizontalAlignment,
style::{HeightMode, TextBoxStyle, TextBoxStyleBuilder, VerticalOverdraw},
utils::test::size_for,
utils::test::{size_for, TestFont},
TextBox,
};

Expand Down Expand Up @@ -414,4 +414,142 @@ pub mod test {
"............ ",
]);
}

#[test]
fn rendering_justified_text_with_negative_left_side_bearing() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
display.set_allow_overdraw(true);

let text = "j000 0 j00 00j00 0";
let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
let size = Size::new(50, 0);

TextBox::with_textbox_style(
text,
Rectangle::new(Point::zero(), size),
character_style,
TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Justified)
.height_mode(HeightMode::FitToText)
.build(),
)
.draw(&mut display)
.unwrap();

display.assert_pattern(&[
"..#.####.####.####.........####........#.####.####",
"....#..#.#..#.#..#.........#..#..........#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.####.####.####.........####........#.####.####",
"..#....................................#..........",
"..#....................................#..........",
"##...................................##...........",
"####.####.#.####.####....#### ",
"#..#.#..#...#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"####.####.#.####.####....#### ",
"..........#.................. ",
"..........#.................. ",
"........##................... ",
]);
}

#[test]
fn correctly_breaks_long_words_for_monospace_fonts() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
display.set_allow_overdraw(true);

let text = "000000000000000000";
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();

TextBox::with_textbox_style(
text,
Rectangle::new(Point::zero(), size_for(&FONT_6X10, 10, 2)),
character_style,
TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Left)
.height_mode(HeightMode::FitToText)
.build(),
)
.draw(&mut display)
.unwrap();

display.assert_pattern(&[
"............................................................",
"..#.....#.....#.....#.....#.....#.....#.....#.....#.....#...",
".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#..",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#..",
"..#.....#.....#.....#.....#.....#.....#.....#.....#.....#...",
"............................................................",
"............................................................",
"................................................ ",
"..#.....#.....#.....#.....#.....#.....#.....#... ",
".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#.. ",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
"#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#.. ",
"..#.....#.....#.....#.....#.....#.....#.....#... ",
"................................................ ",
"................................................ ",
]);
}

#[test]
fn correctly_breaks_long_words_for_fonts_with_letter_spacing() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
display.set_allow_overdraw(true);

let text = "000000000000000000";
let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
let size = Size::new(49, 0);

TextBox::with_textbox_style(
text,
Rectangle::new(Point::zero(), size),
character_style,
TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Left)
.height_mode(HeightMode::FitToText)
.build(),
)
.draw(&mut display)
.unwrap();

display.assert_pattern(&[
"####.####.####.####.####.####.####.####.####.####",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
"####.####.####.####.####.####.####.####.####.####",
".................................................",
".................................................",
".................................................",
"####.####.####.####.####.####.####.#### ",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
"#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
"####.####.####.####.####.####.####.#### ",
"....................................... ",
"....................................... ",
"....................................... ",
]);
}
}
6 changes: 5 additions & 1 deletion src/style/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ use crate::{
line_iter::{ElementHandler, LineElementParser, LineEndType},
space_config::SpaceConfig,
},
utils::str_width,
utils::{str_width, str_width_and_left_offset},
};
use embedded_graphics::text::{renderer::TextRenderer, LineHeight};

Expand Down Expand Up @@ -385,6 +385,10 @@ impl<'a, S: TextRenderer> ElementHandler for MeasureLineElementHandler<'a, S> {
str_width(self.style, st)
}

fn measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
str_width_and_left_offset(self.style, st)
}

fn whitespace(&mut self, _st: &str, count: u32, width: u32) -> Result<(), Self::Error> {
self.cursor += width;
self.pos = self.pos.max(self.cursor);
Expand Down
Loading

0 comments on commit 805e78a

Please sign in to comment.