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 |
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 | |
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>; |
33 | 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> { |
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 |
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 |
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. |
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 ] |
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 |
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 | |
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 |
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 | |
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 |
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 | |
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>; |
253 | 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 | { |
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 | |