use crate::color::Color;
use crate::content::TextAlign;
use crate::font::BuiltinFont;
use crate::page::PageBuilder;
#[derive(Debug, Clone)]
pub struct FlowStyle {
pub margin_top: f64,
pub margin_bottom: f64,
pub margin_left: f64,
pub margin_right: f64,
pub body_font: BuiltinFont,
pub body_font_size: f64,
pub body_line_height: f64,
pub body_color: Color,
pub h1_font: BuiltinFont,
pub h1_size: f64,
pub h1_color: Color,
pub h1_space_before: f64,
pub h1_space_after: f64,
pub h2_font: BuiltinFont,
pub h2_size: f64,
pub h2_color: Color,
pub h2_space_before: f64,
pub h2_space_after: f64,
pub h3_font: BuiltinFont,
pub h3_size: f64,
pub h3_color: Color,
pub h3_space_before: f64,
pub h3_space_after: f64,
pub paragraph_spacing: f64,
pub code_font: BuiltinFont,
pub code_size: f64,
pub code_bg: Color,
}
impl Default for FlowStyle {
fn default() -> Self {
Self {
margin_top: 60.0,
margin_bottom: 50.0,
margin_left: 60.0,
margin_right: 60.0,
body_font: BuiltinFont::TimesRoman,
body_font_size: 11.0,
body_line_height: 16.0,
body_color: Color::BLACK,
h1_font: BuiltinFont::HelveticaBold,
h1_size: 22.0,
h1_color: Color::rgb_u8(20, 50, 120),
h1_space_before: 20.0,
h1_space_after: 10.0,
h2_font: BuiltinFont::HelveticaBold,
h2_size: 16.0,
h2_color: Color::rgb_u8(40, 70, 140),
h2_space_before: 14.0,
h2_space_after: 6.0,
h3_font: BuiltinFont::HelveticaBold,
h3_size: 13.0,
h3_color: Color::rgb_u8(60, 90, 160),
h3_space_before: 10.0,
h3_space_after: 4.0,
paragraph_spacing: 8.0,
code_font: BuiltinFont::Courier,
code_size: 9.5,
code_bg: Color::rgb_u8(245, 246, 250),
}
}
}
#[derive(Debug, Clone)]
pub struct Span {
pub text: String,
pub font: BuiltinFont,
pub size: f64,
pub color: Color,
}
impl Span {
pub fn text(t: impl Into<String>) -> Self {
Self {
text: t.into(),
font: BuiltinFont::TimesRoman,
size: 11.0,
color: Color::BLACK,
}
}
pub fn bold(mut self) -> Self {
self.font = BuiltinFont::TimesBold;
self
}
pub fn italic(mut self) -> Self {
self.font = BuiltinFont::TimesItalic;
self
}
pub fn size(mut self, s: f64) -> Self {
self.size = s;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = c;
self
}
}
#[derive(Debug, Clone)]
enum Block {
Heading { text: String, level: u8 },
Paragraph(Vec<Span>),
CodeBlock(String),
Rule { color: Color, thickness: f64 },
Space(f64),
BulletList(Vec<String>),
}
pub struct TextFlow {
style: FlowStyle,
blocks: Vec<Block>,
}
impl TextFlow {
pub fn new(style: FlowStyle) -> Self {
Self { style, blocks: Vec::new() }
}
pub fn heading(mut self, text: impl Into<String>, level: u8) -> Self {
self.blocks.push(Block::Heading { text: text.into(), level: level.clamp(1, 3) });
self
}
pub fn paragraph(mut self, text: impl Into<String>) -> Self {
self.blocks.push(Block::Paragraph(vec![Span::text(text)]));
self
}
pub fn rich_paragraph(mut self, spans: Vec<Span>) -> Self {
self.blocks.push(Block::Paragraph(spans));
self
}
pub fn code(mut self, text: impl Into<String>) -> Self {
self.blocks.push(Block::CodeBlock(text.into()));
self
}
pub fn rule(mut self) -> Self {
self.blocks.push(Block::Rule {
color: Color::rgb_u8(200, 210, 220),
thickness: 0.5,
});
self
}
pub fn space(mut self, pts: f64) -> Self {
self.blocks.push(Block::Space(pts));
self
}
pub fn bullets(mut self, items: Vec<impl Into<String>>) -> Self {
self.blocks.push(Block::BulletList(items.into_iter().map(Into::into).collect()));
self
}
pub fn render(&self, page_width: f64, page_height: f64) -> Vec<PageBuilder> {
let mut pages: Vec<PageBuilder> = Vec::new();
let mut current = PageBuilder::new(page_width, page_height);
let _body_f = current.use_times();
let _head_f = current.use_helvetica();
let _code_f = current.use_courier();
let text_width = page_width - self.style.margin_left - self.style.margin_right;
let mut y = page_height - self.style.margin_top;
let min_y = self.style.margin_bottom;
macro_rules! new_page {
() => {{
pages.push(current);
current = PageBuilder::new(page_width, page_height);
let _bf = current.use_times();
let _hf = current.use_helvetica();
let _cf = current.use_courier();
y = page_height - self.style.margin_top;
}};
}
macro_rules! check_space {
($needed:expr) => {
if y - $needed < min_y {
new_page!();
}
};
}
for block in &self.blocks {
match block {
Block::Heading { text, level } => {
let (font, size, color, space_before, space_after) = match level {
1 => (BuiltinFont::HelveticaBold, self.style.h1_size, self.style.h1_color,
self.style.h1_space_before, self.style.h1_space_after),
2 => (BuiltinFont::HelveticaBold, self.style.h2_size, self.style.h2_color,
self.style.h2_space_before, self.style.h2_space_after),
_ => (BuiltinFont::HelveticaBold, self.style.h3_size, self.style.h3_color,
self.style.h3_space_before, self.style.h3_space_after),
};
let key = self.heading_key(*level);
check_space!(space_before + size + space_after);
y -= space_before;
current.content_stream()
.draw_text(text, self.style.margin_left, y, &key, font, size, color, TextAlign::Left);
y -= size;
if *level == 1 {
let line_w = font.string_width(text, size);
current.content_stream()
.line(self.style.margin_left, y,
self.style.margin_left + line_w, y,
color, 0.8);
y -= 3.0;
}
y -= space_after;
}
Block::Paragraph(spans) => {
let merged: String = spans.iter().map(|s| s.text.as_str()).collect::<Vec<_>>().join("");
let font = self.style.body_font;
let key = self.body_key();
let size = self.style.body_font_size;
let lh = self.style.body_line_height;
let words: Vec<&str> = merged.split_whitespace().collect();
if words.is_empty() {
y -= self.style.paragraph_spacing;
continue;
}
let space_w = font.char_width(' ') * size / 1000.0;
let mut line_words: Vec<&str> = Vec::new();
let mut line_w = 0.0;
let mut lines: Vec<String> = Vec::new();
for word in &words {
let ww = font.string_width(word, size);
if line_words.is_empty() {
line_words.push(word);
line_w = ww;
} else if line_w + space_w + ww <= text_width {
line_words.push(word);
line_w += space_w + ww;
} else {
lines.push(line_words.join(" "));
line_words = vec![word];
line_w = ww;
}
}
if !line_words.is_empty() {
lines.push(line_words.join(" "));
}
for line in &lines {
check_space!(lh);
current.content_stream()
.draw_text(line, self.style.margin_left, y, &key, font, size,
self.style.body_color, TextAlign::Left);
y -= lh;
}
y -= self.style.paragraph_spacing;
}
Block::CodeBlock(code) => {
let font = self.style.code_font;
let key = self.code_key();
let size = self.style.code_size;
let lh = size + 3.0;
let pad = 8.0;
let code_lines: Vec<&str> = code.lines().collect();
let block_h = (code_lines.len() as f64) * lh + pad * 2.0;
check_space!(block_h);
current.content_stream()
.filled_rect(self.style.margin_left - pad, y - block_h + pad,
text_width + pad * 2.0, block_h,
self.style.code_bg);
current.content_stream()
.stroked_rect(self.style.margin_left - pad, y - block_h + pad,
text_width + pad * 2.0, block_h,
Color::rgb_u8(200, 210, 220), 0.5);
y -= pad;
for line in &code_lines {
check_space!(lh);
current.content_stream()
.draw_text(line, self.style.margin_left, y, &key, font, size,
Color::rgb_u8(40, 40, 120), TextAlign::Left);
y -= lh;
}
y -= pad + self.style.paragraph_spacing;
}
Block::Rule { color, thickness } => {
check_space!(10.0);
y -= 4.0;
current.content_stream()
.line(self.style.margin_left, y,
page_width - self.style.margin_right, y,
*color, *thickness);
y -= 6.0;
}
Block::Space(pts) => {
y -= pts;
if y < min_y { new_page!(); }
}
Block::BulletList(items) => {
let font = self.style.body_font;
let key = self.body_key();
let size = self.style.body_font_size;
let lh = self.style.body_line_height;
let indent = self.style.margin_left + 18.0;
let bullet_x = self.style.margin_left + 6.0;
let item_width = text_width - 18.0;
for item in items {
let words: Vec<&str> = item.split_whitespace().collect();
if words.is_empty() { continue; }
let space_w = font.char_width(' ') * size / 1000.0;
let mut lines: Vec<String> = Vec::new();
let mut line_words: Vec<&str> = Vec::new();
let mut line_w = 0.0;
for word in &words {
let ww = font.string_width(word, size);
if line_words.is_empty() {
line_words.push(word);
line_w = ww;
} else if line_w + space_w + ww <= item_width {
line_words.push(word);
line_w += space_w + ww;
} else {
lines.push(line_words.join(" "));
line_words = vec![word];
line_w = ww;
}
}
if !line_words.is_empty() {
lines.push(line_words.join(" "));
}
check_space!(lh * lines.len() as f64);
current.content_stream()
.draw_text("•", bullet_x, y, &key, font, size,
self.style.body_color, TextAlign::Left);
for (li, line) in lines.iter().enumerate() {
if li > 0 { check_space!(lh); }
current.content_stream()
.draw_text(line, indent, y, &key, font, size,
self.style.body_color, TextAlign::Left);
y -= lh;
}
}
y -= self.style.paragraph_spacing;
}
}
}
pages.push(current);
pages
}
fn body_key(&self) -> String { "F1Reg".into() } fn heading_key(&self, _level: u8) -> String { "F2Reg".into() } fn code_key(&self) -> String { "F3Reg".into() } }