1use deckcraft_model::resolve::{self, Ctx};
5use deckcraft_model::text::{Hyperlink, TextBody};
6use deckcraft_model::{Presentation, Shape, ShapeKind};
7use deckcraft_text::{Opts, TextLayout};
8use kurbo::{Affine, Rect};
9
10use crate::{SlideFields, shape_geometry, table};
11
12pub struct PlacedText {
14 pub layout: TextLayout,
15 pub transform: Affine,
17 pub body: TextBody,
18}
19
20pub struct PlacedLink {
22 pub rect: Rect,
24 pub link: Hyperlink,
25}
26
27#[derive(Default)]
28pub struct Placed {
29 pub texts: Vec<PlacedText>,
30 pub links: Vec<PlacedLink>,
31}
32
33pub fn place_slide(pres: &Presentation, index: usize) -> Placed {
35 let mut out = Placed::default();
36 let Some(slide) = pres.slides.get(index) else {
37 return out;
38 };
39 let Some(rctx) = Ctx::for_slide(pres, slide) else {
40 return out;
41 };
42 let fields = SlideFields::new(
43 pres.slide_number(index),
44 pres.header_footer.footer_text.clone(),
45 &crate::RenderOpts::default(),
46 );
47 let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout);
48 if show_master {
49 let mctx = Ctx::for_master(pres, rctx.master);
50 for s in rctx.master.shapes.iter().filter(|s| s.ph.is_none()) {
51 place(&mut out, &mctx, &fields, s, Affine::IDENTITY, 0);
52 }
53 }
54 if show_layout && let Some(l) = rctx.layout {
55 let lctx = Ctx::for_layout(pres, rctx.master, l);
56 for s in l.shapes.iter().filter(|s| s.ph.is_none()) {
57 place(&mut out, &lctx, &fields, s, Affine::IDENTITY, 0);
58 }
59 }
60 for s in &slide.shapes {
61 place(&mut out, &rctx, &fields, s, Affine::IDENTITY, 0);
62 }
63 out
64}
65
66fn place(
67 out: &mut Placed,
68 rctx: &Ctx,
69 fields: &SlideFields,
70 s: &Shape,
71 parent: Affine,
72 depth: usize,
73) {
74 if depth > 32 || s.hidden {
75 return;
76 }
77 let x = resolve::xfrm(rctx, s);
78 if !x.is_finite() {
79 return;
80 }
81 let m = parent * x.affine();
82 if let Some(link) = &s.click {
83 out.links.push(PlacedLink {
84 rect: m.transform_rect_bbox(Rect::new(0.0, 0.0, x.w, x.h)),
85 link: link.clone(),
86 });
87 }
88 match &s.kind {
89 ShapeKind::Group { children, child } => {
90 let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
91 let gm = parent * deckcraft_geom::group_child_affine(&x, ch);
92 for c in children {
93 place(out, rctx, fields, c, gm, depth + 1);
94 }
95 }
96 ShapeKind::Table(t) => {
97 for (r, c, rect) in table::cell_rects(t, &table::row_heights(rctx, t, fields)) {
99 let Some(cell) = t.cell(r, c).filter(|cell| !cell.text.is_empty()) else {
100 continue;
101 };
102 let body = table::cell_body(rctx, t, r, c, cell);
103 let tmp = Shape {
104 text: Some(body.clone()),
105 ..Default::default()
106 };
107 place_text(out, rctx, fields, &tmp, &body, rect, m);
108 }
109 }
110 ShapeKind::Chart(_) | ShapeKind::Ink { .. } => {}
111 _ => {
112 let Some(body) = s.text.as_ref().filter(|b| !b.is_empty()) else {
113 return;
114 };
115 let tr = shape_geometry(s, x.w, x.h).text_rect;
116 place_text(out, rctx, fields, s, body, tr, m);
117 }
118 }
119}
120
121fn place_text(
123 out: &mut Placed,
124 rctx: &Ctx,
125 fields: &SlideFields,
126 s: &Shape,
127 body: &TextBody,
128 tr: Rect,
129 m: Affine,
130) {
131 let layout = deckcraft_text::layout(
132 rctx,
133 s,
134 body,
135 &Opts {
136 rect: tr,
137 fields,
138 prompt_color: None,
139 no_shrink: false,
140 },
141 );
142 let m = crate::text_transform(m, tr, layout.rotation);
143 for run in layout
145 .runs
146 .iter()
147 .filter(|r| r.link && !r.glyphs.is_empty())
148 {
149 let Some(link) = run_link(body, run.para, run.chars.0) else {
150 continue;
151 };
152 let Some(line) = layout
153 .lines
154 .iter()
155 .find(|l| l.para == run.para && l.start <= run.chars.0 && run.chars.0 <= l.end)
156 else {
157 continue;
158 };
159 let first = run.glyphs.first().map(|g| g.1).unwrap_or(0.0);
161 let (x0, x1) = line
162 .span(run.chars.0, run.chars.1)
163 .unwrap_or((first, first));
164 let r = Rect::new(x0, line.top, x1, line.bottom);
165 out.links.push(PlacedLink {
166 rect: m.transform_rect_bbox(r),
167 link,
168 });
169 }
170 out.texts.push(PlacedText {
171 layout,
172 transform: m,
173 body: body.clone(),
174 });
175}
176
177fn run_link(body: &TextBody, para: usize, at: usize) -> Option<Hyperlink> {
179 let p = body.paragraphs.get(para)?;
180 let mut pos = 0;
181 for r in &p.runs {
182 let n = r.char_len();
183 if at < pos + n.max(1) {
184 return r.props.link.clone();
185 }
186 pos += n;
187 }
188 None
189}