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}