use uzor::types::Rect;
use uzor_text::LineShaper;
use crate::compose::{compose, ComposeStyle};
use crate::region::{FixedRegionSequence, Frame, PlacedBlock, Region};
use crate::scene::{BlockId, BlockNode};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockOverride {
pub rect: Rect,
}
pub struct BuildStep<'a> {
pub visible: &'a [BlockId],
pub overrides: &'a [(BlockId, BlockOverride)],
}
impl<'a> BuildStep<'a> {
pub fn new(visible: &'a [BlockId], overrides: &'a [(BlockId, BlockOverride)]) -> Self {
Self { visible, overrides }
}
}
pub struct ComposedFrame<'a> {
pub width: f64,
pub height: f64,
pub blocks: Vec<PlacedBlock<'a>>,
}
pub fn slice_build_steps<'a>(
flow: &'a [BlockNode<'a>],
steps: &[BuildStep<'_>],
width: f64,
height: f64,
style: &ComposeStyle,
shaper: &dyn LineShaper,
) -> Vec<ComposedFrame<'a>> {
let mut regions = FixedRegionSequence::new(Rect::new(0.0, 0.0, width, height));
let base = compose(flow, &mut regions, style, shaper).into_iter().next().unwrap_or_else(|| Frame {
region: Region { rect: Rect::new(0.0, 0.0, width, height) },
blocks: Vec::new(),
overflow: None,
});
steps
.iter()
.map(|step| {
let mut blocks: Vec<PlacedBlock<'a>> = base.blocks.iter().filter(|b| step.visible.contains(&b.id)).cloned().collect();
for &(id, over) in step.overrides {
if let Some(placed) = blocks.iter_mut().find(|b| b.id == id) {
let dx = over.rect.x - placed.rect.x;
let dy = over.rect.y - placed.rect.y;
placed.translate(dx, dy);
placed.rect.width = over.rect.width;
placed.rect.height = over.rect.height;
}
}
ComposedFrame { width, height, blocks }
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use uzor::fonts::FontFamily;
use uzor_text::{CosmicShaper, FontSpec, Paragraph, StyledRun};
use crate::compose::ComposeStyle;
use crate::scene::Block;
fn font() -> FontSpec {
FontSpec::new(FontFamily::Roboto, 16.0)
}
#[test]
fn a_block_absent_from_visible_does_not_appear_in_that_steps_frame() {
let shaper = CosmicShaper::headless();
let run_a = [StyledRun::new("Title.", font())];
let run_b = [StyledRun::new("Body copy.", font())];
let title_id = BlockId(1);
let body_id = BlockId(2);
let flow = [
BlockNode::new(Block::Paragraph(Paragraph::new(&run_a, 300.0))).with_id(title_id),
BlockNode::new(Block::Spacer(10.0)),
BlockNode::new(Block::Paragraph(Paragraph::new(&run_b, 300.0))).with_id(body_id),
];
let style = ComposeStyle::new(6.0, font());
let step0_visible = [title_id];
let step1_visible = [title_id, body_id];
let step0 = BuildStep::new(&step0_visible, &[]);
let step1 = BuildStep::new(&step1_visible, &[]);
let frames = slice_build_steps(&flow, &[step0, step1], 300.0, 400.0, &style, &shaper);
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].blocks.len(), 1, "step 0 only shows the title");
assert_eq!(frames[1].blocks.len(), 2, "step 1 shows both blocks");
assert!(frames[0].blocks.iter().all(|b| b.id == title_id));
}
#[test]
fn a_per_block_override_replaces_the_naturally_composed_rect_for_that_step_only() {
let shaper = CosmicShaper::headless();
let run = [StyledRun::new("Movable caption.", font())];
let block_id = BlockId(1);
let flow = [BlockNode::new(Block::Paragraph(Paragraph::new(&run, 300.0))).with_id(block_id)];
let style = ComposeStyle::new(6.0, font());
let override_rect = Rect::new(250.0, 300.0, 40.0, 20.0);
let overrides = [(block_id, BlockOverride { rect: override_rect })];
let visible = [block_id];
let step0 = BuildStep::new(&visible, &overrides);
let step1 = BuildStep::new(&visible, &[]);
let frames = slice_build_steps(&flow, &[step0, step1], 300.0, 400.0, &style, &shaper);
let step0_rect = frames[0].blocks[0].rect;
let step1_rect = frames[1].blocks[0].rect;
assert_eq!(step0_rect, override_rect, "the override must replace the natural composed rect exactly for its own step");
assert_ne!(step1_rect, override_rect, "a step with no override must keep the naturally composed rect");
}
#[test]
fn the_same_source_block_carries_an_identical_id_across_every_step() {
let shaper = CosmicShaper::headless();
let run = [StyledRun::new("Stable id caption.", font())];
let flow = [BlockNode::new(Block::Paragraph(Paragraph::new(&run, 300.0)))];
let style = ComposeStyle::new(6.0, font());
let step1_visible = [BlockId::structural(0)];
let step0 = BuildStep::new(&[], &[]);
let step1 = BuildStep::new(&step1_visible, &[]);
let frames = slice_build_steps(&flow, &[step0, step1], 300.0, 400.0, &style, &shaper);
assert!(frames[0].blocks.is_empty(), "no visible ids -> an empty frame, never a stub placeholder");
assert_eq!(frames[1].blocks.len(), 1);
assert_eq!(frames[1].blocks[0].id, BlockId::structural(0));
}
}