plotters/element/
text.rs

1use std::borrow::Borrow;
2use std::i32;
3
4use super::{Drawable, PointCollection};
5use crate::style::{FontDesc, FontResult, LayoutBox, TextStyle};
6use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
7
8/// A single line text element. This can be owned or borrowed string, dependents on
9/// `String` or `str` moved into.
10pub struct Text<'a, Coord, T: Borrow<str>> {
11    text: T,
12    coord: Coord,
13    style: TextStyle<'a>,
14}
15
16impl<'a, Coord, T: Borrow<str>> Text<'a, Coord, T> {
17    /// Create a new text element
18    /// - `text`: The text for the element
19    /// - `points`: The upper left conner for the text element
20    /// - `style`: The text style
21    /// - Return the newly created text element
22    pub fn new<S: Into<TextStyle<'a>>>(text: T, points: Coord, style: S) -> Self {
23        Self {
24            text,
25            coord: points,
26            style: style.into(),
27        }
28    }
29}
30
31impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord> for &'a Text<'b, Coord, T> {
32    type Point = &'a Coord;
33    type IntoIter = std::iter::Once<&'a Coord>;
34    fn point_iter(self) -> Self::IntoIter {
35        std::iter::once(&self.coord)
36    }
37}
38
39impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB> for Text<'a, Coord, T> {
40    fn draw<I: Iterator<Item = BackendCoord>>(
41        &self,
42        mut points: I,
43        backend: &mut DB,
44        _: (u32, u32),
45    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
46        if let Some(a) = points.next() {
47            return backend.draw_text(self.text.borrow(), &self.style, a);
48        }
49        Ok(())
50    }
51}
52
53/// An multi-line text element. The `Text` element allows only single line text
54/// and the `MultiLineText` supports drawing multiple lines
55pub struct MultiLineText<'a, Coord, T: Borrow<str>> {
56    lines: Vec<T>,
57    coord: Coord,
58    style: TextStyle<'a>,
59    line_height: f64,
60}
61
62impl<'a, Coord, T: Borrow<str>> MultiLineText<'a, Coord, T> {
63    /// Create an empty multi-line text element.
64    /// Lines can be append to the empty multi-line by calling `push_line` method
65    ///
66    /// `pos`: The upper left corner
67    /// `style`: The style of the text
68    pub fn new<S: Into<TextStyle<'a>>>(pos: Coord, style: S) -> Self {
69        MultiLineText {
70            lines: vec![],
71            coord: pos,
72            style: style.into(),
73            line_height: 1.25,
74        }
75    }
76
77    /// Set the line height of the multi-line text element
78    pub fn set_line_height(&mut self, value: f64) -> &mut Self {
79        self.line_height = value;
80        self
81    }
82
83    /// Push a new line into the given multi-line text
84    /// `line`: The line to be pushed
85    pub fn push_line<L: Into<T>>(&mut self, line: L) {
86        self.lines.push(line.into());
87    }
88
89    /// Estimate the multi-line text element's dimension
90    pub fn estimate_dimension(&self) -> FontResult<(i32, i32)> {
91        let (mut mx, mut my) = (0, 0);
92
93        for ((x, y), t) in self.layout_lines((0, 0)).zip(self.lines.iter()) {
94            let (dx, dy) = self.style.font.box_size(t.borrow())?;
95            mx = mx.max(x + dx as i32);
96            my = my.max(y + dy as i32);
97        }
98
99        Ok((mx, my))
100    }
101
102    /// Move the location to the specified location
103    pub fn relocate(&mut self, coord: Coord) {
104        self.coord = coord
105    }
106
107    fn layout_lines(&self, (x0, y0): BackendCoord) -> impl Iterator<Item = BackendCoord> {
108        let font_height = self.style.font.get_size();
109        let actual_line_height = font_height * self.line_height;
110        (0..self.lines.len() as u32).map(move |idx| {
111            let y = f64::from(y0) + f64::from(idx) * actual_line_height;
112            // TODO: Support text alignment as well, currently everything is left aligned
113            let x = f64::from(x0);
114            (x.round() as i32, y.round() as i32)
115        })
116    }
117}
118
119fn layout_multiline_text<'a, F: FnMut(&'a str)>(
120    text: &'a str,
121    max_width: u32,
122    font: FontDesc<'a>,
123    mut func: F,
124) {
125    for line in text.lines() {
126        if max_width == 0 || line.is_empty() {
127            func(line);
128        } else {
129            let mut remaining = &line[0..];
130
131            while !remaining.is_empty() {
132                let mut left = 0;
133                while left < remaining.len() {
134                    let width = font.box_size(&remaining[0..=left]).unwrap_or((0, 0)).0 as i32;
135
136                    if width > max_width as i32 {
137                        break;
138                    }
139                    left += 1;
140                }
141
142                if left == 0 {
143                    left += 1;
144                }
145
146                let cur_line = &remaining[..left];
147                remaining = &remaining[left..];
148
149                func(cur_line);
150            }
151        }
152    }
153}
154
155impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
156    /// Compute the line layout
157    pub fn compute_line_layout(&self) -> FontResult<Vec<LayoutBox>> {
158        let mut ret = vec![];
159        for ((x, y), t) in self.layout_lines(self.coord).zip(self.lines.iter()) {
160            let (dx, dy) = self.style.font.box_size(t.borrow())?;
161            ret.push(((x, y), (x + dx as i32, y + dy as i32)));
162        }
163        Ok(ret)
164    }
165}
166
167impl<'a, Coord> MultiLineText<'a, Coord, &'a str> {
168    /// Parse a multi-line text into an multi-line element.
169    ///
170    /// `text`: The text that is parsed
171    /// `pos`: The position of the text
172    /// `style`: The style for this text
173    /// `max_width`: The width of the multi-line text element, the line will break
174    /// into two lines if the line is wider than the max_width. If 0 is given, do not
175    /// do any line wrapping
176    pub fn from_str<ST: Into<&'a str>, S: Into<TextStyle<'a>>>(
177        text: ST,
178        pos: Coord,
179        style: S,
180        max_width: u32,
181    ) -> Self {
182        let text = text.into();
183        let mut ret = MultiLineText::new(pos, style);
184
185        layout_multiline_text(text, max_width, ret.style.font.clone(), |l| {
186            ret.push_line(l)
187        });
188        ret
189    }
190}
191
192impl<'a, Coord> MultiLineText<'a, Coord, String> {
193    /// Parse a multi-line text into an multi-line element.
194    ///
195    /// `text`: The text that is parsed
196    /// `pos`: The position of the text
197    /// `style`: The style for this text
198    /// `max_width`: The width of the multi-line text element, the line will break
199    /// into two lines if the line is wider than the max_width. If 0 is given, do not
200    /// do any line wrapping
201    pub fn from_string<S: Into<TextStyle<'a>>>(
202        text: String,
203        pos: Coord,
204        style: S,
205        max_width: u32,
206    ) -> Self {
207        let mut ret = MultiLineText::new(pos, style);
208
209        layout_multiline_text(text.as_str(), max_width, ret.style.font.clone(), |l| {
210            ret.push_line(l.to_string())
211        });
212        ret
213    }
214}
215
216impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord>
217    for &'a MultiLineText<'b, Coord, T>
218{
219    type Point = &'a Coord;
220    type IntoIter = std::iter::Once<&'a Coord>;
221    fn point_iter(self) -> Self::IntoIter {
222        std::iter::once(&self.coord)
223    }
224}
225
226impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB>
227    for MultiLineText<'a, Coord, T>
228{
229    fn draw<I: Iterator<Item = BackendCoord>>(
230        &self,
231        mut points: I,
232        backend: &mut DB,
233        _: (u32, u32),
234    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
235        if let Some(a) = points.next() {
236            for (point, text) in self.layout_lines(a).zip(self.lines.iter()) {
237                backend.draw_text(text.borrow(), &self.style, point)?;
238            }
239        }
240        Ok(())
241    }
242}