Skip to main content

laser_pdf/elements/
center_in_preferred_height.rs

1use crate::*;
2
3pub struct CenterInPreferredHeight<E: Element>(pub E);
4
5impl<E: Element> Element for CenterInPreferredHeight<E> {
6    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
7        let layout = self.layout(
8            ctx.text_pieces_cache,
9            ctx.width,
10            ctx.first_height,
11            Some(ctx.full_height),
12        );
13
14        if layout.pre_break {
15            FirstLocationUsage::WillSkip
16        } else if layout.size.height.is_some() {
17            FirstLocationUsage::WillUse
18        } else {
19            FirstLocationUsage::NoneHeight
20        }
21    }
22
23    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
24        let layout = self.layout(
25            ctx.text_pieces_cache,
26            ctx.width,
27            ctx.first_height,
28            ctx.breakable.as_ref().map(|b| b.full_height),
29        );
30
31        if layout.pre_break {
32            let breakable = ctx.breakable.as_mut().unwrap();
33
34            *breakable.break_count = 1;
35        }
36
37        layout.size
38    }
39
40    fn draw(&self, ctx: DrawCtx) -> ElementSize {
41        let layout = self.layout(
42            ctx.text_pieces_cache,
43            ctx.width,
44            ctx.first_height,
45            ctx.breakable.as_ref().map(|b| b.full_height),
46        );
47
48        let height_available;
49        let mut location;
50        let center_height;
51
52        if layout.size.height.is_none() {
53            return layout.size;
54        } else if layout.pre_break {
55            let breakable = ctx.breakable.unwrap();
56
57            location = (breakable.do_break)(ctx.pdf, 0, None);
58            height_available = breakable.full_height;
59
60            center_height = if breakable.preferred_height_break_count == 1 {
61                ctx.preferred_height.unwrap_or(0.)
62            } else {
63                breakable.full_height
64            };
65        } else {
66            location = ctx.location;
67            height_available = ctx.first_height;
68            center_height = if ctx
69                .breakable
70                .map(|b| b.preferred_height_break_count == 0)
71                .unwrap_or(true)
72            {
73                ctx.preferred_height.unwrap_or(0.)
74            } else {
75                ctx.first_height
76            };
77        }
78
79        let y_offset = if let Some(height) = layout.size.height {
80            (center_height - height).max(0.) / 2.
81        } else {
82            0.
83        };
84
85        location.pos.1 -= y_offset;
86
87        self.0.draw(DrawCtx {
88            pdf: ctx.pdf,
89            text_pieces_cache: ctx.text_pieces_cache,
90            location,
91            width: ctx.width,
92            first_height: height_available,
93            preferred_height: None,
94            breakable: None,
95        });
96
97        ElementSize {
98            width: layout.size.width,
99            height: Some(center_height),
100        }
101    }
102}
103
104#[derive(Debug)]
105struct Layout {
106    pre_break: bool,
107    size: ElementSize,
108}
109
110impl<E: Element> CenterInPreferredHeight<E> {
111    fn layout(
112        &self,
113        text_pieces_cache: &TextPiecesCache,
114        width: WidthConstraint,
115        first_height: f32,
116        full_height: Option<f32>,
117    ) -> Layout {
118        let height_available = full_height.unwrap_or(first_height);
119
120        let size = self.0.measure(MeasureCtx {
121            text_pieces_cache,
122            width,
123            first_height: height_available,
124            breakable: None,
125        });
126
127        let pre_break;
128
129        if let (Some(height), Some(full_height)) = (size.height, full_height) {
130            pre_break = height > first_height && full_height > first_height;
131        } else {
132            pre_break = false;
133        };
134
135        Layout { pre_break, size }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::{
143        elements::ref_element::RefElement,
144        test_utils::{record_passes::RecordPasses, *},
145    };
146    use insta::*;
147
148    #[test]
149    fn test_unbreakable() {
150        let output = test_element(
151            TestElementParams {
152                width: WidthConstraint {
153                    max: 12.,
154                    expand: true,
155                },
156                first_height: 21.,
157                preferred_height: Some(20.),
158                breakable: None,
159                pos: (11., 29.0),
160                ..Default::default()
161            },
162            |assert, callback| {
163                let content = RecordPasses::new(FakeText {
164                    lines: 3,
165                    line_height: 5.,
166                    width: 3.,
167                });
168
169                let element = CenterInPreferredHeight(RefElement(&content));
170
171                let ret = callback.call(element);
172
173                if assert {
174                    assert_debug_snapshot!(content.into_passes());
175                }
176
177                ret
178            },
179        );
180
181        assert_debug_snapshot!(output);
182    }
183
184    #[test]
185    fn test_breakable() {
186        let output = test_element(
187            TestElementParams {
188                width: WidthConstraint {
189                    max: 12.,
190                    expand: true,
191                },
192                first_height: 21.,
193                preferred_height: Some(19.),
194                breakable: Some(TestElementParamsBreakable {
195                    full_height: 25.,
196                    preferred_height_break_count: 0,
197                    ..Default::default()
198                }),
199                pos: (11., 29.0),
200                ..Default::default()
201            },
202            |assert, callback| {
203                let content = RecordPasses::new(FakeText {
204                    lines: 3,
205                    line_height: 5.,
206                    width: 3.,
207                });
208
209                let element = CenterInPreferredHeight(RefElement(&content));
210
211                let ret = callback.call(element);
212
213                if assert {
214                    assert_debug_snapshot!(content.into_passes());
215                }
216
217                ret
218            },
219        );
220
221        assert_debug_snapshot!(output);
222    }
223
224    #[test]
225    fn test_pre_break() {
226        let output = test_element(
227            TestElementParams {
228                width: WidthConstraint {
229                    max: 12.,
230                    expand: true,
231                },
232                first_height: 21.,
233                preferred_height: Some(25.),
234                breakable: Some(TestElementParamsBreakable {
235                    full_height: 26.,
236                    preferred_height_break_count: 1,
237                    ..Default::default()
238                }),
239                pos: (11., 29.0),
240                ..Default::default()
241            },
242            |assert, callback| {
243                let content = RecordPasses::new(FakeText {
244                    lines: 5,
245                    line_height: 5.,
246                    width: 3.,
247                });
248
249                let element = CenterInPreferredHeight(RefElement(&content));
250
251                let ret = callback.call(element);
252
253                if assert {
254                    assert_debug_snapshot!(content.into_passes());
255                }
256
257                ret
258            },
259        );
260
261        assert_debug_snapshot!(output);
262    }
263
264    #[test]
265    fn test_preferred_height() {
266        let output = test_element(
267            TestElementParams {
268                width: WidthConstraint {
269                    max: 12.,
270                    expand: true,
271                },
272                first_height: 21.,
273                breakable: Some(TestElementParamsBreakable {
274                    full_height: 26.,
275                    preferred_height_break_count: 4,
276                }),
277                pos: (11., 29.0),
278                preferred_height: None,
279                ..Default::default()
280            },
281            |assert, callback| {
282                let content = RecordPasses::new(FakeText {
283                    lines: 5,
284                    line_height: 5.,
285                    width: 3.,
286                });
287
288                let element = CenterInPreferredHeight(RefElement(&content));
289
290                let ret = callback.call(element);
291
292                if assert {
293                    assert_debug_snapshot!(content.into_passes());
294                }
295
296                ret
297            },
298        );
299
300        assert_debug_snapshot!(output);
301    }
302}