#![allow(dead_code)]
use std::fmt;
use text_typeset::layout::block::{BlockLayoutParams, FragmentParams};
use text_typeset::layout::frame::{FrameBorderStyle, FrameLayoutParams, FramePosition};
use text_typeset::layout::paragraph::Alignment;
use text_typeset::layout::table::{CellLayoutParams, TableLayoutParams};
use text_typeset::{DecorationKind, RenderFrame, Typesetter, UnderlineStyle, VerticalAlignment};
pub const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
#[derive(Clone, Copy, PartialEq)]
pub struct Rect(pub [f32; 4]);
impl Rect {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self([x, y, w, h])
}
pub fn x(&self) -> f32 {
self.0[0]
}
pub fn y(&self) -> f32 {
self.0[1]
}
pub fn w(&self) -> f32 {
self.0[2]
}
pub fn h(&self) -> f32 {
self.0[3]
}
pub fn right(&self) -> f32 {
self.0[0] + self.0[2]
}
pub fn bottom(&self) -> f32 {
self.0[1] + self.0[3]
}
pub fn overlaps(&self, other: &Rect) -> bool {
self.x() < other.right()
&& other.x() < self.right()
&& self.y() < other.bottom()
&& other.y() < self.bottom()
}
pub fn contains_point(&self, px: f32, py: f32) -> bool {
px >= self.x() && px <= self.right() && py >= self.y() && py <= self.bottom()
}
pub fn contains(&self, other: &Rect) -> bool {
other.x() >= self.x()
&& other.right() <= self.right()
&& other.y() >= self.y()
&& other.bottom() <= self.bottom()
}
}
impl From<[f32; 4]> for Rect {
fn from(a: [f32; 4]) -> Self {
Self(a)
}
}
impl From<Rect> for [f32; 4] {
fn from(r: Rect) -> Self {
r.0
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[x={}, y={}, w={}, h={}]",
self.0[0], self.0[1], self.0[2], self.0[3]
)
}
}
impl fmt::Debug for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
pub trait RenderFrameExt {
fn glyph_rects(&self) -> Vec<Rect>;
fn image_rects(&self) -> Vec<Rect>;
fn decorations_of(&self, kind: DecorationKind) -> Vec<Rect>;
fn cursor_rect(&self) -> Option<Rect>;
fn selection_rects(&self) -> Vec<Rect>;
fn glyph_count(&self) -> usize;
fn decoration_count(&self, kind: DecorationKind) -> usize;
}
impl RenderFrameExt for RenderFrame {
fn glyph_rects(&self) -> Vec<Rect> {
self.glyphs.iter().map(|q| Rect::from(q.screen)).collect()
}
fn image_rects(&self) -> Vec<Rect> {
self.images.iter().map(|q| Rect::from(q.screen)).collect()
}
fn decorations_of(&self, kind: DecorationKind) -> Vec<Rect> {
self.decorations
.iter()
.filter(|d| d.kind == kind)
.map(|d| Rect::from(d.rect))
.collect()
}
fn cursor_rect(&self) -> Option<Rect> {
self.decorations_of(DecorationKind::Cursor)
.into_iter()
.next()
}
fn selection_rects(&self) -> Vec<Rect> {
self.decorations_of(DecorationKind::Selection)
}
fn glyph_count(&self) -> usize {
self.glyphs.len()
}
fn decoration_count(&self, kind: DecorationKind) -> usize {
self.decorations.iter().filter(|d| d.kind == kind).count()
}
}
pub fn assert_no_glyph_overlap(frame: &RenderFrame) {
let rects: Vec<Rect> = frame.glyph_rects();
for i in 0..rects.len() {
for j in (i + 1)..rects.len() {
if !rects[i].overlaps(&rects[j]) {
continue;
}
let ox =
(rects[i].right().min(rects[j].right()) - rects[i].x().max(rects[j].x())).max(0.0);
let oy = (rects[i].bottom().min(rects[j].bottom()) - rects[i].y().max(rects[j].y()))
.max(0.0);
let overlap_area = ox * oy;
let area_i = rects[i].w() * rects[i].h();
let area_j = rects[j].w() * rects[j].h();
let smaller = area_i.min(area_j);
if smaller > 0.0 {
let ratio = overlap_area / smaller;
assert!(
ratio < 0.5,
"glyph[{}] {} significantly overlaps glyph[{}] {} (overlap ratio {:.2})",
i,
rects[i],
j,
rects[j],
ratio
);
}
}
}
}
pub fn assert_glyph_count_preserved(before: &RenderFrame, after: &RenderFrame) {
assert!(
after.glyphs.len() >= before.glyphs.len(),
"glyph count decreased: {} -> {}",
before.glyphs.len(),
after.glyphs.len()
);
}
pub fn assert_decoration_count_preserved(
before: &RenderFrame,
after: &RenderFrame,
kind: DecorationKind,
) {
let before_count = before.decoration_count(kind);
let after_count = after.decoration_count(kind);
assert!(
after_count >= before_count,
"{:?} decoration count decreased: {} -> {}",
kind,
before_count,
after_count
);
}
pub fn assert_blocks_non_overlapping(blocks: &[(f32, f32)]) {
let mut sorted: Vec<(f32, f32)> = blocks.to_vec();
sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
for i in 0..sorted.len().saturating_sub(1) {
let bottom = sorted[i].0 + sorted[i].1;
let next_y = sorted[i + 1].0;
assert!(
bottom <= next_y + 0.01,
"block[{}] y={} h={} bottom={} overlaps block[{}] y={}",
i,
sorted[i].0,
sorted[i].1,
bottom,
i + 1,
next_y
);
}
}
pub fn assert_caret_is_real(rect: [f32; 4], label: &str) {
assert!(
rect[3] > 0.0,
"caret height is zero for {}: {:?}",
label,
rect
);
assert!(
rect[1] >= 0.0,
"caret y is negative for {} (sentinel?): {:?}",
label,
rect
);
}
pub fn make_typesetter() -> Typesetter {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
ts
}
pub fn make_block(id: usize, text: &str) -> BlockLayoutParams {
make_block_at(id, 0, text)
}
pub fn make_block_at(id: usize, position: usize, text: &str) -> BlockLayoutParams {
BlockLayoutParams {
block_id: id,
position,
text: text.to_string(),
fragments: vec![FragmentParams {
text: text.to_string(),
offset: 0,
length: text.len(),
font_family: None,
font_weight: None,
font_bold: None,
font_italic: None,
font_point_size: None,
underline_style: UnderlineStyle::None,
overline: false,
strikeout: false,
is_link: false,
letter_spacing: 0.0,
word_spacing: 0.0,
foreground_color: None,
underline_color: None,
background_color: None,
anchor_href: None,
tooltip: None,
vertical_alignment: VerticalAlignment::Normal,
image_name: None,
image_width: 0.0,
image_height: 0.0,
}],
alignment: Alignment::Left,
top_margin: 0.0,
bottom_margin: 0.0,
left_margin: 0.0,
right_margin: 0.0,
text_indent: 0.0,
list_marker: String::new(),
list_indent: 0.0,
tab_positions: vec![],
line_height_multiplier: None,
non_breakable_lines: false,
checkbox: None,
background_color: None,
}
}
pub fn make_cell(row: usize, col: usize, text: &str) -> CellLayoutParams {
CellLayoutParams {
row,
column: col,
blocks: vec![make_block(row * 100 + col, text)],
background_color: None,
}
}
pub fn make_cell_at(
row: usize,
col: usize,
block_id: usize,
position: usize,
text: &str,
) -> CellLayoutParams {
CellLayoutParams {
row,
column: col,
blocks: vec![make_block_at(block_id, position, text)],
background_color: None,
}
}
pub fn make_table(
id: usize,
rows: usize,
cols: usize,
cells: Vec<CellLayoutParams>,
) -> TableLayoutParams {
TableLayoutParams {
table_id: id,
rows,
columns: cols,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells,
}
}
pub fn make_frame(id: usize, blocks: Vec<BlockLayoutParams>) -> FrameLayoutParams {
FrameLayoutParams {
frame_id: id,
position: FramePosition::Inline,
width: None,
height: None,
margin_top: 0.0,
margin_bottom: 0.0,
margin_left: 0.0,
margin_right: 0.0,
padding: 4.0,
border_width: 1.0,
border_style: FrameBorderStyle::Full,
blocks,
tables: vec![],
frames: vec![],
}
}
#[allow(dead_code)]
pub fn dump_frame(frame: &RenderFrame) {
eprintln!("=== RenderFrame dump ===");
eprintln!("glyphs ({}):", frame.glyphs.len());
for (i, q) in frame.glyphs.iter().enumerate() {
eprintln!(" [{}] {}", i, Rect::from(q.screen));
}
eprintln!("images ({}):", frame.images.len());
for (i, q) in frame.images.iter().enumerate() {
eprintln!(" [{}] {} name={:?}", i, Rect::from(q.screen), q.name);
}
eprintln!("decorations ({}):", frame.decorations.len());
for (i, d) in frame.decorations.iter().enumerate() {
eprintln!(" [{}] {:?}\t{}", i, d.kind, Rect::from(d.rect));
}
eprintln!(
"atlas: {}x{}, dirty={}",
frame.atlas_width, frame.atlas_height, frame.atlas_dirty
);
}