use crate::ir::*;
use crate::theme::Theme;
fn budget_for_theme(theme: &Theme) -> f32 {
let chrome_emu = 1_828_800.0; let content_h = (theme.slide_h as f32 - chrome_emu).max(914_400.0);
let per_unit = (6_858_000.0 - chrome_emu) / 14.0;
content_h / per_unit
}
fn width_scale(theme: &Theme) -> f32 {
let ref_w = 12_192_000.0;
(theme.slide_w as f32 / ref_w).clamp(0.5, 1.2)
}
const CODE_LINE_WEIGHT: f32 = 0.7;
pub fn paginate(slides: Vec<Slide>, theme: &Theme) -> Vec<Slide> {
let budget = budget_for_theme(theme);
let wscale = width_scale(theme);
let mut out = Vec::new();
for mut slide in slides {
slide.blocks = coalesce_columns(std::mem::take(&mut slide.blocks));
if matches!(slide.kind, SlideKind::Section) {
let blocks = std::mem::take(&mut slide.blocks);
let title = slide.title.clone();
let notes = slide.notes.clone();
let bg = slide.bg_image.clone();
out.push(slide);
if !blocks.is_empty() {
let follow = Slide {
kind: SlideKind::Content,
title,
blocks,
notes,
bg_image: bg,
layout_hint: None,
};
for s in paginate(vec![follow], theme) {
out.push(s);
}
}
continue;
}
if !matches!(slide.kind, SlideKind::Content) {
out.push(slide);
continue;
}
let total: f32 = slide.blocks.iter().map(|b| block_weight(b, wscale)).sum();
if total <= budget {
out.push(slide);
continue;
}
let mut current = Slide {
kind: SlideKind::Content,
title: slide.title.clone(),
blocks: Vec::new(),
notes: None,
bg_image: slide.bg_image.clone(),
layout_hint: slide.layout_hint.clone(),
};
let mut weight = 0.0_f32;
let mut emitted_first = false;
for block in slide.blocks {
let w = block_weight(&block, wscale);
if !current.blocks.is_empty() && weight + w > budget {
let orphan_lead = current.blocks.last().map_or(false, is_lead_in);
let carried_lead = if orphan_lead {
current.blocks.pop()
} else {
None
};
if !current.blocks.is_empty() {
out.push(current);
emitted_first = true;
}
let cont_title = if emitted_first && !slide.title.ends_with(" (cont.)") {
format!("{} (cont.)", slide.title)
} else {
slide.title.clone()
};
current = Slide {
kind: SlideKind::Content,
title: cont_title,
blocks: Vec::new(),
notes: None,
bg_image: slide.bg_image.clone(),
layout_hint: slide.layout_hint.clone(),
};
weight = 0.0;
if let Some(lead) = carried_lead {
weight += block_weight(&lead, wscale);
current.blocks.push(lead);
}
}
if matches!(&block, Block::CodeBlock { lines, .. } if (lines.len() as f32) * CODE_LINE_WEIGHT + 1.5 > budget + 2.0)
{
if let Block::CodeBlock {
lang,
title,
lines,
line_numbers,
} = block
{
let chunk_size = ((budget / CODE_LINE_WEIGHT) as usize)
.saturating_sub(2)
.max(6);
for (idx, chunk) in lines.chunks(chunk_size).enumerate() {
if !current.blocks.is_empty() && idx > 0 {
out.push(current);
emitted_first = true;
current = Slide {
kind: SlideKind::Content,
title: if emitted_first && !slide.title.ends_with(" (cont.)") {
format!("{} (cont.)", slide.title)
} else {
slide.title.clone()
},
blocks: Vec::new(),
notes: None,
bg_image: slide.bg_image.clone(),
layout_hint: slide.layout_hint.clone(),
};
weight = 0.0;
}
current.blocks.push(Block::CodeBlock {
lang: lang.clone(),
title: if idx == 0 { title.clone() } else { None },
lines: chunk.to_vec(),
line_numbers,
});
weight += chunk.len() as f32 + 1.5;
}
}
continue;
}
if matches!(&block, Block::Table { rows, .. } if (rows.len() as f32 + 1.0) * 1.3 > budget + 1.0)
{
if let Block::Table { headers, rows } = block {
let max_rows = (((budget - 1.0) / 1.3) as usize).saturating_sub(1).max(4);
for (idx, chunk) in rows.chunks(max_rows).enumerate() {
if !current.blocks.is_empty() && idx > 0 {
out.push(current);
emitted_first = true;
current = Slide {
kind: SlideKind::Content,
title: if emitted_first && !slide.title.ends_with(" (cont.)") {
format!("{} (cont.)", slide.title)
} else {
slide.title.clone()
},
blocks: Vec::new(),
notes: None,
bg_image: slide.bg_image.clone(),
layout_hint: slide.layout_hint.clone(),
};
weight = 0.0;
}
current.blocks.push(Block::Table {
headers: headers.clone(),
rows: chunk.to_vec(),
});
weight += (chunk.len() as f32 + 1.0) * 1.3;
}
}
continue;
}
weight += w;
current.blocks.push(block);
}
if !current.blocks.is_empty() {
out.push(current);
}
}
out
}
fn block_weight(b: &Block, wscale: f32) -> f32 {
match b {
Block::Paragraph(runs) => {
let chars = total_chars(runs) as f32;
(chars / (85.0 * wscale)).max(1.0) + 0.4
}
Block::Heading { .. } => 1.6,
Block::List(items) => {
let total: f32 = items
.iter()
.map(|i| {
let chars = i.runs.iter().map(|r| r.text.chars().count()).sum::<usize>() as f32;
let lines = (chars / (75.0 * wscale)).max(1.0);
lines + 0.1 * i.level as f32
})
.sum();
let all_short = items.iter().all(|i| {
let chars = i.runs.iter().map(|r| r.text.chars().count()).sum::<usize>() as f32;
chars <= 75.0 * wscale
});
if items.len() > crate::theme::LONG_LIST_THRESHOLD && all_short {
(total / 2.0).max(items.len() as f32 / 2.0)
} else {
total
}
}
Block::CodeBlock { lines, title, .. } => {
lines.len() as f32 * CODE_LINE_WEIGHT + 1.5 + if title.is_some() { 0.6 } else { 0.0 }
}
Block::Quote(paras) => {
paras
.iter()
.map(|runs| (total_chars(runs) as f32 / (70.0 * wscale)).max(1.0))
.sum::<f32>()
+ 0.6
}
Block::Table { rows, .. } => (rows.len() as f32 + 1.0) * 1.3,
Block::ColumnBreak => 0.0,
Block::Columns { left, right } => {
let col_scale = wscale * 0.5;
let lw: f32 = left.iter().map(|b| block_weight(b, col_scale)).sum();
let rw: f32 = right.iter().map(|b| block_weight(b, col_scale)).sum();
lw.max(rw)
}
Block::Image { .. } => 13.0,
Block::Footnotes(items) => {
let total: f32 = items
.iter()
.map(|i| {
let chars = i.runs.iter().map(|r| r.text.chars().count()).sum::<usize>() as f32;
(chars / (110.0 * wscale)).max(0.7)
})
.sum();
total + 0.4
}
}
}
fn is_lead_in(b: &Block) -> bool {
match b {
Block::Paragraph(runs) => {
let text: String = runs.iter().map(|r| r.text.clone()).collect();
let trimmed = text.trim();
if trimmed.is_empty() {
return false;
}
let chars = trimmed.chars().count();
chars < 120 && trimmed.ends_with(':')
}
Block::Heading { .. } => true,
_ => false,
}
}
fn coalesce_columns(blocks: Vec<Block>) -> Vec<Block> {
if !blocks.iter().any(|b| matches!(b, Block::ColumnBreak)) {
return blocks;
}
let mut left = Vec::new();
let mut right = Vec::new();
let mut break_seen = false;
for b in blocks {
match b {
Block::ColumnBreak => {
break_seen = true;
}
other if break_seen => right.push(other),
other => left.push(other),
}
}
vec![Block::Columns { left, right }]
}
fn total_chars(runs: &[Run]) -> usize {
runs.iter().map(|r| r.text.chars().count()).sum()
}