1use deckcraft_color::{ColorScheme, Rgba, SchemeSlot};
5use deckcraft_geom::Xfrm;
6
7use crate::style::{ColorRef, Effects, Fill, Line};
8use crate::text::{Anchor, AutoFit, BodyProps, LevelStyle, ListStyle, ParaProps, RunProps};
9use crate::{Background, Layout, Master, PhType, Presentation, Shape, Slide};
10
11#[derive(Clone, Copy)]
13pub enum Owner {
14 Slide,
15 Layout,
16 Master,
17}
18
19pub struct Ctx<'a> {
20 pub pres: &'a Presentation,
21 pub master: &'a Master,
22 pub layout: Option<&'a Layout>,
23 pub owner: Owner,
24 pub scheme: ColorScheme,
25 pub color_map: Vec<(String, String)>,
27}
28
29impl<'a> Ctx<'a> {
30 pub fn for_slide(pres: &'a Presentation, slide: &Slide) -> Option<Ctx<'a>> {
31 let (master, layout) = pres.master_for(slide)?;
32 let map = slide
33 .color_map
34 .as_ref()
35 .or(layout.and_then(|l| l.color_map.as_ref()))
36 .unwrap_or(&master.color_map)
37 .clone();
38 Some(Ctx {
39 pres,
40 master,
41 layout,
42 owner: Owner::Slide,
43 scheme: master.scheme_with(&map),
44 color_map: map,
45 })
46 }
47 pub fn for_layout(pres: &'a Presentation, master: &'a Master, layout: &'a Layout) -> Ctx<'a> {
48 let map = layout
49 .color_map
50 .as_ref()
51 .unwrap_or(&master.color_map)
52 .clone();
53 Ctx {
54 pres,
55 master,
56 layout: Some(layout),
57 owner: Owner::Layout,
58 scheme: master.scheme_with(&map),
59 color_map: map,
60 }
61 }
62 pub fn for_master(pres: &'a Presentation, master: &'a Master) -> Ctx<'a> {
63 Ctx {
64 pres,
65 master,
66 layout: None,
67 owner: Owner::Master,
68 scheme: master.scheme(),
69 color_map: master.color_map.clone(),
70 }
71 }
72 pub fn color(&self, c: &ColorRef, ph: Option<Rgba>) -> Rgba {
73 let c = match &c.base {
74 crate::ColorBase::Scheme { slot }
75 if matches!(
76 slot,
77 SchemeSlot::Bg1 | SchemeSlot::Tx1 | SchemeSlot::Bg2 | SchemeSlot::Tx2
78 ) =>
79 {
80 ColorRef {
81 base: crate::ColorBase::Scheme {
82 slot: crate::map_slot(&self.color_map, *slot),
83 },
84 mods: c.mods.clone(),
85 }
86 }
87 _ => c.clone(),
88 };
89 c.resolve(&self.master.theme.colors, ph)
90 }
91}
92
93fn compatible(slide: PhType, other: PhType) -> bool {
94 use PhType::*;
95 match slide {
96 Title | CtrTitle => matches!(other, Title | CtrTitle),
97 SubTitle | Body | Obj | Chart | Table | ClipArt | Diagram | Media | Picture => {
98 !matches!(other, Title | CtrTitle | Date | Footer | SlideNum | Header)
99 }
100 k => k == other,
101 }
102}
103
104fn find_ph<'s>(shapes: &'s [Shape], shape: &Shape, by_idx: bool) -> Option<&'s Shape> {
105 let ph = shape.ph.as_ref()?;
106 if by_idx
107 && ph.idx != 0
108 && let Some(s) = shapes.iter().find(|s| {
109 s.ph.as_ref()
110 .is_some_and(|p| p.idx == ph.idx && compatible(ph.kind, p.kind))
111 })
112 {
113 return Some(s);
114 }
115 shapes
117 .iter()
118 .find(|s| {
119 s.ph.as_ref().is_some_and(|p| {
120 p.kind == ph.kind
121 && (!by_idx || ph.idx == 0 || p.idx == ph.idx || ph.kind.is_title())
122 })
123 })
124 .or_else(|| {
125 shapes
126 .iter()
127 .find(|s| s.ph.as_ref().is_some_and(|p| p.kind == ph.kind))
128 })
129 .or_else(|| {
130 shapes
131 .iter()
132 .find(|s| s.ph.as_ref().is_some_and(|p| compatible(ph.kind, p.kind)))
133 })
134}
135
136fn master_ph(master: &Master, kind: PhType) -> Option<&Shape> {
137 let want = match kind {
138 PhType::Title | PhType::CtrTitle => PhType::Title,
139 PhType::Date | PhType::Footer | PhType::SlideNum | PhType::Header => kind,
140 _ => PhType::Body,
141 };
142 master.shapes.iter().find(|s| s.ph_type() == Some(want))
143}
144
145pub fn parents<'a>(ctx: &Ctx<'a>, shape: &Shape) -> (Option<&'a Shape>, Option<&'a Shape>) {
147 let Some(ph) = shape.ph.as_ref() else {
148 return (None, None);
149 };
150 match ctx.owner {
151 Owner::Slide => {
152 let lp = ctx.layout.and_then(|l| find_ph(&l.shapes, shape, true));
153 let kind = lp.and_then(Shape::ph_type).unwrap_or(ph.kind);
154 (lp, master_ph(ctx.master, kind))
155 }
156 Owner::Layout => (None, master_ph(ctx.master, ph.kind)),
157 Owner::Master => (None, None),
158 }
159}
160
161pub fn master_list_style<'a>(ctx: &Ctx<'a>, shape: &Shape) -> &'a ListStyle {
163 match shape.ph_type() {
164 Some(PhType::Title | PhType::CtrTitle) => &ctx.master.title_style,
165 Some(
166 PhType::Body
167 | PhType::Obj
168 | PhType::SubTitle
169 | PhType::Table
170 | PhType::Chart
171 | PhType::Diagram
172 | PhType::Media
173 | PhType::Picture
174 | PhType::ClipArt,
175 ) => &ctx.master.body_style,
176 Some(_) => &ctx.master.other_style,
177 None => &ctx.pres.default_text_style,
178 }
179}
180
181pub fn xfrm(ctx: &Ctx, shape: &Shape) -> Xfrm {
182 if let Some(x) = shape.xfrm {
183 return x;
184 }
185 let (lp, mp) = parents(ctx, shape);
186 lp.and_then(|s| s.xfrm)
187 .or_else(|| mp.and_then(|s| s.xfrm))
188 .unwrap_or_else(|| {
189 let sz = ctx.pres.slide_size;
190 Xfrm::new(
191 sz.width * 0.1,
192 sz.height * 0.1,
193 sz.width * 0.8,
194 sz.height * 0.2,
195 )
196 })
197}
198
199pub fn fill(ctx: &Ctx, shape: &Shape) -> (Option<Fill>, Option<Rgba>) {
201 let (lp, mp) = parents(ctx, shape);
202 let style_fill = |s: &Shape| {
203 s.style.as_ref().map(|st| {
204 let (idx, col) = &st.fill_ref;
205 let ph = ctx.color(col, None);
206 let f = match *idx {
207 0 => Some(Fill::None),
208 i if i >= 1001 => ctx
209 .master
210 .theme
211 .format
212 .bg_fills
213 .get((i - 1001) as usize)
214 .cloned(),
215 i => ctx.master.theme.format.fills.get((i - 1) as usize).cloned(),
216 };
217 (f, Some(ph))
218 })
219 };
220 for s in [Some(shape), lp, mp].into_iter().flatten() {
221 if let Some(f) = &s.fill {
222 if let Fill::Gradient(g) = f
225 && g.stops.is_empty()
226 && let Some((Some(Fill::Gradient(sg)), ph)) = style_fill(s)
227 {
228 let mut g = g.clone();
229 g.stops = sg.stops;
230 return (Some(Fill::Gradient(g)), ph);
231 }
232 return (Some(f.clone()), None);
233 }
234 if let Some(r) = style_fill(s) {
235 return r;
236 }
237 }
238 if shape.text_box {
239 return (Some(Fill::None), None);
240 }
241 (None, None)
242}
243
244pub fn line(ctx: &Ctx, shape: &Shape) -> (Line, Option<Rgba>) {
245 let (lp, mp) = parents(ctx, shape);
246 let mut out = Line::default();
247 let mut ph = None;
248 for s in [Some(shape), lp, mp].into_iter().flatten() {
249 if let Some(l) = &s.line {
250 out.inherit(l);
251 }
252 if let Some(st) = &s.style {
253 let (idx, col) = &st.line_ref;
254 if ph.is_none() {
255 ph = Some(ctx.color(col, None));
256 }
257 if *idx == 0 {
258 out.inherit(&Line::none());
259 } else if let Some(l) = ctx.master.theme.format.lines.get((*idx - 1) as usize) {
260 out.inherit(l);
261 }
262 break;
263 }
264 }
265 (out, ph)
266}
267
268pub fn effects(ctx: &Ctx, shape: &Shape) -> (Option<Effects>, Option<Rgba>) {
269 let (lp, mp) = parents(ctx, shape);
270 for s in [Some(shape), lp, mp].into_iter().flatten() {
271 if let Some(e) = &s.effects {
272 return (Some(e.clone()), None);
273 }
274 if let Some(st) = &s.style {
275 let (idx, col) = &st.effect_ref;
276 if *idx == 0 {
277 return (None, None);
278 }
279 return (
280 ctx.master
281 .theme
282 .format
283 .effects
284 .get((*idx - 1) as usize)
285 .cloned(),
286 Some(ctx.color(col, None)),
287 );
288 }
289 }
290 (None, None)
291}
292
293pub fn body(ctx: &Ctx, shape: &Shape) -> BodyProps {
295 let (lp, mp) = parents(ctx, shape);
296 let mut b = shape
297 .text
298 .as_ref()
299 .map(|t| t.body.clone())
300 .unwrap_or_default();
301 for s in [lp, mp].into_iter().flatten() {
302 if let Some(t) = &s.text {
303 b.inherit(&t.body);
304 }
305 }
306 b.inherit(&BodyProps {
307 inset_l: Some(7.2),
308 inset_t: Some(3.6),
309 inset_r: Some(7.2),
310 inset_b: Some(3.6),
311 anchor: Some(
312 if shape.ph.is_none()
313 && !shape.text_box
314 && !matches!(shape.kind, crate::ShapeKind::Table(_))
315 {
316 Anchor::Middle
317 } else {
318 Anchor::Top
319 },
320 ),
321 anchor_ctr: Some(false),
322 wrap: Some(true),
323 autofit: Some(if shape.text_box {
324 AutoFit::Shape
325 } else {
326 AutoFit::None
327 }),
328 vert: Some(crate::text::TextDir::Horizontal),
329 rot: Some(0.0),
330 columns: Some(1),
331 col_spacing: Some(0.0),
332 warp: None,
333 warp_adj: Vec::new(),
334 rtl_col: Some(false),
335 upright: Some(false),
336 });
337 b
338}
339
340pub fn level_style(ctx: &Ctx, shape: &Shape, level: u8) -> LevelStyle {
342 let (lp, mp) = parents(ctx, shape);
343 let mut out = LevelStyle::default();
344 let add = |out: &mut LevelStyle, ls: Option<&LevelStyle>| {
345 if let Some(l) = ls {
346 out.para.inherit(&l.para);
347 out.run.inherit(&l.run);
348 }
349 };
350 if let Some(t) = &shape.text {
351 add(&mut out, t.list_style.level(level));
352 }
353 for s in [lp, mp].into_iter().flatten() {
354 if let Some(t) = &s.text {
355 add(&mut out, t.list_style.level(level));
356 }
357 }
358 if let Some(st) = shape.style.as_ref().or(lp.and_then(|s| s.style.as_ref())) {
360 let font = match st.font_ref.0.as_str() {
361 "major" => Some("+mj-lt".to_string()),
362 "minor" => Some("+mn-lt".to_string()),
363 _ => None,
364 };
365 let r = RunProps {
366 font,
367 fill: st.font_ref.1.clone().map(Fill::solid),
368 ..Default::default()
369 };
370 out.run.inherit(&r);
371 }
372 add(&mut out, master_list_style(ctx, shape).level(level));
373 add(&mut out, ctx.pres.default_text_style.level(level));
374 out.run.inherit(&RunProps {
376 size: Some(18.0),
377 font: Some("+mn-lt".into()),
378 bold: Some(false),
379 italic: Some(false),
380 ..Default::default()
381 });
382 out.para.inherit(&ParaProps {
383 margin_left: Some(0.0),
384 indent: Some(0.0),
385 align: Some(crate::text::Align::Left),
386 ..Default::default()
387 });
388 out
389}
390
391pub fn run(ctx: &Ctx, shape: &Shape, para: &crate::text::Paragraph, run: &RunProps) -> RunProps {
393 let mut r = run.clone();
394 let lvl = level_style(ctx, shape, para.level);
395 r.inherit(&lvl.run);
396 if r.link.is_some() {
400 if run.fill.is_none() {
401 r.fill = Some(Fill::solid(ColorRef::scheme(SchemeSlot::Hlink)));
402 }
403 if run.underline.is_none() {
404 r.underline = Some("sng".into());
405 }
406 }
407 r
408}
409
410pub fn para(ctx: &Ctx, shape: &Shape, para: &crate::text::Paragraph) -> ParaProps {
411 let mut p = para.props.clone();
412 let lvl = level_style(ctx, shape, para.level);
413 p.inherit(&lvl.para);
414 p
415}
416
417pub fn font_family(ctx: &Ctx, r: &RunProps) -> String {
419 let f = r.font.as_deref().unwrap_or("+mn-lt");
420 let name = ctx.master.theme.font(f);
421 if name.is_empty() {
422 ctx.master.theme.fonts.minor.latin.clone()
423 } else {
424 name
425 }
426}
427
428pub fn text_color(ctx: &Ctx, r: &RunProps) -> Rgba {
430 match &r.fill {
431 Some(Fill::Solid { color }) => ctx.color(color, None),
432 Some(Fill::Gradient(g)) => g
433 .stops
434 .first()
435 .map(|s| ctx.color(&s.color, None))
436 .unwrap_or(Rgba::BLACK),
437 Some(Fill::None) => Rgba::TRANSPARENT,
438 _ => ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None),
439 }
440}
441
442pub fn background(ctx: &Ctx, slide: Option<&Slide>) -> (Fill, Option<Rgba>) {
444 let bg = slide
445 .and_then(|s| s.background.as_ref())
446 .or(ctx.layout.and_then(|l| l.background.as_ref()))
447 .or(ctx.master.background.as_ref());
448 match bg {
449 Some(Background::Fill { fill }) => (fill.clone(), None),
450 Some(Background::Ref { idx, color }) => {
451 let ph = ctx.color(color, None);
452 let f = if *idx >= 1001 {
453 ctx.master
454 .theme
455 .format
456 .bg_fills
457 .get((*idx - 1001) as usize)
458 .cloned()
459 } else {
460 ctx.master
461 .theme
462 .format
463 .fills
464 .get(idx.saturating_sub(1) as usize)
465 .cloned()
466 };
467 (f.unwrap_or(Fill::solid(ColorRef::rgb(ph))), Some(ph))
468 }
469 None => (Fill::solid(ColorRef::scheme(SchemeSlot::Bg1)), None),
470 }
471}
472
473pub fn show_master_shapes(slide: &Slide, layout: Option<&Layout>) -> (bool, bool) {
475 let show_layout = slide.show_master_shapes;
476 let show_master = show_layout && layout.is_none_or(|l| l.show_master_shapes);
477 (show_layout, show_master)
478}