Skip to main content

laser_pdf/elements/
h_align.rs

1use crate::*;
2
3#[derive(Copy, Clone, Serialize, Deserialize)]
4pub enum HorizontalAlignment {
5    Left,
6    Center,
7    Right,
8}
9
10pub struct HAlign<E: Element>(pub HorizontalAlignment, pub E);
11
12impl<E: Element> Element for HAlign<E> {
13    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
14        self.1.first_location_usage(FirstLocationUsageCtx {
15            width: WidthConstraint {
16                expand: false,
17                ..ctx.width
18            },
19            ..ctx
20        })
21    }
22
23    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
24        let width = ctx.width;
25
26        let size = self.1.measure(MeasureCtx {
27            width: WidthConstraint {
28                expand: false,
29                max: width.max,
30            },
31            ..ctx
32        });
33
34        ElementSize {
35            width: size.width.map(|w| width.constrain(w)),
36            height: size.height,
37        }
38    }
39
40    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
41        let width = ctx.width;
42
43        let size = if width.expand {
44            let mut break_count = 0;
45            let mut extra_location_min_height = None;
46
47            let element_size = self.1.measure(MeasureCtx {
48                text_pieces_cache: ctx.text_pieces_cache,
49                width: WidthConstraint {
50                    max: width.max,
51                    expand: false,
52                },
53                first_height: ctx.first_height,
54                breakable: ctx.breakable.as_ref().map(|b| BreakableMeasure {
55                    full_height: b.full_height,
56                    break_count: &mut break_count,
57                    extra_location_min_height: &mut extra_location_min_height,
58                }),
59            });
60
61            let x_offset;
62            let element_width;
63
64            if let Some(w) = element_size.width {
65                x_offset = match self.0 {
66                    HorizontalAlignment::Left => 0.,
67                    HorizontalAlignment::Center => (width.max - w) / 2.0,
68                    HorizontalAlignment::Right => width.max - w,
69                };
70                element_width = w;
71            } else {
72                x_offset = 0.;
73                element_width = ctx.width.max;
74            }
75
76            ctx.location.pos.0 += x_offset;
77
78            let width_constraint = WidthConstraint {
79                max: element_width,
80                expand: true,
81            };
82
83            if let Some(breakable) = ctx.breakable {
84                self.1.draw(DrawCtx {
85                    width: width_constraint,
86                    breakable: Some(BreakableDraw {
87                        full_height: breakable.full_height,
88                        preferred_height_break_count: breakable.preferred_height_break_count,
89                        do_break: &mut |pdf, location_id, height| {
90                            let mut location = (breakable.do_break)(pdf, location_id, height);
91
92                            location.pos.0 += x_offset;
93
94                            location
95                        },
96                    }),
97                    ..ctx
98                })
99            } else {
100                self.1.draw(DrawCtx {
101                    width: width_constraint,
102                    breakable: None,
103                    ..ctx
104                })
105            }
106        } else {
107            self.1.draw(ctx)
108        };
109
110        ElementSize {
111            width: size.width.map(|w| width.constrain(w)),
112            height: size.height,
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::{
120        elements::none::NoneElement,
121        test_utils::{BuildElement, ElementProxy, ElementTestParams, FakeImage, FakeText},
122    };
123
124    use super::*;
125    use HorizontalAlignment::*;
126
127    #[test]
128    fn test_h_align_none() {
129        for output in ElementTestParams::default().run(&HAlign(Center, NoneElement)) {
130            output.assert_size(ElementSize {
131                width: None,
132                height: None,
133            });
134
135            if let Some(b) = output.breakable {
136                b.assert_break_count(0)
137                    .assert_extra_location_min_height(None);
138            }
139        }
140    }
141
142    #[test]
143    fn test_h_align_fake_text() {
144        let element = BuildElement(|build_ctx, callback| {
145            let content = FakeText {
146                width: 5.,
147                line_height: 1.,
148                lines: 10,
149            };
150
151            let proxy = ElementProxy {
152                before_draw: &|ctx: &mut DrawCtx| {
153                    assert_eq!(
154                        ctx.width,
155                        WidthConstraint {
156                            max: if build_ctx.width.expand { 5. } else { 6. },
157                            expand: build_ctx.width.expand,
158                        }
159                    );
160                    assert_eq!(
161                        ctx.location.pos.0,
162                        12. + if ctx.width.expand { 0.5 } else { 0. }
163                    );
164                },
165                after_break: &|_location_idx: u32,
166                               location: &Location,
167                               width: WidthConstraint,
168                               _first_height| {
169                    assert_eq!(location.pos.0, 12. + if width.expand { 0.5 } else { 0. });
170                },
171                ..ElementProxy::new(content)
172            };
173            callback.call(HAlign(Center, proxy))
174        });
175
176        for output in (ElementTestParams {
177            first_height: 1.,
178            full_height: 2.,
179            width: 6.,
180            ..Default::default()
181        })
182        .run(&element)
183        {
184            output.assert_size(ElementSize {
185                width: Some(output.width.constrain(5.)),
186                height: Some(if output.breakable.is_none() {
187                    10.
188                } else if output.first_height == 1. {
189                    1.
190                } else {
191                    2.
192                }),
193            });
194
195            if let Some(b) = output.breakable {
196                b.assert_break_count(if output.first_height == 1. { 5 } else { 4 })
197                    .assert_extra_location_min_height(None);
198            }
199        }
200    }
201
202    #[test]
203    fn test_h_align_too_wide() {
204        // If the element wants to be wider than the width constraint the element should just get
205        // the width constraint.
206
207        let element = BuildElement(|build_ctx, callback| {
208            let content = FakeText {
209                width: 5.,
210                line_height: 1.,
211                lines: 2,
212            };
213
214            let proxy = ElementProxy {
215                before_draw: &|ctx: &mut DrawCtx| {
216                    assert_eq!(
217                        ctx.width,
218                        WidthConstraint {
219                            max: 4.,
220                            expand: build_ctx.width.expand,
221                        }
222                    );
223                    assert_eq!(ctx.location.pos.0, 12.);
224                },
225                after_break: &|_location_idx: u32,
226                               location: &Location,
227                               _width: WidthConstraint,
228                               _first_height| {
229                    assert_eq!(location.pos.0, 12.);
230                },
231                ..ElementProxy::new(content)
232            };
233            callback.call(HAlign(Center, proxy))
234        });
235
236        for output in (ElementTestParams {
237            first_height: 1.,
238            full_height: 2.,
239            width: 4.,
240            ..Default::default()
241        })
242        .run(&element)
243        {
244            output.assert_size(ElementSize {
245                width: Some(output.width.constrain(5.)),
246                height: Some(if output.breakable.is_none() {
247                    2.
248                } else if output.first_height == 1. {
249                    1.
250                } else {
251                    2.
252                }),
253            });
254
255            if let Some(b) = output.breakable {
256                b.assert_break_count(if output.first_height == 1. { 1 } else { 0 })
257                    .assert_extra_location_min_height(None);
258            }
259        }
260    }
261
262    #[test]
263    fn test_overdraw() {
264        struct Overdraw;
265
266        impl Element for Overdraw {
267            fn measure(&self, _: MeasureCtx) -> ElementSize {
268                ElementSize {
269                    width: Some(5.),
270                    height: None,
271                }
272            }
273
274            fn draw(&self, _: DrawCtx) -> ElementSize {
275                ElementSize {
276                    width: Some(5.),
277                    height: None,
278                }
279            }
280        }
281
282        for output in (ElementTestParams {
283            first_height: 1.,
284            full_height: 2.,
285            width: 4.,
286            ..Default::default()
287        })
288        .run(&HAlign(Center, Overdraw))
289        {
290            output.assert_size(ElementSize {
291                width: Some(4.),
292                height: None,
293            });
294
295            if let Some(b) = output.breakable {
296                b.assert_break_count(0)
297                    .assert_extra_location_min_height(None);
298            }
299        }
300    }
301
302    #[test]
303    fn test_h_align_fake_image() {
304        let element = BuildElement(|build_ctx, callback| {
305            let content = FakeImage {
306                width: 5.,
307                height: 2.,
308            };
309
310            let proxy = ElementProxy {
311                before_draw: &|ctx: &mut DrawCtx| {
312                    assert_eq!(
313                        ctx.width,
314                        WidthConstraint {
315                            max: if build_ctx.width.expand { 5. } else { 10. },
316                            expand: build_ctx.width.expand,
317                        }
318                    );
319                    assert_eq!(
320                        ctx.location.pos.0,
321                        12. + if ctx.width.expand { 5. } else { 0. }
322                    );
323                },
324                after_break: &|_location_idx: u32,
325                               location: &Location,
326                               width: WidthConstraint,
327                               _first_height| {
328                    assert_eq!(location.pos.0, 12. + if width.expand { 5. } else { 0. });
329                },
330                ..ElementProxy::new(content)
331            };
332            callback.call(HAlign(Right, proxy))
333        });
334
335        for output in (ElementTestParams {
336            first_height: 1.,
337            full_height: 4.,
338            width: 10.,
339            ..Default::default()
340        })
341        .run(&element)
342        {
343            output.assert_size(ElementSize {
344                width: Some(output.width.constrain(5.)),
345                height: Some(2.),
346            });
347
348            if let Some(b) = output.breakable {
349                b.assert_break_count(if output.first_height == 1. { 1 } else { 0 })
350                    .assert_extra_location_min_height(None);
351            }
352        }
353    }
354
355    #[test]
356    fn test_h_align_fake_image_too_wide() {
357        let element = BuildElement(|build_ctx, callback| {
358            let content = FakeImage {
359                width: 10.,
360                height: 4.,
361            };
362
363            let proxy = ElementProxy {
364                before_draw: &|ctx: &mut DrawCtx| {
365                    assert_eq!(
366                        ctx.width,
367                        WidthConstraint {
368                            max: 5.,
369                            expand: build_ctx.width.expand,
370                        }
371                    );
372                    assert_eq!(ctx.location.pos.0, 12.);
373                },
374                after_break: &|_location_idx: u32,
375                               location: &Location,
376                               _width: WidthConstraint,
377                               _first_height| {
378                    assert_eq!(location.pos.0, 12.);
379                },
380                ..ElementProxy::new(content)
381            };
382            callback.call(HAlign(Right, proxy))
383        });
384
385        for output in (ElementTestParams {
386            first_height: 1.,
387            full_height: 4.,
388            width: 5.,
389            ..Default::default()
390        })
391        .run(&element)
392        {
393            output.assert_size(ElementSize {
394                width: Some(5.),
395                height: Some(2.),
396            });
397
398            if let Some(b) = output.breakable {
399                b.assert_break_count(if output.first_height == 1. { 1 } else { 0 })
400                    .assert_extra_location_min_height(None);
401            }
402        }
403    }
404
405    #[test]
406    fn test_h_align_cancel_out() {
407        // Alignments should be able to cancel each other out.
408
409        let element = BuildElement(|build_ctx, callback| {
410            let content = FakeImage {
411                width: 5.,
412                height: 2.,
413            };
414
415            let proxy = ElementProxy {
416                before_draw: &|ctx: &mut DrawCtx| {
417                    assert_eq!(
418                        ctx.width,
419                        WidthConstraint {
420                            max: if build_ctx.width.expand { 5. } else { 10. },
421                            expand: build_ctx.width.expand,
422                        }
423                    );
424                    assert_eq!(ctx.location.pos.0, 12.);
425                },
426                after_break: &|_location_idx: u32,
427                               location: &Location,
428                               _width: WidthConstraint,
429                               _first_height| {
430                    assert_eq!(location.pos.0, 12.);
431                },
432                ..ElementProxy::new(content)
433            };
434            callback.call(HAlign(Left, HAlign(Right, proxy)))
435        });
436
437        for output in (ElementTestParams {
438            first_height: 1.,
439            full_height: 4.,
440            width: 10.,
441            ..Default::default()
442        })
443        .run(&element)
444        {
445            output.assert_size(ElementSize {
446                width: Some(output.width.constrain(5.)),
447                height: Some(2.),
448            });
449
450            if let Some(b) = output.breakable {
451                b.assert_break_count(if output.first_height == 1. { 1 } else { 0 })
452                    .assert_extra_location_min_height(None);
453            }
454        }
455    }
456}