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: (i32, i32)) = points.next() {
46 return backend.draw_text(self.text.borrow(), &self.style, pos: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<((i32, i32), (i32, i32))> = vec![];
191 for ((x: i32, y: i32), t: &T) in self.layout_lines(self.coord).zip(self.lines.iter()) {
192 let (dx: u32, dy: u32) = self.style.font.box_size(text: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: &str = text.into();
215 let mut ret: MultiLineText<'_, Coord, …> = MultiLineText::new(pos, style);
216
217 layout_multiline_text(text, max_width, ret.style.font.clone(), |l: &str| {
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<'_, Coord, …> = MultiLineText::new(pos, style);
240
241 layout_multiline_text(text.as_str(), max_width, ret.style.font.clone(), |l: &str| {
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: (i32, i32)) = points.next() {
268 for (point: (i32, i32), text: &T) in self.layout_lines(a).zip(self.lines.iter()) {
269 backend.draw_text(text.borrow(), &self.style, pos:point)?;
270 }
271 }
272 Ok(())
273 }
274}
275