• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use std::borrow::Borrow;
2 
3 use super::{Drawable, PointCollection};
4 use crate::style::{FontDesc, FontResult, LayoutBox, TextStyle};
5 use 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.
9 pub struct Text<'a, Coord, T: Borrow<str>> {
10     text: T,
11     coord: Coord,
12     style: TextStyle<'a>,
13 }
14 
15 impl<'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
new<S: Into<TextStyle<'a>>>(text: T, points: Coord, style: S) -> Self21     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 
30 impl<'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>;
point_iter(self) -> Self::IntoIter33     fn point_iter(self) -> Self::IntoIter {
34         std::iter::once(&self.coord)
35     }
36 }
37 
38 impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB> for Text<'a, Coord, T> {
draw<I: Iterator<Item = BackendCoord>>( &self, mut points: I, backend: &mut DB, _: (u32, u32), ) -> Result<(), DrawingErrorKind<DB::ErrorType>>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
54 pub struct MultiLineText<'a, Coord, T: Borrow<str>> {
55     lines: Vec<T>,
56     coord: Coord,
57     style: TextStyle<'a>,
58     line_height: f64,
59 }
60 
61 impl<'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
new<S: Into<TextStyle<'a>>>(pos: Coord, style: S) -> Self67     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
set_line_height(&mut self, value: f64) -> &mut Self77     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
push_line<L: Into<T>>(&mut self, line: L)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
estimate_dimension(&self) -> FontResult<(i32, i32)>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
relocate(&mut self, coord: Coord)102     pub fn relocate(&mut self, coord: Coord) {
103         self.coord = coord
104     }
105 
layout_lines(&self, (x0, y0): BackendCoord) -> impl Iterator<Item = BackendCoord>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.
layout_multiline_text<'a, F: FnMut(&'a str)>( text: &'a str, max_width: u32, font: FontDesc<'a>, mut func: F, )122 fn 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]
test_multi_layout()168 fn 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 
187 impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
188     /// Compute the line layout
compute_line_layout(&self) -> FontResult<Vec<LayoutBox>>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 
199 impl<'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
from_str<ST: Into<&'a str>, S: Into<TextStyle<'a>>>( text: ST, pos: Coord, style: S, max_width: u32, ) -> Self208     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 
224 impl<'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
from_string<S: Into<TextStyle<'a>>>( text: String, pos: Coord, style: S, max_width: u32, ) -> Self233     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 
248 impl<'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>;
point_iter(self) -> Self::IntoIter253     fn point_iter(self) -> Self::IntoIter {
254         std::iter::once(&self.coord)
255     }
256 }
257 
258 impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB>
259     for MultiLineText<'a, Coord, T>
260 {
draw<I: Iterator<Item = BackendCoord>>( &self, mut points: I, backend: &mut DB, _: (u32, u32), ) -> Result<(), DrawingErrorKind<DB::ErrorType>>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 }
275