Skip to content

Commit

Permalink
Test rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
w0rm committed Apr 28, 2024
1 parent 93e7818 commit 3db4f67
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 63 deletions.
3 changes: 3 additions & 0 deletions src/alignment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ impl HorizontalAlignment {
) -> (i32, SpaceConfig) {
let space_width = str_width(renderer, " ");
let space_config = SpaceConfig::new(space_width, None);
if measurement.max_line_width < measurement.width {
panic!("{} {}", measurement.max_line_width, measurement.width)
}
let remaining_space = measurement.max_line_width - measurement.width;
match self {
HorizontalAlignment::Left => (0, space_config),
Expand Down
76 changes: 15 additions & 61 deletions src/rendering/line_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,10 @@ pub(crate) mod test {
plugin::{NoPlugin, PluginMarker as Plugin, PluginWrapper},
rendering::{cursor::Cursor, space_config::SpaceConfig},
style::TabSize,
utils::{str_width, str_width_and_left_offset, 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 @@ -801,68 +804,17 @@ pub(crate) mod test {
assert_line_elements(&mut parser, 2, &[RenderElement::string("So", 12)], &mw);
}

/// A font where each glyph is 4x10 pixels, where the
/// glyph 'j' has a negative left side bearing of 2 pixels
struct TestTextStyle {}

impl TextRenderer for TestTextStyle {
type Color = Rgb888;

fn draw_string<D>(
&self,
text: &str,
position: Point,
baseline: embedded_graphics::text::Baseline,
_target: &mut D,
) -> Result<Point, D::Error>
where
D: embedded_graphics::prelude::DrawTarget<Color = Self::Color>,
{
return Ok(self.measure_string(text, position, baseline).next_position);
}

fn draw_whitespace<D>(
&self,
width: u32,
position: Point,
_baseline: embedded_graphics::text::Baseline,
_target: &mut D,
) -> Result<Point, D::Error>
where
D: embedded_graphics::prelude::DrawTarget<Color = Self::Color>,
{
return Ok(Point::new(position.x + width as i32, position.y));
}

fn measure_string(
&self,
text: &str,
position: Point,
_baseline: embedded_graphics::text::Baseline,
) -> embedded_graphics::text::renderer::TextMetrics {
let offset = if text.starts_with("j") { -2 } else { 0 };
let width = text.len() as u32 * 4;
let top_left = Point::new(position.x + offset, position.y);
embedded_graphics::text::renderer::TextMetrics {
bounding_box: Rectangle::new(top_left, Size::new(width, 10)),
next_position: Point::new(top_left.x + width as i32, position.y),
}
}

fn line_height(&self) -> u32 {
10
}
}

#[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 = TestTextStyle {};
// the glyph 'j' occupies 2 pixels because of the negative left side bearing
// however, the first 'j' on the line is prepended with an extra 2 pixel whitespace
let size = Size::new(4 * text.len() as u32 - 2, 10);
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),
Expand All @@ -880,15 +832,17 @@ pub(crate) mod test {

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),
RenderElement::string("just", 14),
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", 10),
RenderElement::string("jet", 11), // 1 for j + 5 for each additional glyph
]
);
}
Expand Down
48 changes: 47 additions & 1 deletion src/rendering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,13 @@ pub mod test {
pixelcolor::BinaryColor,
prelude::*,
primitives::Rectangle,
text::renderer::TextRenderer,
};

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 +415,49 @@ 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(&[
"..#.####.####.####.........####........#.####.####",
"....#..#.#..#.#..#.........#..#..........#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
"..#.####.####.####.........####........#.####.####",
"..#....................................#..........",
"..#....................................#..........",
"##...................................##...........",
"####.####.#.####.####....#### ",
"#..#.#..#...#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"#..#.#..#.#.#..#.#..#....#..# ",
"####.####.#.####.####....#### ",
"..........#.................. ",
"..........#.................. ",
"........##................... ",
]);
}
}
176 changes: 175 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ pub fn str_width_and_left_offset(renderer: &impl TextRenderer, s: &str) -> (u32,

#[cfg(test)]
pub mod test {
use az::SaturatingAs;
use embedded_graphics::{
draw_target::DrawTarget,
geometry::Point,
mono_font::{ascii::FONT_6X9, MonoFont, MonoTextStyle},
pixelcolor::BinaryColor,
pixelcolor::{BinaryColor, PixelColor},
prelude::Size,
primitives::{Line, PrimitiveStyle, Rectangle, StyledDrawable},
text::{
renderer::{CharacterStyle, TextMetrics, TextRenderer},
Baseline,
},
Drawable, Pixel,
};

use super::str_width;
Expand All @@ -40,6 +49,171 @@ pub mod test {
font.character_size.x_axis() * chars + font.character_size.y_axis() * lines
}

/// A font where each glyph is 4x10 pixels, except for the
/// glyph 'j' that is 3x10 with a negative left side bearing of 2 pixels
#[derive(Copy, Clone)]
pub struct TestFont<C> {
text_color: C,
background_color: C,
letter_spacing: u32,
}

enum LineElement {
Char(char),
Spacing,
Done,
}

fn left_side_bearing(c: char) -> i32 {
match c {
'j' => -2,
_ => 0,
}
}

fn char_width(c: char) -> u32 {
match c {
'j' => 3,
_ => 4,
}
}

impl<C> TestFont<C> {
pub fn new(text_color: C, background_color: C) -> Self {
Self {
text_color,
background_color,
letter_spacing: 1,
}
}

fn line_elements<'t>(
&self,
mut position: Point,
text: &'t str,
) -> impl Iterator<Item = (Point, LineElement)> + 't
where {
let mut chars = text.chars();
let mut next_char = chars.next();
let mut spacing = next_char.map(left_side_bearing);
let letter_spacing = self.letter_spacing as i32;

core::iter::from_fn(move || {
if let Some(offset) = spacing {
let p = position;
position.x += offset;
spacing = None;
Some((p, LineElement::Spacing))
} else if let Some(c) = next_char {
let p = position;
position.x += char_width(c) as i32;
next_char = chars.next();
spacing = next_char.map(|c| letter_spacing + left_side_bearing(c));
Some((p, LineElement::Char(c)))
} else {
Some((position, LineElement::Done))
}
})
}
}

impl<C> CharacterStyle for TestFont<C>
where
C: PixelColor,
{
type Color = C;
}

impl<C> TextRenderer for TestFont<C>
where
C: PixelColor,
{
type Color = C;

fn draw_string<D>(
&self,
text: &str,
position: Point,
_baseline: Baseline,
target: &mut D,
) -> Result<Point, D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
let style = PrimitiveStyle::with_stroke(self.text_color, 1);
let bg_style = PrimitiveStyle::with_fill(self.background_color);
let letter_spacing = self.letter_spacing;
for (p, element) in self.line_elements(position, text) {
match element {
LineElement::Char('j') => {
// draw the 'j' character background, occyping the space behind the stem
Rectangle::new(p + Point::new(2, 0), Size::new(1, 10))
.draw_styled(&bg_style, target)?;
// draw the 'j' character
Pixel(p + Point::new(2, 0), self.text_color).draw(target)?;
Line::new(p + Point::new(2, 2), p + Point::new(2, 8))
.draw_styled(&style, target)?;
Line::new(p + Point::new(0, 9), p + Point::new(1, 9))
.draw_styled(&style, target)?;
}
LineElement::Char(_) => {
// draw the background for other characters
Rectangle::new(p, Size::new(4, 10)).draw_styled(&bg_style, target)?;
// draw a 4x7 rectangle for other characters
Rectangle::new(p, Size::new(4, 7)).draw_styled(&style, target)?
}
LineElement::Spacing => {
// draw a 1x10 rectangle for letter spacing
Rectangle::new(p, Size::new(letter_spacing, 10))
.draw_styled(&bg_style, target)?
}
LineElement::Done => return Ok(p),
}
}
Ok(position)
}

fn draw_whitespace<D>(
&self,
width: u32,
position: Point,
_baseline: Baseline,
target: &mut D,
) -> Result<Point, D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
let bg_style = PrimitiveStyle::with_fill(self.background_color);
Rectangle::new(position, Size::new(width, 10)).draw_styled(&bg_style, target)?;
return Ok(Point::new(position.x + width as i32, position.y));
}

fn measure_string(&self, text: &str, position: Point, _baseline: Baseline) -> TextMetrics {
// the bounding box position can be to the left of the position,
// when the first character has a negative left side bearing
// e.g. letter 'j'
let mut bb_left = position.x;
let mut bb_right = position.x;
for (p, element) in self.line_elements(position, text) {
bb_left = bb_left.min(p.x);
bb_right = bb_right.max(p.x);
if let LineElement::Done = element {
break;
}
}
let bb_width = bb_right - position.x;
let bb_size = Size::new(bb_width.saturating_as(), self.line_height());
TextMetrics {
bounding_box: Rectangle::new(Point::new(bb_left, position.y), bb_size),
next_position: position + bb_size.x_axis(),
}
}

fn line_height(&self) -> u32 {
10
}
}

#[test]
fn width_of_nbsp_is_single_space() {
let renderer = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
Expand Down

0 comments on commit 3db4f67

Please sign in to comment.