piet/
text.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Traits for fonts and text handling.
5
6use std::ops::{Range, RangeBounds};
7
8use crate::kurbo::{Point, Rect, Size};
9use crate::{Color, Error, FontFamily, FontStyle, FontWeight};
10
11/// The Piet text API.
12///
13/// This trait is the interface for text-related functionality, such as font
14/// management and text layout.
15pub trait Text: Clone {
16    /// A concrete type that implements the [`TextLayoutBuilder`] trait.
17    type TextLayoutBuilder: TextLayoutBuilder<Out = Self::TextLayout>;
18
19    /// A concrete type that implements the [`TextLayout`] trait.
20    type TextLayout: TextLayout;
21
22    /// Query the platform for a font with a given name, and return a [`FontFamily`]
23    /// object corresponding to that font, if it is found.
24    ///
25    /// # Examples
26    ///
27    /// Trying a preferred font, falling back if it isn't found.
28    ///
29    /// ```
30    /// # use piet::*;
31    /// # let mut ctx = NullRenderContext::new();
32    /// # let text = ctx.text();
33    /// let text_font = text.font_family("Charter")
34    ///     .or_else(|| text.font_family("Garamond"))
35    ///     .unwrap_or(FontFamily::SERIF);
36    /// ```
37    fn font_family(&mut self, family_name: &str) -> Option<FontFamily>;
38
39    /// Load the provided font data and make it available for use.
40    ///
41    /// This method takes font data (such as the contents of a file on disk) and
42    /// attempts to load it, making it subsequently available for use.
43    ///
44    /// If loading is successful, this method will return a [`FontFamily`] handle
45    /// that can be used to select this font when constructing a [`TextLayout`].
46    ///
47    /// # Notes
48    ///
49    /// ## font families and styles:
50    ///
51    /// If you wish to use multiple fonts in a given family, you will need to
52    /// load them individually. This method will return the same handle for
53    /// each font in the same family; the handle **does not refer to a specific
54    /// font**. This means that if you load bold and regular fonts from the
55    /// same family, to *use* the bold version you must, when constructing your
56    /// [`TextLayout`], pass the family as well as the correct weight.
57    ///
58    /// *If you wish to use custom fonts, load each concrete instance of the
59    /// font-family that you wish to use; that is, if you are using regular,
60    /// bold, italic, and bold-italic, you should be loading four distinct fonts.*
61    ///
62    /// ## family name masking
63    ///
64    /// If you load a custom font, the family name of your custom font will take
65    /// precedence over system families of the same name; so your 'Helvetica' will
66    /// potentially interfere with the use of the platform 'Helvetica'.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// # use piet::*;
72    /// # let mut ctx = NullRenderContext::new();
73    /// # let text = ctx.text();
74    /// # fn get_font_data(name: &str) -> Vec<u8> { Vec::new() }
75    /// let helvetica_regular = get_font_data("Helvetica-Regular");
76    /// let helvetica_bold = get_font_data("Helvetica-Bold");
77    ///
78    /// let regular = text.load_font(&helvetica_regular).unwrap();
79    /// let bold = text.load_font(&helvetica_bold).unwrap();
80    /// assert_eq!(regular, bold);
81    ///
82    /// let layout = text.new_text_layout("Custom Fonts")
83    ///     .font(regular, 12.0)
84    ///     .range_attribute(6.., FontWeight::BOLD);
85    ///
86    /// ```
87    fn load_font(&mut self, data: &[u8]) -> Result<FontFamily, Error>;
88
89    /// Create a new layout object to display the provided `text`.
90    ///
91    /// The returned object is a [`TextLayoutBuilder`]; methods on that type
92    /// can be used to customize the layout.
93    ///
94    /// Internally, the `text` argument will be stored in an `Rc` or `Arc`, so that
95    /// the layout can be cheaply cloned. To avoid duplicating the storage of text
96    /// (which is likely also owned elsewhere in your application) you can pass
97    /// a type such as `Rc<str>` or `Rc<String>`; alternatively you can just use
98    /// `String` or `&static str`.
99    fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder;
100}
101
102/// A type that stores text.
103///
104/// This allows the client to more completely control how text is stored.
105/// If you do not care about this, implementations are provided for `String`,
106/// `Arc<str>`, and `Rc<str>`.
107///
108/// This has a `'static` bound because the inner type will be behind a shared
109/// pointer.
110///
111/// # Implementors
112///
113/// This trait expects immutable data. Mutating the data (using interior mutability)
114/// May cause any [`TextLayout`] objects using this [`TextStorage`] to become
115/// inconsistent.
116pub trait TextStorage: 'static {
117    /// Return the underlying text as a contiguous buffer.
118    ///
119    /// Types that do not store their text as a contiguous buffer (such as ropes
120    /// or gap buffers) will need to use a wrapper to maintain a separate
121    /// contiguous buffer as required.
122    ///
123    /// In practice, these types should be using a [`TextLayout`] object
124    /// per paragraph, and in general a separate buffer will be unnecessary.
125    fn as_str(&self) -> &str;
126}
127
128impl std::ops::Deref for dyn TextStorage {
129    type Target = str;
130    fn deref(&self) -> &Self::Target {
131        self.as_str()
132    }
133}
134
135#[derive(Debug, Clone)]
136/// Attributes that can be applied to text.
137pub enum TextAttribute {
138    /// The font family.
139    FontFamily(FontFamily),
140    /// The font size, in points.
141    FontSize(f64),
142    /// The [`FontWeight`].
143    Weight(FontWeight),
144    /// The foreground color of the text.
145    TextColor(crate::Color),
146    /// The [`FontStyle`]; either regular or italic.
147    Style(FontStyle),
148    /// Underline.
149    Underline(bool),
150    /// Strikethrough.
151    Strikethrough(bool),
152}
153
154/// A trait for laying out text.
155pub trait TextLayoutBuilder: Sized {
156    /// The type of the generated [`TextLayout`].
157    type Out: TextLayout;
158
159    /// Set a max width for this layout.
160    ///
161    /// You may pass an `f64` to this method to indicate a width (in display points)
162    /// that will be used for word-wrapping.
163    ///
164    /// If you pass `f64::INFINITY`, words will not be wrapped; this is the
165    /// default behaviour.
166    fn max_width(self, width: f64) -> Self;
167
168    /// Set the [`TextAlignment`] to be used for this layout.
169    fn alignment(self, alignment: TextAlignment) -> Self;
170
171    /// A convenience method for setting the default font family and size.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// # use piet::*;
177    /// # let mut ctx = NullRenderContext::new();
178    /// # let mut text = ctx.text();
179    ///
180    /// let times = text.font_family("Times New Roman").unwrap();
181    ///
182    /// // the following are equivalent
183    /// let layout_one = text.new_text_layout("hello everyone!")
184    ///     .font(times.clone(), 12.0)
185    ///     .build();
186    ///
187    /// let layout_two = text.new_text_layout("hello everyone!")
188    ///     .default_attribute(TextAttribute::FontFamily(times.clone()))
189    ///     .default_attribute(TextAttribute::FontSize(12.0))
190    ///     .build();
191    /// ```
192    fn font(self, font: FontFamily, font_size: f64) -> Self {
193        self.default_attribute(TextAttribute::FontFamily(font))
194            .default_attribute(TextAttribute::FontSize(font_size))
195    }
196
197    /// A convenience method for setting the default text color.
198    ///
199    /// This is equivalent to passing `TextAttribute::TextColor` to the
200    /// `default_attribute` method.
201    fn text_color(self, color: Color) -> Self {
202        self.default_attribute(TextAttribute::TextColor(color))
203    }
204
205    /// Add a default [`TextAttribute`] for this layout.
206    ///
207    /// Default attributes will be used for regions of the layout that do not
208    /// have explicit attributes added via [`range_attribute`].
209    ///
210    /// You must set default attributes before setting range attributes,
211    /// or the implementation is free to ignore them.
212    ///
213    /// [`range_attribute`]: TextLayoutBuilder::range_attribute
214    fn default_attribute(self, attribute: impl Into<TextAttribute>) -> Self;
215
216    /// Add a [`TextAttribute`] to a range of this layout.
217    ///
218    /// The `range` argument is can be any of the range forms accepted by
219    /// slice indexing, such as `..`, `..n`, `n..`, `n..m`, etcetera.
220    ///
221    /// The `attribute` argument is a [`TextAttribute`] or any type that can be
222    /// converted to such an attribute; for instance you may pass a [`FontWeight`]
223    /// directly.
224    ///
225    /// ## Notes
226    ///
227    /// This is a low-level API; what this means in particular is that it is designed
228    /// to be efficiently implemented, not necessarily ergonomic to use, and there
229    /// may be a few gotchas.
230    ///
231    /// **ranges of added attributes should be added in non-decreasing start order**.
232    /// This is to say that attributes should be added in the order of the start
233    /// of their ranges. Attributes added out of order may be skipped.
234    ///
235    /// **attributes do not stack**. Setting the range `0..100` to `FontWeight::BOLD`
236    /// and then setting the range `20..50` to `FontWeight::THIN` will result in
237    /// the range `50..100` being reset to the default font weight; we will not
238    /// remember that you had earlier set it to `BOLD`.
239    ///
240    /// ## Examples
241    ///
242    /// ```
243    /// # use piet::*;
244    /// # let mut ctx = NullRenderContext::new();
245    /// # let mut text = ctx.text();
246    ///
247    /// let times = text.font_family("Times New Roman").unwrap();
248    /// let layout = text.new_text_layout("This API is okay, I guess?")
249    ///     .font(FontFamily::MONOSPACE, 12.0)
250    ///     .default_attribute(FontStyle::Italic)
251    ///     .range_attribute(..5, FontWeight::BOLD)
252    ///     .range_attribute(5..14, times)
253    ///     .range_attribute(20.., TextAttribute::TextColor(Color::rgb(1.0, 0., 0.,)))
254    ///     .build();
255    /// ```
256    fn range_attribute(
257        self,
258        range: impl RangeBounds<usize>,
259        attribute: impl Into<TextAttribute>,
260    ) -> Self;
261
262    /// Attempt to build the [`TextLayout`].
263    ///
264    /// This should only fail in exceptional circumstances.
265    fn build(self) -> Result<Self::Out, Error>;
266}
267
268/// The alignment of text in a [`TextLayout`].
269#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
270pub enum TextAlignment {
271    /// Text is aligned to the left edge in left-to-right scripts, and the
272    /// right edge in right-to-left scripts.
273    #[default]
274    Start,
275    /// Text is aligned to the right edge in left-to-right scripts, and the
276    /// left edge in right-to-left scripts.
277    End,
278    /// Lines are centered in the available space.
279    Center,
280    /// Line width is increased to fill available space.
281    ///
282    /// This may be achieved through increases in word or character spacing,
283    /// or through ligatures where available.
284    Justified,
285}
286
287/// A drawable text object.
288///
289/// ## Line Breaks
290///
291/// A text layout may be broken into multiple lines in order to fit within a given width.
292/// Line breaking is generally done between words (whitespace-separated).
293///
294/// A line's text and [`LineMetric`][]s can be accessed by 0-indexed line number.
295///
296/// ## Text Position
297///
298/// A text position is the offset in the underlying string, defined in utf-8
299/// code units, as is standard for Rust strings.
300///
301/// However, text position is also related to valid cursor positions. Therefore:
302/// - The beginning of a line has text position `0`.
303/// - The end of a line is a valid text position. e.g. `text.len()` is a valid text position.
304/// - If the text position is not at a code point or grapheme boundary, undesirable behavior may
305///   occur.
306pub trait TextLayout: Clone {
307    /// The total size of this `TextLayout`.
308    ///
309    /// This is the size required to draw this `TextLayout`, as provided by the
310    /// platform text system.
311    ///
312    /// If the layout is empty (the text is the empty string) the returned
313    /// `Size` will have the height required to draw a cursor in the layout's
314    /// default font.
315    ///
316    /// # Note
317    ///
318    /// This is not currently defined very rigorously; in particular we do not
319    /// specify whether this should include half-leading or paragraph spacing
320    /// above or below the text.
321    ///
322    /// We would ultimately like to review and attempt to standardize this
323    /// behaviour, but it is out of scope for the time being.
324    fn size(&self) -> Size;
325
326    /// The width of this layout, including the width of any trailing whitespace.
327    ///
328    /// In many situations you do not want to include the width of trailing
329    /// whitespace when measuring width; for instance when word-wrap is enabled,
330    /// trailing whitespace is ignored. In other circumstances, however, this
331    /// width is important, such as when editing a single line of text; in these
332    /// cases you want to use this method to ensure you account for the actual
333    /// size of any trailing whitespace.
334    fn trailing_whitespace_width(&self) -> f64;
335
336    /// Returns a `Rect` representing the bounding box of the glyphs in this layout,
337    /// relative to the top-left of the layout object.
338    ///
339    /// This is sometimes called the bounding box or the inking rect, and is
340    /// used to determine when the layout has become visible (for instance,
341    /// during scrolling) and thus needs to be drawn.
342    fn image_bounds(&self) -> Rect;
343
344    /// The text used to create this layout.
345    fn text(&self) -> &str;
346
347    /// Given a line number, return a reference to that line's underlying string.
348    ///
349    /// This will include any trailing whitespace.
350    fn line_text(&self, line_number: usize) -> Option<&str>;
351
352    /// Given a line number, return a reference to that line's metrics, if the line exists.
353    ///
354    /// If this layout's text is the empty string, calling this method with `0`
355    /// returns some [`LineMetric`]; this will use the layout's default font to
356    /// determine what the expected height of the first line would be, which is
357    /// necessary for things like cursor drawing.
358    fn line_metric(&self, line_number: usize) -> Option<LineMetric>;
359
360    /// Returns total number of lines in the text layout.
361    ///
362    /// The return value will always be greater than 0; a layout of the empty
363    /// string is considered to have a single line.
364    fn line_count(&self) -> usize;
365
366    /// Given a `Point`, return a [`HitTestPoint`] describing the corresponding
367    /// text position.
368    ///
369    /// This is used for things like mapping a mouse click to a cursor position.
370    ///
371    /// The point should be in the coordinate space of the layout object.
372    ///
373    /// ## Notes:
374    ///
375    /// This will always return *some* text position. If the point is outside of
376    /// the bounds of the layout, it will return the nearest text position.
377    ///
378    /// For more on text positions, see docs for the [`TextLayout`] trait.
379    fn hit_test_point(&self, point: Point) -> HitTestPoint;
380
381    /// Given a grapheme boundary in the string used to create this [`TextLayout`],
382    /// return a [`HitTestPosition`] object describing the location of that boundary
383    /// within the layout.
384    ///
385    /// For more on text positions, see docs for the [`TextLayout`] trait.
386    ///
387    /// ## Notes:
388    ///
389    /// The user is expected to ensure that the provided index is a grapheme
390    /// boundary. If it is a character boundary but *not* a grapheme boundary,
391    /// the return value may be backend-specific.
392    ///
393    /// ## Panics:
394    ///
395    /// This method will panic if the text position is not a character boundary,
396    fn hit_test_text_position(&self, idx: usize) -> HitTestPosition;
397
398    /// Returns a vector of `Rect`s that cover the region of the text indicated
399    /// by `range`.
400    ///
401    /// The returned rectangles are suitable for things like drawing selection
402    /// regions or highlights.
403    ///
404    /// `range` will be clamped to the length of the text if necessary.
405    ///
406    /// Note: this implementation is not currently BiDi aware; it will be updated
407    /// when BiDi support is added.
408    fn rects_for_range(&self, range: impl RangeBounds<usize>) -> Vec<Rect> {
409        let text_len = self.text().len();
410        let mut range = crate::util::resolve_range(range, text_len);
411        range.start = range.start.min(text_len);
412        range.end = range.end.min(text_len);
413
414        if range.start >= range.end {
415            return Vec::new();
416        }
417
418        let first_line = self.hit_test_text_position(range.start).line;
419        let last_line = self.hit_test_text_position(range.end).line;
420
421        let mut result = Vec::new();
422
423        for line in first_line..=last_line {
424            let metrics = self.line_metric(line).unwrap();
425            let y0 = metrics.y_offset;
426            let y1 = y0 + metrics.height;
427            let line_range_start = if line == first_line {
428                range.start
429            } else {
430                metrics.start_offset
431            };
432
433            let line_range_end = if line == last_line {
434                range.end
435            } else {
436                metrics.end_offset - metrics.trailing_whitespace
437            };
438
439            let start_x = self.hit_test_text_position(line_range_start).point.x;
440            //HACK: because we don't have affinity, if the line has an emergency
441            //break we need to manually use the layout width as the end point
442            //for the selection rect. See https://github.com/linebender/piet/issues/323
443            let end_x = if line != last_line && metrics.trailing_whitespace == 0 {
444                self.size().width
445            } else {
446                self.hit_test_text_position(line_range_end).point.x
447            };
448            result.push(Rect::new(start_x, y0, end_x, y1));
449        }
450        result
451    }
452}
453
454/// Metadata about each line in a text layout.
455#[derive(Clone, Debug, Default, PartialEq)]
456pub struct LineMetric {
457    /// The start index of this line in the underlying `String` used to create the
458    /// [`TextLayout`] to which this line belongs.
459    pub start_offset: usize,
460
461    /// The end index of this line in the underlying `String` used to create the
462    /// [`TextLayout`] to which this line belongs.
463    ///
464    /// This is the end of an exclusive range; this index is not part of the line.
465    ///
466    /// Includes trailing whitespace.
467    pub end_offset: usize,
468
469    /// The length of the trailing whitespace at the end of this line, in utf-8
470    /// code units.
471    ///
472    /// When lines are broken on whitespace (as is common), the whitespace
473    /// is assigned to the end of the preceding line. Reporting the size of
474    /// the trailing whitespace section lets an API consumer measure and render
475    /// only the trimmed line up to the whitespace.
476    pub trailing_whitespace: usize,
477
478    /// The distance from the top of the line (`y_offset`) to the baseline.
479    pub baseline: f64,
480
481    /// The height of the line.
482    ///
483    /// This value is intended to be used to determine the height of features
484    /// such as cursors and selection regions. Although it is generally the case
485    /// that `y_offset + height` for line `n` is equal to the `y_offset` of
486    /// line `n + 1`, this is not strictly enforced, and should not be counted on.
487    pub height: f64,
488
489    /// The y position of the top of this line, relative to the top of the layout.
490    ///
491    /// It should be possible to use this position, in conjunction with `height`,
492    /// to determine the region that would be used for things like text selection.
493    pub y_offset: f64,
494}
495
496impl LineMetric {
497    /// The utf-8 range in the underlying `String` used to create the
498    /// [`TextLayout`] to which this line belongs.
499    #[inline]
500    pub fn range(&self) -> Range<usize> {
501        self.start_offset..self.end_offset
502    }
503}
504
505/// Result of hit testing a point in a [`TextLayout`].
506///
507/// This type is returned by [`TextLayout::hit_test_point`].
508#[derive(Debug, Default, PartialEq, Eq)]
509#[non_exhaustive]
510pub struct HitTestPoint {
511    /// The index representing the grapheme boundary closest to the `Point`.
512    pub idx: usize,
513    /// Whether or not the point was inside the bounds of the layout object.
514    ///
515    /// A click outside the layout object will still resolve to a position in the
516    /// text; for instance a click to the right edge of a line will resolve to the
517    /// end of that line, and a click below the last line will resolve to a
518    /// position in that line.
519    pub is_inside: bool,
520}
521
522/// Result of hit testing a text position in a [`TextLayout`].
523///
524/// This type is returned by [`TextLayout::hit_test_text_position`].
525#[derive(Debug, Default)]
526#[non_exhaustive]
527pub struct HitTestPosition {
528    /// the `point`'s `x` value is the position of the leading edge of the
529    /// grapheme cluster containing the text position. The `y` value corresponds
530    /// to the baseline of the line containing that grapheme cluster.
531    //FIXME: maybe we should communicate more about this position? for instance
532    //instead of returning an x/y point, we could return the x offset, the line's y_offset,
533    //and the line height (everything you would need to draw a cursor)
534    pub point: Point,
535    /// The number of the line containing this position.
536    ///
537    /// This value can be used to retrieve the [`LineMetric`] for this line,
538    /// via the [`TextLayout::line_metric`] method.
539    pub line: usize,
540}
541
542impl HitTestPoint {
543    /// Only for use by backends
544    #[doc(hidden)]
545    pub fn new(idx: usize, is_inside: bool) -> HitTestPoint {
546        HitTestPoint { idx, is_inside }
547    }
548}
549
550impl HitTestPosition {
551    /// Only for use by backends
552    #[doc(hidden)]
553    pub fn new(point: Point, line: usize) -> HitTestPosition {
554        HitTestPosition { point, line }
555    }
556}
557
558impl From<FontFamily> for TextAttribute {
559    fn from(t: FontFamily) -> TextAttribute {
560        TextAttribute::FontFamily(t)
561    }
562}
563
564impl From<FontWeight> for TextAttribute {
565    fn from(src: FontWeight) -> TextAttribute {
566        TextAttribute::Weight(src)
567    }
568}
569
570impl From<FontStyle> for TextAttribute {
571    fn from(src: FontStyle) -> TextAttribute {
572        TextAttribute::Style(src)
573    }
574}
575
576impl TextStorage for std::sync::Arc<str> {
577    fn as_str(&self) -> &str {
578        self
579    }
580}
581
582impl TextStorage for std::rc::Rc<str> {
583    fn as_str(&self) -> &str {
584        self
585    }
586}
587
588impl TextStorage for String {
589    fn as_str(&self) -> &str {
590        self.as_str()
591    }
592}
593
594impl TextStorage for std::sync::Arc<String> {
595    fn as_str(&self) -> &str {
596        self
597    }
598}
599
600impl TextStorage for std::rc::Rc<String> {
601    fn as_str(&self) -> &str {
602        self
603    }
604}
605
606impl TextStorage for &'static str {
607    fn as_str(&self) -> &str {
608        self
609    }
610}