plotters/element/
text.rs

1use std::borrow::Borrow;
2
3use super::{Drawable, PointCollection};
4use crate::style::{FontDesc, FontResult, LayoutBox, TextStyle};
5use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
6
7/// A single line text element. This can be owned or borrowed string, dependents on
8/// `String` or `str` moved into.
9pub struct Text<'a, Coord, T: Borrow<str>> {
10    text: T,
11    coord: Coord,
12    style: TextStyle<'a>,
13}
14
15impl<'a, Coord, T: Borrow<str>> Text<'a, Coord, T> {
16    /// Create a new text element
17    /// - `text`: The text for the element
18    /// - `points`: The upper left conner for the text element
19    /// - `style`: The text style
20    /// - Return the newly created text element
21    pub fn new<S: Into<TextStyle<'a>>>(text: T, points: Coord, style: S) -> Self {
22        Self {
23            text,
24            coord: points,
25            style: style.into(),
26        }
27    }
28}
29
30impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord> for &'a Text<'b, Coord, T> {
31    type Point = &'a Coord;
32    type IntoIter = std::iter::Once<&'a Coord>;
33    fn point_iter(self) -> Self::IntoIter {
34        std::iter::once(&self.coord)
35    }
36}
37
38impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB> for Text<'a, Coord, T> {
39    fn draw<I: Iterator<Item = BackendCoord>>(
40        &self,
41        mut points: I,
42        backend: &mut DB,
43        _: (u32, u32),
44    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
45        if let Some(a) = points.next() {
46            return backend.draw_text(self.text.borrow(), &self.style, a);
47        }
48        Ok(())
49    }
50}
51
52/// An multi-line text element. The `Text` element allows only single line text
53/// and the `MultiLineText` supports drawing multiple lines
54pub struct MultiLineText<'a, Coord, T: Borrow<str>> {
55    lines: Vec<T>,
56    coord: Coord,
57    style: TextStyle<'a>,
58    line_height: f64,
59}
60
61impl<'a, Coord, T: Borrow<str>> MultiLineText<'a, Coord, T> {
62    /// Create an empty multi-line text element.
63    /// Lines can be append to the empty multi-line by calling `push_line` method
64    ///
65    /// `pos`: The upper left corner
66    /// `style`: The style of the text
67    pub fn new<S: Into<TextStyle<'a>>>(pos: Coord, style: S) -> Self {
68        MultiLineText {
69            lines: vec![],
70            coord: pos,
71            style: style.into(),
72            line_height: 1.25,
73        }
74    }
75
76    /// Set the line height of the multi-line text element
77    pub fn set_line_height(&mut self, value: f64) -> &mut Self {
78        self.line_height = value;
79        self
80    }
81
82    /// Push a new line into the given multi-line text
83    /// `line`: The line to be pushed
84    pub fn push_line<L: Into<T>>(&mut self, line: L) {
85        self.lines.push(line.into());
86    }
87
88    /// Estimate the multi-line text element's dimension
89    pub fn estimate_dimension(&self) -> FontResult<(i32, i32)> {
90        let (mut mx, mut my) = (0, 0);
91
92        for ((x, y), t) in self.layout_lines((0, 0)).zip(self.lines.iter()) {
93            let (dx, dy) = self.style.font.box_size(t.borrow())?;
94            mx = mx.max(x + dx as i32);
95            my = my.max(y + dy as i32);
96        }
97
98        Ok((mx, my))
99    }
100
101    /// Move the location to the specified location
102    pub fn relocate(&mut self, coord: Coord) {
103        self.coord = coord
104    }
105
106    fn layout_lines(&self, (x0, y0): BackendCoord) -> impl Iterator<Item = BackendCoord> {
107        let font_height = self.style.font.get_size();
108        let actual_line_height = font_height * self.line_height;
109        (0..self.lines.len() as u32).map(move |idx| {
110            let y = f64::from(y0) + f64::from(idx) * actual_line_height;
111            // TODO: Support text alignment as well, currently everything is left aligned
112            let x = f64::from(x0);
113            (x.round() as i32, y.round() as i32)
114        })
115    }
116}
117
118// Rewrite of the layout function for multiline-text. It crashes when UTF-8 is used
119// instead of ASCII. Solution taken from:
120// https://stackoverflow.com/questions/68122526/splitting-a-utf-8-string-into-chunks
121// and modified for our purposes.
122fn layout_multiline_text<'a, F: FnMut(&'a str)>(
123    text: &'a str,
124    max_width: u32,
125    font: FontDesc<'a>,
126    mut func: F,
127) {
128    for line in text.lines() {
129        if max_width == 0 || line.is_empty() {
130            func(line);
131        } else {
132            let mut indices = line.char_indices().map(|(idx, _)| idx).peekable();
133
134            let it = std::iter::from_fn(|| {
135                let start_idx = match indices.next() {
136                    Some(idx) => idx,
137                    None => return None,
138                };
139
140                // iterate over indices
141                for idx in indices.by_ref() {
142                    let substring = &line[start_idx..idx];
143                    let width = font.box_size(substring).unwrap_or((0, 0)).0 as i32;
144                    if width > max_width as i32 {
145                        break;
146                    }
147                }
148
149                let end_idx = match indices.peek() {
150                    Some(idx) => *idx,
151                    None => line.bytes().len(),
152                };
153
154                Some(&line[start_idx..end_idx])
155            });
156
157            for chunk in it {
158                func(chunk);
159            }
160        }
161    }
162}
163
164// Only run the test on Linux because the default font is different
165// on other platforms, causing different multiline splits.
166#[cfg(all(feature = "ttf", target_os = "linux"))]
167#[test]
168fn test_multi_layout() {
169    use plotters_backend::{FontFamily, FontStyle};
170
171    let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);
172
173    layout_multiline_text("öäabcde", 40, font, |txt| {
174        println!("Got: {}", txt);
175        assert!(txt == "öäabc" || txt == "de");
176    });
177
178    let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);
179    layout_multiline_text("öä", 100, font, |txt| {
180        // This does not divide the line, but still crashed in the previous implementation
181        // of layout_multiline_text. So this test should be reliable
182        println!("Got: {}", txt);
183        assert_eq!(txt, "öä")
184    });
185}
186
187impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
188    /// Compute the line layout
189    pub fn compute_line_layout(&self) -> FontResult<Vec<LayoutBox>> {
190        let mut ret = vec![];
191        for ((x, y), t) in self.layout_lines(self.coord).zip(self.lines.iter()) {
192            let (dx, dy) = self.style.font.box_size(t.borrow())?;
193            ret.push(((x, y), (x + dx as i32, y + dy as i32)));
194        }
195        Ok(ret)
196    }
197}
198
199impl<'a, Coord> MultiLineText<'a, Coord, &'a str> {
200    /// Parse a multi-line text into an multi-line element.
201    ///
202    /// `text`: The text that is parsed
203    /// `pos`: The position of the text
204    /// `style`: The style for this text
205    /// `max_width`: The width of the multi-line text element, the line will break
206    /// into two lines if the line is wider than the max_width. If 0 is given, do not
207    /// do any line wrapping
208    pub fn from_str<ST: Into<&'a str>, S: Into<TextStyle<'a>>>(
209        text: ST,
210        pos: Coord,
211        style: S,
212        max_width: u32,
213    ) -> Self {
214        let text = text.into();
215        let mut ret = MultiLineText::new(pos, style);
216
217        layout_multiline_text(text, max_width, ret.style.font.clone(), |l| {
218            ret.push_line(l)
219        });
220        ret
221    }
222}
223
224impl<'a, Coord> MultiLineText<'a, Coord, String> {
225    /// Parse a multi-line text into an multi-line element.
226    ///
227    /// `text`: The text that is parsed
228    /// `pos`: The position of the text
229    /// `style`: The style for this text
230    /// `max_width`: The width of the multi-line text element, the line will break
231    /// into two lines if the line is wider than the max_width. If 0 is given, do not
232    /// do any line wrapping
233    pub fn from_string<S: Into<TextStyle<'a>>>(
234        text: String,
235        pos: Coord,
236        style: S,
237        max_width: u32,
238    ) -> Self {
239        let mut ret = MultiLineText::new(pos, style);
240
241        layout_multiline_text(text.as_str(), max_width, ret.style.font.clone(), |l| {
242            ret.push_line(l.to_string())
243        });
244        ret
245    }
246}
247
248impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord>
249    for &'a MultiLineText<'b, Coord, T>
250{
251    type Point = &'a Coord;
252    type IntoIter = std::iter::Once<&'a Coord>;
253    fn point_iter(self) -> Self::IntoIter {
254        std::iter::once(&self.coord)
255    }
256}
257
258impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB>
259    for MultiLineText<'a, Coord, T>
260{
261    fn draw<I: Iterator<Item = BackendCoord>>(
262        &self,
263        mut points: I,
264        backend: &mut DB,
265        _: (u32, u32),
266    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
267        if let Some(a) = points.next() {
268            for (point, text) in self.layout_lines(a).zip(self.lines.iter()) {
269                backend.draw_text(text.borrow(), &self.style, point)?;
270            }
271        }
272        Ok(())
273    }
274}