embedded_text/lib.rs
1//! TextBox for embedded-graphics.
2//!
3//! This crate provides a configurable [`TextBox`] to render multiline text inside a bounding
4//! `Rectangle` using [embedded-graphics].
5//!
6//! [`TextBox`] supports the common text alignments:
7//! - [`Horizontal`]:
8//! - `Left`
9//! - `Right`
10//! - `Center`
11//! - `Justified`
12//! - [`Vertical`]:
13//! - `Top`
14//! - `Middle`
15//! - `Bottom`
16//!
17//! [`TextBox`] also supports some special characters not handled by embedded-graphics' `Text`:
18//! - non-breaking space (`\u{200b}`)
19//! - zero-width space (`\u{a0}`)
20//! - soft hyphen (`\u{ad}`)
21//! - carriage return (`\r`)
22//! - tab (`\t`) with configurable tab size
23//!
24//! `TextBox` also supports text coloring using [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code).
25//!
26//! ### Example
27//!
28//! The examples are based on [the embedded-graphics simulator]. The simulator is built on top of
29//! `SDL2`. See the [simulator README] for more information.
30//!
31//! 
32//!
33//! 
34//!
35//! ```rust,no_run
36//! use embedded_graphics::{
37//! mono_font::{ascii::FONT_6X10, MonoTextStyle},
38//! pixelcolor::BinaryColor,
39//! prelude::*,
40//! primitives::Rectangle,
41//! };
42//! use embedded_graphics_simulator::{
43//! BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, Window,
44//! };
45//! use embedded_text::{
46//! alignment::HorizontalAlignment,
47//! style::{HeightMode, TextBoxStyleBuilder},
48//! TextBox,
49//! };
50//!
51//! fn main() {
52//! let text = "Hello, World!\n\
53//! A paragraph is a number of lines that end with a manual newline. Paragraph spacing is the \
54//! number of pixels between two paragraphs.\n\
55//! Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when \
56//! an unknown printer took a galley of type and scrambled it to make a type specimen book.";
57//!
58//! // Specify the styling options:
59//! // * Use the 6x10 MonoFont from embedded-graphics.
60//! // * Draw the text fully justified.
61//! // * Use `FitToText` height mode to stretch the text box to the exact height of the text.
62//! // * Draw the text with `BinaryColor::On`, which will be displayed as light blue.
63//! let character_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
64//! let textbox_style = TextBoxStyleBuilder::new()
65//! .height_mode(HeightMode::FitToText)
66//! .alignment(HorizontalAlignment::Justified)
67//! .paragraph_spacing(6)
68//! .build();
69//!
70//! // Specify the bounding box. Note the 0px height. The `FitToText` height mode will
71//! // measure and adjust the height of the text box in `into_styled()`.
72//! let bounds = Rectangle::new(Point::zero(), Size::new(128, 0));
73//!
74//! // Create the text box and apply styling options.
75//! let text_box = TextBox::with_textbox_style(text, bounds, character_style, textbox_style);
76//!
77//! // Create a simulated display with the dimensions of the text box.
78//! let mut display = SimulatorDisplay::new(text_box.bounding_box().size);
79//!
80//! // Draw the text box.
81//! text_box.draw(&mut display).unwrap();
82//!
83//! // Set up the window and show the display's contents.
84//! let output_settings = OutputSettingsBuilder::new()
85//! .theme(BinaryColorTheme::OledBlue)
86//! .scale(2)
87//! .build();
88//! Window::new("TextBox example with paragraph spacing", &output_settings).show_static(&display);
89//! }
90//! ```
91//!
92//! ## Cargo features
93//!
94//! * `plugin` (*experimental*): allows implementing custom plugins.
95//! * `ansi` (default enabled): enables ANSI sequence support using the `Ansi` plugin.
96//!
97//! [embedded-graphics]: https://github.com/embedded-graphics/embedded-graphics/
98//! [the embedded-graphics simulator]: https://github.com/embedded-graphics/embedded-graphics/tree/master/simulator
99//! [simulator README]: https://github.com/embedded-graphics/embedded-graphics/tree/master/simulator#usage-without-sdl2
100//! [`Horizontal`]: HorizontalAlignment
101//! [`Vertical`]: VerticalAlignment
102
103#![cfg_attr(not(test), no_std)]
104#![deny(clippy::missing_inline_in_public_items)]
105#![deny(clippy::cargo)]
106#![deny(missing_docs)]
107#![warn(clippy::all)]
108#![allow(clippy::needless_doctest_main)]
109
110pub mod alignment;
111mod parser;
112pub mod plugin;
113mod rendering;
114pub mod style;
115mod utils;
116
117use crate::{
118 alignment::{HorizontalAlignment, VerticalAlignment},
119 plugin::{NoPlugin, PluginMarker as Plugin, PluginWrapper},
120 style::{HeightMode, TabSize, TextBoxStyle},
121};
122use embedded_graphics::{
123 geometry::{Dimensions, Point},
124 primitives::Rectangle,
125 text::{
126 renderer::{CharacterStyle, TextRenderer},
127 LineHeight,
128 },
129 transform::Transform,
130};
131use object_chain::{Chain, ChainElement, Link};
132
133#[cfg(feature = "plugin")]
134pub use crate::{
135 parser::{ChangeTextStyle, Token},
136 rendering::{cursor::Cursor, TextBoxProperties},
137};
138
139/// A text box object.
140/// ==================
141///
142/// The `TextBox` object can be used to draw text on a draw target. It is meant to be a more
143/// feature-rich alternative to `Text` in embedded-graphics.
144///
145/// To construct a [`TextBox`] object at least a text string, a bounding box and character style are
146/// required. For advanced formatting options an additional [`TextBoxStyle`] object might be used.
147/// For more information about text box styling, see the documentation of the [`style`] module.
148///
149/// Text rendering in `embedded-graphics` is designed to be extendable by text renderers for
150/// different font formats. `embedded-text` follows this philosophy by using the same text renderer
151/// infrastructure. To use a text renderer in an `embedded-text` project each renderer provides a
152/// character style object. See the [`embedded-graphics` documentation] for more information on text
153/// renderers and character styling.
154///
155/// Plugins
156/// -------
157///
158/// The feature set of `TextBox` can be extended by plugins. Plugins can be used to implement
159/// optional features which are not essential to the core functionality of `embedded-text`.
160///
161/// Use the [`add_plugin`] method to add a plugin to the `TextBox` object. Multiple plugins can be
162/// used at the same time. Plugins are applied in the reverse order they are added. Note that some
163/// plugins may interfere with others if used together or not in the expected order.
164///
165/// If you need to extract data from plugins after the text box has been rendered,
166/// you can use the [`take_plugins`] method.
167///
168/// See the list of built-in plugins in the [`plugin`] module.
169///
170/// *Note:* Implementing custom plugins is experimental and require enabling the `plugin` feature.
171///
172/// ### Example: advanced text styling using the ANSI plugin
173///
174/// ```rust,ignore
175/// # use embedded_graphics::{
176/// # Drawable,
177/// # geometry::{Point, Size},
178/// # primitives::Rectangle,
179/// # mock_display::MockDisplay,
180/// # mono_font::{
181/// # ascii::FONT_6X10, MonoTextStyle, MonoTextStyleBuilder,
182/// # },
183/// # pixelcolor::BinaryColor,
184/// # };
185/// # let mut display: MockDisplay<BinaryColor> = MockDisplay::default();
186/// # display.set_allow_out_of_bounds_drawing(true);
187/// # let character_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
188/// # let bounding_box = Rectangle::new(Point::zero(), Size::new(100, 20));
189/// use embedded_text::{TextBox, plugin::ansi::Ansi};
190/// TextBox::new(
191/// "Some \x1b[4munderlined\x1b[24m text",
192/// bounding_box,
193/// character_style,
194/// )
195/// .add_plugin(Ansi::new())
196/// .draw(&mut display)?;
197/// # Ok::<(), core::convert::Infallible>(())
198/// ```
199///
200/// Vertical offsetting
201/// -------------------
202///
203/// You can use the [`set_vertical_offset`] method to move the text inside the text box. Vertical
204/// offset is applied after all vertical measurements and alignments. This can be useful to scroll
205/// text in a fixed text box. Setting a positive value moves the text down.
206///
207/// Residual text
208/// -------------
209///
210/// If the text does not fit the given bounding box, the [`draw`] method returns the part which was
211/// not processed. The return value can be used to flow text into multiple text boxes.
212///
213/// [`draw`]: embedded_graphics::Drawable::draw()
214/// [`set_vertical_offset`]: TextBox::set_vertical_offset()
215/// [`add_plugin`]: TextBox::add_plugin()
216/// [`take_plugins`]: TextBox::take_plugins()
217/// [`embedded-graphics` documentation]: https://docs.rs/embedded-graphics/0.7.1/embedded_graphics/text/index.html
218#[derive(Clone, Debug, Hash)]
219#[must_use]
220pub struct TextBox<'a, S, M = NoPlugin<<S as TextRenderer>::Color>>
221where
222 S: TextRenderer,
223{
224 /// The text to be displayed in this `TextBox`
225 pub text: &'a str,
226
227 /// The bounding box of this `TextBox`
228 pub bounds: Rectangle,
229
230 /// The character style of the [`TextBox`].
231 pub character_style: S,
232
233 /// The style of the [`TextBox`].
234 pub style: TextBoxStyle,
235
236 /// Vertical offset applied to the text just before rendering.
237 pub vertical_offset: i32,
238
239 plugin: PluginWrapper<'a, M, S::Color>,
240}
241
242impl<'a, S> TextBox<'a, S, NoPlugin<<S as TextRenderer>::Color>>
243where
244 S: TextRenderer + CharacterStyle,
245{
246 /// Creates a new `TextBox` instance with a given bounding `Rectangle`.
247 #[inline]
248 pub fn new(text: &'a str, bounds: Rectangle, character_style: S) -> Self {
249 TextBox::with_textbox_style(text, bounds, character_style, TextBoxStyle::default())
250 }
251
252 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a given
253 /// `TextBoxStyle`.
254 #[inline]
255 pub fn with_textbox_style(
256 text: &'a str,
257 bounds: Rectangle,
258 character_style: S,
259 textbox_style: TextBoxStyle,
260 ) -> Self {
261 let mut styled = TextBox {
262 text,
263 bounds,
264 character_style,
265 style: textbox_style,
266 vertical_offset: 0,
267 plugin: PluginWrapper::new(NoPlugin::new()),
268 };
269
270 styled.style.height_mode.apply(&mut styled);
271
272 styled
273 }
274
275 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
276 /// `TextBoxStyle` with the given horizontal alignment.
277 #[inline]
278 pub fn with_alignment(
279 text: &'a str,
280 bounds: Rectangle,
281 character_style: S,
282 alignment: HorizontalAlignment,
283 ) -> Self {
284 TextBox::with_textbox_style(
285 text,
286 bounds,
287 character_style,
288 TextBoxStyle::with_alignment(alignment),
289 )
290 }
291
292 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
293 /// `TextBoxStyle` and the given vertical alignment.
294 #[inline]
295 pub fn with_vertical_alignment(
296 text: &'a str,
297 bounds: Rectangle,
298 character_style: S,
299 vertical_alignment: VerticalAlignment,
300 ) -> Self {
301 TextBox::with_textbox_style(
302 text,
303 bounds,
304 character_style,
305 TextBoxStyle::with_vertical_alignment(vertical_alignment),
306 )
307 }
308
309 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
310 /// `TextBoxStyle` and the given [height mode].
311 ///
312 /// [height mode]: HeightMode
313 #[inline]
314 pub fn with_height_mode(
315 text: &'a str,
316 bounds: Rectangle,
317 character_style: S,
318 mode: HeightMode,
319 ) -> Self {
320 TextBox::with_textbox_style(
321 text,
322 bounds,
323 character_style,
324 TextBoxStyle::with_height_mode(mode),
325 )
326 }
327
328 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
329 /// `TextBoxStyle` and the given line height.
330 #[inline]
331 pub fn with_line_height(
332 text: &'a str,
333 bounds: Rectangle,
334 character_style: S,
335 line_height: LineHeight,
336 ) -> Self {
337 TextBox::with_textbox_style(
338 text,
339 bounds,
340 character_style,
341 TextBoxStyle::with_line_height(line_height),
342 )
343 }
344
345 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
346 /// `TextBoxStyle` and the given paragraph spacing.
347 #[inline]
348 pub fn with_paragraph_spacing(
349 text: &'a str,
350 bounds: Rectangle,
351 character_style: S,
352 spacing: u32,
353 ) -> Self {
354 TextBox::with_textbox_style(
355 text,
356 bounds,
357 character_style,
358 TextBoxStyle::with_paragraph_spacing(spacing),
359 )
360 }
361
362 /// Creates a new `TextBox` instance with a given bounding `Rectangle` and a default
363 /// `TextBoxStyle` and the given tab size.
364 #[inline]
365 pub fn with_tab_size(
366 text: &'a str,
367 bounds: Rectangle,
368 character_style: S,
369 tab_size: TabSize,
370 ) -> Self {
371 TextBox::with_textbox_style(
372 text,
373 bounds,
374 character_style,
375 TextBoxStyle::with_tab_size(tab_size),
376 )
377 }
378
379 /// Sets the vertical text offset.
380 ///
381 /// Vertical offset changes the vertical position of the displayed text within the bounding box.
382 /// Setting a positive value moves the text down.
383 #[inline]
384 pub fn set_vertical_offset(&mut self, offset: i32) -> &mut Self {
385 self.vertical_offset = offset;
386 self
387 }
388
389 /// Adds a new plugin to the `TextBox`.
390 #[inline]
391 pub fn add_plugin<M>(self, plugin: M) -> TextBox<'a, S, Chain<M>>
392 where
393 M: Plugin<'a, <S as TextRenderer>::Color>,
394 {
395 let mut styled = TextBox {
396 text: self.text,
397 bounds: self.bounds,
398 character_style: self.character_style,
399 style: self.style,
400 vertical_offset: self.vertical_offset,
401 plugin: PluginWrapper::new(Chain::new(plugin)),
402 };
403 styled.style.height_mode.apply(&mut styled);
404 styled
405 }
406}
407
408impl<'a, S, P> TextBox<'a, S, P>
409where
410 S: TextRenderer + CharacterStyle,
411 P: Plugin<'a, <S as TextRenderer>::Color> + ChainElement,
412{
413 /// Adds a new plugin to the `TextBox`.
414 #[inline]
415 pub fn add_plugin<M>(self, plugin: M) -> TextBox<'a, S, Link<M, P>>
416 where
417 M: Plugin<'a, <S as TextRenderer>::Color>,
418 {
419 let parent = self.plugin.into_inner();
420
421 let mut styled = TextBox {
422 text: self.text,
423 bounds: self.bounds,
424 character_style: self.character_style,
425 style: self.style,
426 vertical_offset: self.vertical_offset,
427 plugin: PluginWrapper::new(parent.append(plugin)),
428 };
429 styled.style.height_mode.apply(&mut styled);
430 styled
431 }
432
433 /// Deconstruct the text box and return the plugins.
434 #[inline]
435 pub fn take_plugins(self) -> P {
436 self.plugin.into_inner()
437 }
438}
439
440impl<'a, S, M> Transform for TextBox<'a, S, M>
441where
442 S: TextRenderer + Clone,
443 M: Plugin<'a, S::Color>,
444{
445 #[inline]
446 fn translate(&self, by: Point) -> Self {
447 Self {
448 bounds: self.bounds.translate(by),
449 ..self.clone()
450 }
451 }
452
453 #[inline]
454 fn translate_mut(&mut self, by: Point) -> &mut Self {
455 self.bounds.translate_mut(by);
456
457 self
458 }
459}
460
461impl<'a, S, M> Dimensions for TextBox<'a, S, M>
462where
463 S: TextRenderer,
464 M: Plugin<'a, S::Color>,
465{
466 #[inline]
467 fn bounding_box(&self) -> Rectangle {
468 self.bounds
469 }
470}
471
472impl<'a, S, M> TextBox<'a, S, M>
473where
474 S: TextRenderer,
475 M: Plugin<'a, S::Color>,
476{
477 /// Sets the height of the [`TextBox`] to the height of the text.
478 #[inline]
479 fn fit_height(&mut self) -> &mut Self {
480 self.fit_height_limited(u32::MAX)
481 }
482
483 /// Sets the height of the [`TextBox`] to the height of the text, limited to `max_height`.
484 ///
485 /// This method allows you to set a maximum height. The [`TextBox`] will take up at most
486 /// `max_height` pixel vertical space.
487 #[inline]
488 fn fit_height_limited(&mut self, max_height: u32) -> &mut Self {
489 // Measure text given the width of the textbox
490 let text_height = self
491 .style
492 .measure_text_height_impl(
493 self.plugin.clone(),
494 &self.character_style,
495 self.text,
496 self.bounding_box().size.width,
497 )
498 .min(max_height)
499 .min(i32::MAX as u32);
500
501 // Apply height
502 self.bounds.size.height = text_height;
503
504 self
505 }
506}