use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::text::{Hyperlink, TextBody};
use deckcraft_model::{Presentation, Shape, ShapeKind};
use deckcraft_text::{Opts, TextLayout};
use kurbo::{Affine, Rect};
use crate::{SlideFields, shape_geometry, table};
pub struct PlacedText {
pub layout: TextLayout,
pub transform: Affine,
pub body: TextBody,
}
pub struct PlacedLink {
pub rect: Rect,
pub link: Hyperlink,
}
#[derive(Default)]
pub struct Placed {
pub texts: Vec<PlacedText>,
pub links: Vec<PlacedLink>,
}
pub fn place_slide(pres: &Presentation, index: usize) -> Placed {
let mut out = Placed::default();
let Some(slide) = pres.slides.get(index) else {
return out;
};
let Some(rctx) = Ctx::for_slide(pres, slide) else {
return out;
};
let fields = SlideFields::new(
pres.slide_number(index),
pres.header_footer.footer_text.clone(),
&crate::RenderOpts::default(),
);
let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout);
if show_master {
let mctx = Ctx::for_master(pres, rctx.master);
for s in rctx.master.shapes.iter().filter(|s| s.ph.is_none()) {
place(&mut out, &mctx, &fields, s, Affine::IDENTITY, 0);
}
}
if show_layout && let Some(l) = rctx.layout {
let lctx = Ctx::for_layout(pres, rctx.master, l);
for s in l.shapes.iter().filter(|s| s.ph.is_none()) {
place(&mut out, &lctx, &fields, s, Affine::IDENTITY, 0);
}
}
for s in &slide.shapes {
place(&mut out, &rctx, &fields, s, Affine::IDENTITY, 0);
}
out
}
fn place(
out: &mut Placed,
rctx: &Ctx,
fields: &SlideFields,
s: &Shape,
parent: Affine,
depth: usize,
) {
if depth > 32 || s.hidden {
return;
}
let x = resolve::xfrm(rctx, s);
if !x.is_finite() {
return;
}
let m = parent * x.affine();
if let Some(link) = &s.click {
out.links.push(PlacedLink {
rect: m.transform_rect_bbox(Rect::new(0.0, 0.0, x.w, x.h)),
link: link.clone(),
});
}
match &s.kind {
ShapeKind::Group { children, child } => {
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
let gm = parent * deckcraft_geom::group_child_affine(&x, ch);
for c in children {
place(out, rctx, fields, c, gm, depth + 1);
}
}
ShapeKind::Table(t) => {
for (r, c, rect) in table::cell_rects(t, &table::row_heights(rctx, t, fields)) {
let Some(cell) = t.cell(r, c).filter(|cell| !cell.text.is_empty()) else {
continue;
};
let body = table::cell_body(rctx, t, r, c, cell);
let tmp = Shape {
text: Some(body.clone()),
..Default::default()
};
place_text(out, rctx, fields, &tmp, &body, rect, m);
}
}
ShapeKind::Chart(_) | ShapeKind::Ink { .. } => {}
_ => {
let Some(body) = s.text.as_ref().filter(|b| !b.is_empty()) else {
return;
};
let tr = shape_geometry(s, x.w, x.h).text_rect;
place_text(out, rctx, fields, s, body, tr, m);
}
}
}
fn place_text(
out: &mut Placed,
rctx: &Ctx,
fields: &SlideFields,
s: &Shape,
body: &TextBody,
tr: Rect,
m: Affine,
) {
let layout = deckcraft_text::layout(
rctx,
s,
body,
&Opts {
rect: tr,
fields,
prompt_color: None,
no_shrink: false,
},
);
let m = crate::text_transform(m, tr, layout.rotation);
for run in layout
.runs
.iter()
.filter(|r| r.link && !r.glyphs.is_empty())
{
let Some(link) = run_link(body, run.para, run.chars.0) else {
continue;
};
let Some(line) = layout
.lines
.iter()
.find(|l| l.para == run.para && l.start <= run.chars.0 && run.chars.0 <= l.end)
else {
continue;
};
let first = run.glyphs.first().map(|g| g.1).unwrap_or(0.0);
let (x0, x1) = line
.span(run.chars.0, run.chars.1)
.unwrap_or((first, first));
let r = Rect::new(x0, line.top, x1, line.bottom);
out.links.push(PlacedLink {
rect: m.transform_rect_bbox(r),
link,
});
}
out.texts.push(PlacedText {
layout,
transform: m,
body: body.clone(),
});
}
fn run_link(body: &TextBody, para: usize, at: usize) -> Option<Hyperlink> {
let p = body.paragraphs.get(para)?;
let mut pos = 0;
for r in &p.runs {
let n = r.char_len();
if at < pos + n.max(1) {
return r.props.link.clone();
}
pos += n;
}
None
}