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//! ![embedded-text example](https://raw.githubusercontent.com/embedded-graphics/embedded-text/master/assets/paragraph_spacing.png)
32//!
33//! ![embedded-text example with colored text](https://raw.githubusercontent.com/embedded-graphics/embedded-text/master/assets/plugin-ansi.png)
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}