diff --git a/src/conv.rs b/src/conv.rs index 2926a0e..a25c5dc 100644 --- a/src/conv.rs +++ b/src/conv.rs @@ -47,7 +47,7 @@ impl DPU { } pub(crate) fn to_line_metrics(self, metrics: ttf_parser::LineMetrics) -> LineMetrics { LineMetrics { - position: self.i16_to_px(metrics.position), + top: self.i16_to_px(metrics.position), thickness: self.i16_to_px(metrics.thickness), } } @@ -56,6 +56,11 @@ impl DPU { /// Metrics for line marks #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct LineMetrics { - pub position: f32, + /// The vertical position of the top of the line + pub top: f32, + /// The recommended thickness of the line + /// + /// Be aware that `top` and `top + thickness` could round to the same value, + /// thus adjustments might be necessary to ensure visibility of the line. pub thickness: f32, } diff --git a/src/display/glyph_pos.rs b/src/display/glyph_pos.rs index 67a1537..1cc9b65 100644 --- a/src/display/glyph_pos.rs +++ b/src/display/glyph_pos.rs @@ -7,40 +7,9 @@ use super::{Line, TextDisplay}; use crate::conv::to_usize; -use crate::fonts::{self, FaceId}; -#[allow(unused)] -use crate::format::FormattableText; +use crate::fonts::{self, FaceId, ScaledFaceRef}; use crate::{Glyph, Range, Vec2, shaper}; - -/// Effect formatting marker -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Effect { - /// Index in text at which formatting becomes active - /// - /// (Note that we use `u32` not `usize` since it can be assumed text length - /// will never exceed `u32::MAX`.) - pub start: u32, - /// User-specified value - /// - /// Usage is not specified by `kas-text`, but typically this field will be - /// used as an index into a colour palette or not used at all. - pub color: u16, - /// Effect flags - pub flags: EffectFlags, -} - -bitflags::bitflags! { - /// Text effects - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct EffectFlags: u16 { - /// Glyph is underlined - const UNDERLINE = 1 << 0; - /// Glyph is crossed through by a center-line - const STRIKETHROUGH = 1 << 1; - } -} +use std::fmt::Debug; /// Used to return the position of a glyph with associated metrics #[derive(Copy, Clone, Debug, Default, PartialEq)] @@ -132,21 +101,23 @@ impl ExactSizeIterator for MarkerPosIter {} /// A sequence of positioned glyphs with effects /// /// Yielded by [`TextDisplay::runs`]. -pub struct GlyphRun<'a> { +pub struct GlyphRun<'a, E> { run: &'a shaper::GlyphRun, range: Range, offset: Vec2, - effects: &'a [Effect], + top: f32, + bottom: f32, + effects: &'a [(u32, E)], } -impl<'a> GlyphRun<'a> { - /// Get the font face used for this run +impl<'a, E: Copy + Default> GlyphRun<'a, E> { + /// Get the [`FaceId`] for this run #[inline] pub fn face_id(&self) -> FaceId { self.run.face_id } - /// Get the font size used for this run + /// Get the font size for this run /// /// Units are dots-per-Em (see [crate::fonts]). #[inline] @@ -154,6 +125,38 @@ impl<'a> GlyphRun<'a> { self.run.dpem } + /// Get the [`ScaledFaceRef`] for this run + /// + /// This may be useful to access font metrics. + #[inline] + pub fn scaled_face(&self) -> ScaledFaceRef<'_> { + fonts::library() + .get_face(self.run.face_id) + .scale_by_dpu(self.run.dpu) + } + + /// Get the `top` position of the line + /// + /// Note that there may be multiple runs per line and that these may not all + /// have the same [`ScaledFaceRef::ascent`] value when multiple fonts are + /// used, thus it is usually preferable to use the this value for + /// background colors (highlighting). + #[inline] + pub fn line_top(&self) -> f32 { + self.top + } + + /// Get the `bottom` position of the line + /// + /// Note that there may be multiple runs per line and that these may not all + /// have the same [`ScaledFaceRef::descent`] value when multiple fonts are + /// used, thus it is usually preferable to use the this value for + /// background colors (highlighting). + #[inline] + pub fn line_bottom(&self) -> f32 { + self.bottom + } + /// Get an iterator over glyphs for this run /// /// This method ignores effects; if you want those call @@ -170,32 +173,26 @@ impl<'a> GlyphRun<'a> { /// Yield glyphs and effects for this run /// - /// The callback `f` receives `glyph, color` where `color` is the - /// [`Effect::color`] value (defaults to 0). + /// The callback `f` is called for each `glyph` in unspecified order. The + /// corresponding `effect` is passed. /// - /// The callback `g` receives positioning for each underline/strike-through - /// segment: `x1, x2, y_top, h` where `h` is the thickness (height). Note - /// that it is possible to have `h < 1.0` and `y_top, y_top + h` to round to - /// the same number; the renderer is responsible for ensuring such lines - /// are actually visible. The last parameter is `e` as for `f`. + /// The callback `g` is called for sub-ranges of glyphs with parameters + /// `(p, x2, effect)`; `p.0` and `x2` are the x-axis positions of the left + /// and right edges of this sub-range respectively while `x.1` is the + /// vertical position of glyphs (the baseline). This may be combined with + /// information from [`Self::scaled_face`] to draw underline, strike-through + /// and background effects. /// /// Note: this is more computationally expensive than [`GlyphRun::glyphs`], /// so you may prefer to call that. Optionally one may choose to cache the /// result, though this is not really necessary. pub fn glyphs_with_effects(&self, mut f: F, mut g: G) where - F: FnMut(Glyph, u16), - G: FnMut(f32, f32, f32, f32, u16), + F: FnMut(Glyph, E), + G: FnMut(Vec2, f32, E), { - let sf = fonts::library() - .get_face(self.run.face_id) - .scale_by_dpu(self.run.dpu); - let ltr = self.run.level.is_ltr(); - let mut underline = None; - let mut strikethrough = None; - let mut effect_cur = usize::MAX; let mut effect_next = 0; @@ -205,7 +202,7 @@ impl<'a> GlyphRun<'a> { while self .effects .get(effect_next) - .map(|e| e.start <= left_index) + .map(|e| e.0 <= left_index) .unwrap_or(false) { effect_cur = effect_next; @@ -215,125 +212,64 @@ impl<'a> GlyphRun<'a> { let mut next_start = self .effects .get(effect_next) - .map(|e| e.start) + .map(|e| e.0) .unwrap_or(u32::MAX); let mut fmt = self .effects .get(effect_cur) .cloned() - .unwrap_or(Effect::default()); + .unwrap_or((0, E::default())); // In case an effect applies to the left-most glyph, it starts from that // glyph's x coordinate. - if !fmt.flags.is_empty() { - let glyph = &self.run.glyphs[self.range.start()]; - let position = glyph.position + self.offset; - if fmt.flags.contains(EffectFlags::UNDERLINE) - && let Some(metrics) = sf.underline_metrics() - { - let y_top = position.1 - metrics.position; - let h = metrics.thickness; - let x1 = position.0; - underline = Some((x1, y_top, h, fmt.color)); - } - if fmt.flags.contains(EffectFlags::STRIKETHROUGH) - && let Some(metrics) = sf.strikethrough_metrics() - { - let y_top = position.1 - metrics.position; - let h = metrics.thickness; - let x1 = position.0; - strikethrough = Some((x1, y_top, h, fmt.color)); - } - } + let mut range_start = self.run.glyphs[self.range.start()].position + self.offset; // Iterate over glyphs in left-to-right order. for mut glyph in self.run.glyphs[self.range.to_std()].iter().cloned() { glyph.position += self.offset; // Does the effect change? - if (ltr && next_start <= glyph.index) || (!ltr && fmt.start > glyph.index) { + if (ltr && next_start <= glyph.index) || (!ltr && fmt.0 > glyph.index) { + let x2 = glyph.position.0; + g(range_start, x2, fmt.1); + if ltr { // Find the next active effect - loop { - effect_cur = effect_next; - effect_next += 1; - if self - .effects - .get(effect_next) - .map(|e| e.start > glyph.index) - .unwrap_or(true) - { - break; - } - } + effect_cur = effect_next; + effect_next += 1; next_start = self .effects .get(effect_next) - .map(|e| e.start) + .map(|e| e.0) + .inspect(|start| debug_assert!(*start > glyph.index)) .unwrap_or(u32::MAX); } else { // Find the previous active effect - loop { - effect_cur = effect_cur.wrapping_sub(1); - if self.effects.get(effect_cur).map(|e| e.start).unwrap_or(0) <= glyph.index - { - break; - } - } + effect_cur = effect_cur.wrapping_sub(1); + debug_assert!( + self.effects.get(effect_cur).map(|e| e.0).unwrap_or(0) <= glyph.index + ); } fmt = self .effects .get(effect_cur) .cloned() - .unwrap_or(Effect::default()); - - if underline.is_some() != fmt.flags.contains(EffectFlags::UNDERLINE) { - if let Some((x1, y_top, h, e)) = underline { - let x2 = glyph.position.0; - g(x1, x2, y_top, h, e); - underline = None; - } else if let Some(metrics) = sf.underline_metrics() { - let y_top = glyph.position.1 - metrics.position; - let h = metrics.thickness; - let x1 = glyph.position.0; - underline = Some((x1, y_top, h, fmt.color)); - } - } - if strikethrough.is_some() != fmt.flags.contains(EffectFlags::STRIKETHROUGH) { - if let Some((x1, y_top, h, e)) = strikethrough { - let x2 = glyph.position.0; - g(x1, x2, y_top, h, e); - strikethrough = None; - } else if let Some(metrics) = sf.strikethrough_metrics() { - let y_top = glyph.position.1 - metrics.position; - let h = metrics.thickness; - let x1 = glyph.position.0; - strikethrough = Some((x1, y_top, h, fmt.color)); - } - } + .unwrap_or((0, E::default())); + + range_start = glyph.position; } - f(glyph, fmt.color); + f(glyph, fmt.1); } // Effects end at the following glyph's start (or end of this run part) - if let Some((x1, y_top, h, e)) = underline { - let x2 = if self.range.end() < self.run.glyphs.len() { - self.run.glyphs[self.range.end()].position.0 - } else { - self.run.caret - } + self.offset.0; - g(x1, x2, y_top, h, e); - } - if let Some((x1, y_top, h, e)) = strikethrough { - let x2 = if self.range.end() < self.run.glyphs.len() { - self.run.glyphs[self.range.end()].position.0 - } else { - self.run.caret - } + self.offset.0; - g(x1, x2, y_top, h, e); - } + let x2 = if self.range.end() < self.run.glyphs.len() { + self.run.glyphs[self.range.end()].position.0 + } else { + self.run.caret + } + self.offset.0; + g(range_start, x2, fmt.1); } } @@ -445,46 +381,57 @@ impl TextDisplay { /// All glyphs are translated by the given `offset` (this is practically /// free). /// - /// An [`Effect`] sequence supports underline, strikethrough and custom - /// indexing (e.g. for a color palette). This sequence may be the result of - /// [`FormattableText::effect_tokens`], `&[]`, or any other sequence such - /// that [`Effect::start`] values are strictly increasing and compatible - /// with text `char` indices (see also [`FormattableText::effect_tokens`]). - /// (It is not required to re-prepare text when changing the sequence.) + /// The `effects` sequence may be used for rendering effects: glyph color, + /// background color, strike-through, underline. Use `&[]` for no effects + /// (effectively using the default value of `E` everywhere), or use a + /// sequence such that `effects[i].0` values are strictly increasing and + /// each mapping to a distinct glyph cluster. A + /// glyph for index `j` in the source text will use effect `effects[i].1` + /// where `i` is the largest value such that `effects[i].0 <= j`, or the + /// default value of `E` if no such `i` exists. /// /// Runs are yielded in undefined order. The total number of /// glyphs yielded will equal [`TextDisplay::num_glyphs`]. /// /// [Requires status][Self#status-of-preparation]: /// text is fully prepared for display. - pub fn runs<'a>( + pub fn runs<'a, E: Copy + Debug + Default>( &'a self, offset: Vec2, - effects: &'a [Effect], - ) -> impl Iterator> + 'a { + effects: &'a [(u32, E)], + ) -> impl Iterator> + 'a { #[cfg(debug_assertions)] { let mut start = None; for effect in effects { if let Some(i) = start - && effect.start <= i + && effect.0 <= i { panic!( - "TextDisplay::runs: Effect::start indices are not strictly increasing in {effects:?}" + "TextDisplay::runs: effect start indices are not strictly increasing in {effects:?}" ); } - start = Some(effect.start); + start = Some(effect.0); } } + let mut line_iter = self.lines.iter(); + let mut line = line_iter.next().unwrap(); self.wrapped_runs .iter() .filter(|part| !part.glyph_range.is_empty()) - .map(move |part| GlyphRun { - run: &self.runs[to_usize(part.glyph_run)], - range: part.glyph_range, - offset: offset + part.offset, - effects, + .map(move |part| { + while part.text_end > line.text_range.end { + line = line_iter.next().unwrap(); + } + GlyphRun { + run: &self.runs[to_usize(part.glyph_run)], + range: part.glyph_range, + offset: offset + part.offset, + top: offset.1 + line.top, + bottom: offset.1 + line.bottom, + effects, + } }) } diff --git a/src/display/mod.rs b/src/display/mod.rs index c23cf43..944c74a 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -15,7 +15,7 @@ use tinyvec::TinyVec; mod glyph_pos; mod text_runs; mod wrap_lines; -pub use glyph_pos::{Effect, EffectFlags, GlyphRun, MarkerPos, MarkerPosIter}; +pub use glyph_pos::{GlyphRun, MarkerPos, MarkerPosIter}; pub(crate) use text_runs::RunSpecial; pub use wrap_lines::Line; use wrap_lines::RunPart; diff --git a/src/display/text_runs.rs b/src/display/text_runs.rs index 557369e..4083519 100644 --- a/src/display/text_runs.rs +++ b/src/display/text_runs.rs @@ -8,7 +8,7 @@ use super::TextDisplay; use crate::conv::{to_u32, to_usize}; use crate::fonts::{self, FaceId, FontSelector, NoFontMatch}; -use crate::format::FormattableText; +use crate::format::FontToken; use crate::util::ends_with_hard_break; use crate::{Direction, Range, shaper}; use icu_properties::props::{EmojiModifier, EmojiPresentation, RegionalIndicator, Script}; @@ -36,24 +36,17 @@ impl TextDisplay { /// /// This updates the result of [`TextDisplay::prepare_runs`] due to change /// in font size. - pub fn resize_runs( - &mut self, - text: &F, - font: FontSelector, - mut dpem: f32, - ) { - let mut font_tokens = text.font_tokens(dpem, font); - let mut next_fmt = font_tokens.next(); - - let text = text.as_str(); + pub fn resize_runs(&mut self, text: &str, mut font_tokens: impl Iterator) { + let (mut dpem, _) = read_initial_token(&mut font_tokens); + let mut next_token = font_tokens.next(); for run in &mut self.runs { - while let Some(fmt) = next_fmt.as_ref() { - if fmt.start > run.range.start { + while let Some(token) = next_token.as_ref() { + if token.start > run.range.start { break; } - dpem = fmt.dpem; - next_fmt = font_tokens.next(); + dpem = token.dpem; + next_token = font_tokens.next(); } let input = shaper::Input { @@ -159,12 +152,11 @@ impl TextDisplay { /// maximal slices of the `text` which do not contain explicit line breaks /// and have a single text direction according to the /// [Unicode Bidirectional Algorithm](http://www.unicode.org/reports/tr9/). - pub fn prepare_runs( + pub fn prepare_runs( &mut self, - text: &F, + text: &str, direction: Direction, - mut font: FontSelector, - mut dpem: f32, + mut font_tokens: impl Iterator, ) -> Result<(), NoFontMatch> { // This method constructs a list of "hard lines" (the initial line and any // caused by a hard break), each composed of a list of "level runs" (the @@ -174,18 +166,16 @@ impl TextDisplay { self.runs.clear(); - let mut font_tokens = text.font_tokens(dpem, font); - let mut next_fmt = font_tokens.next(); - if let Some(fmt) = next_fmt.as_ref() - && fmt.start == 0 + let (mut dpem, mut font) = read_initial_token(&mut font_tokens); + let mut next_token = font_tokens.next(); + if let Some(token) = next_token.as_ref() + && token.start == 0 { - font = fmt.font; - dpem = fmt.dpem; - next_fmt = font_tokens.next(); + font = token.font; + dpem = token.dpem; + next_token = font_tokens.next(); } - let text = text.as_str(); - let default_para_level = match direction { Direction::Auto => None, Direction::AutoRtl => { @@ -289,8 +279,8 @@ impl TextDisplay { .map(|level| *level != input.level) .unwrap_or(true); - if let Some(fmt) = next_fmt.as_ref() - && to_usize(fmt.start) == index + if let Some(token) = next_token.as_ref() + && to_usize(token.start) == index { require_break = true; } @@ -338,16 +328,16 @@ impl TextDisplay { breaks.push(shaper::GlyphBreak::new(to_u32(index))); } - if let Some(fmt) = next_fmt.as_ref() - && to_usize(fmt.start) == index + if let Some(token) = next_token.as_ref() + && to_usize(token.start) == index { - font = fmt.font; - input.dpem = fmt.dpem; - next_fmt = font_tokens.next(); + font = token.font; + input.dpem = token.dpem; + next_token = font_tokens.next(); debug_assert!( - next_fmt + next_token .as_ref() - .map(|fmt| to_usize(fmt.start) > index) + .map(|token| to_usize(token.start) > index) .unwrap_or(true) ); } @@ -404,6 +394,15 @@ impl TextDisplay { } } +fn read_initial_token(iter: &mut impl Iterator) -> (f32, FontSelector) { + let Some(FontToken { start, dpem, font }) = iter.next() else { + debug_assert!(false, "iterator font_tokens is empty"); + return (16.0, FontSelector::default()); + }; + debug_assert_eq!(start, 0, "iterator font_tokens does not start at 0"); + (dpem, font) +} + fn is_real(script: Script) -> bool { !matches!(script, Script::Common | Script::Unknown | Script::Inherited) } diff --git a/src/display/wrap_lines.rs b/src/display/wrap_lines.rs index 4cf7a20..2d0febb 100644 --- a/src/display/wrap_lines.rs +++ b/src/display/wrap_lines.rs @@ -26,8 +26,8 @@ pub struct RunPart { /// Per-line data (post wrapping) #[derive(Clone, Debug, Default)] pub struct Line { - text_range: Range, // range in text - pub(crate) run_range: Range, // range in wrapped_runs + pub(crate) text_range: Range, // range in text + pub(crate) run_range: Range, // range in wrapped_runs pub(crate) top: f32, pub(crate) bottom: f32, } @@ -702,6 +702,7 @@ impl PartAccumulator for LineAdder { } offset = part.len_no_space - part.len; } + debug_assert!(text_end <= line_text_end); let xoffset = if part.end_space { end_caret - part.offset + offset diff --git a/src/fonts/face.rs b/src/fonts/face.rs index af27a2d..14b56b9 100644 --- a/src/fonts/face.rs +++ b/src/fonts/face.rs @@ -61,7 +61,9 @@ impl<'a> FaceRef<'a> { /// Handle to a loaded font face /// -/// TODO: verify whether these values need adjustment for variations. +/// Several values are relative to the vertical baseline of the text. Due to +/// common axis conventions, it may be necessary to negate these; for example +/// `baseline - self.ascent()`. #[derive(Copy, Clone, Debug)] pub struct ScaledFaceRef<'a>(&'a Face<'a>, DPU); impl<'a> ScaledFaceRef<'a> { diff --git a/src/format.rs b/src/format.rs index 808296b..f81657e 100644 --- a/src/format.rs +++ b/src/format.rs @@ -5,10 +5,10 @@ //! Parsers for formatted text -use crate::Effect; use crate::fonts::FontSelector; #[allow(unused)] -use crate::{Text, TextDisplay}; // for doc-links +use crate::{Text, TextDisplay}; +use std::fmt::Debug; // for doc-links mod plain; @@ -17,15 +17,35 @@ mod markdown; #[cfg(feature = "markdown")] pub use markdown::{Error as MarkdownError, Markdown}; -/// Text, optionally with formatting data -pub trait FormattableText: std::cmp::PartialEq { - /// Length of text +/// A possible effect formatting marker +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Effect { + /// User-specified value /// - /// Default implementation uses [`FormattableText::as_str`]. - #[inline] - fn str_len(&self) -> usize { - self.as_str().len() + /// Usage is not specified by `kas-text`, but typically this field will be + /// used as an index into a colour palette or not used at all. + pub color: u16, + /// Effect flags + pub flags: EffectFlags, +} + +bitflags::bitflags! { + /// Text effects + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct EffectFlags: u16 { + /// Glyph is underlined + const UNDERLINE = 1 << 0; + /// Glyph is crossed through by a center-line + const STRIKETHROUGH = 1 << 1; } +} + +/// Text, optionally with formatting data +pub trait FormattableText: std::cmp::PartialEq { + /// Type of the effect token + type Effect: Copy + Debug + Default; /// Access whole text as contiguous `str` fn as_str(&self) -> &str; @@ -37,12 +57,13 @@ pub trait FormattableText: std::cmp::PartialEq { /// [font size][crate::Text::set_font_size] and [`FontSelector`]; these /// values are passed as a reference (`dpem` and `font`). /// - /// The iterator is expected to yield a stream of tokens such that - /// [`FontToken::start`] values are strictly increasing, less than - /// [`Self::str_len`] and at `char` boundaries (i.e. an index value returned - /// by [`str::char_indices`]. In case the returned iterator is empty or the - /// first [`FontToken::start`] value is greater than zero the reference - /// `dpem` and `font` values are used. + /// The iterator is expected to yield a non-empty stream of tokens such that + /// the first [`FontToken::start`] value is `0` and remaining `start` values + /// are strictly increasing, with each value being less than + /// `self.as_str().len()` and at `char` boundaries (i.e. an index value + /// returned by [`str::char_indices`]. In case the returned iterator is + /// empty or the first [`FontToken::start`] value is greater than zero the + /// reference `dpem` and `font` values are used. /// /// Any changes to the result of this method require full re-preparation of /// text since this affects run breaking and font resolution. @@ -50,20 +71,22 @@ pub trait FormattableText: std::cmp::PartialEq { /// Return the sequence of effect tokens /// - /// These tokens are used to select the font color and - /// [effects](crate::EffectFlags). - /// - /// The values of [`Effect::start`] are expected to be strictly increasing - /// in order, less than [`Self::str_len`]. In case the slice is empty or the - /// first [`Effect::start`] value is greater than zero, values from - /// [`Effect::default()`] are used. + /// The `effects` sequence may be used for rendering effects: glyph color, + /// background color, strike-through, underline. Use `&[]` for no effects + /// (effectively using the default value of `Self::Effect` everywhere), or + /// use a sequence such that `effects[i].0` values are strictly increasing. + /// A glyph for index `j` in the source text will use effect `effects[i].1` + /// where `i` is the largest value such that `effects[i].0 <= j`, or the + /// default value of `Self::Effect` if no such `i` exists. /// /// Changes to the result of this method do not require any re-preparation /// of text. - fn effect_tokens(&self) -> &[Effect]; + fn effect_tokens(&self) -> &[(u32, Self::Effect)]; } impl FormattableText for &F { + type Effect = F::Effect; + fn as_str(&self) -> &str { F::as_str(self) } @@ -72,7 +95,7 @@ impl FormattableText for &F { F::font_tokens(self, dpem, font) } - fn effect_tokens(&self) -> &[Effect] { + fn effect_tokens(&self) -> &[(u32, Self::Effect)] { F::effect_tokens(self) } } diff --git a/src/format/markdown.rs b/src/format/markdown.rs index d9653a3..cefa9b7 100644 --- a/src/format/markdown.rs +++ b/src/format/markdown.rs @@ -5,10 +5,9 @@ //! Markdown parsing -use super::{FontToken, FormattableText}; +use super::{Effect, EffectFlags, FontToken, FormattableText}; use crate::conv::to_u32; use crate::fonts::{FamilySelector, FontSelector, FontStyle, FontWeight}; -use crate::{Effect, EffectFlags}; use pulldown_cmark::{Event, HeadingLevel, Tag, TagEnd}; use std::fmt::Write; use std::iter::FusedIterator; @@ -49,7 +48,7 @@ pub enum Error { pub struct Markdown { text: String, fmt: Vec, - effects: Vec, + effects: Vec<(u32, Effect)>, } impl Markdown { @@ -117,6 +116,8 @@ impl<'a> ExactSizeIterator for FontTokenIter<'a> {} impl<'a> FusedIterator for FontTokenIter<'a> {} impl FormattableText for Markdown { + type Effect = Effect; + #[inline] fn as_str(&self) -> &str { &self.text @@ -127,14 +128,14 @@ impl FormattableText for Markdown { FontTokenIter::new(&self.fmt, dpem, font) } - fn effect_tokens(&self) -> &[Effect] { + fn effect_tokens(&self) -> &[(u32, Effect)] { &self.effects } } fn parse(input: &str) -> Result { let mut text = String::with_capacity(input.len()); - let mut fmt: Vec = Vec::new(); + let mut fmt: Vec = vec![Fmt::default()]; let mut set_last = |item: &StackItem| { let f = item.fmt.clone(); if let Some(last) = fmt.last_mut() @@ -204,11 +205,13 @@ fn parse(input: &str) -> Result { let mut flags = EffectFlags::default(); for token in &fmt { if token.flags != flags { - effects.push(Effect { - start: token.start, - color: 0, - flags: token.flags, - }); + effects.push(( + token.start, + Effect { + color: 0, + flags: token.flags, + }, + )); flags = token.flags; } } diff --git a/src/format/plain.rs b/src/format/plain.rs index 5513db3..2561efb 100644 --- a/src/format/plain.rs +++ b/src/format/plain.rs @@ -6,38 +6,44 @@ //! Implementations for plain text use super::{FontToken, FormattableText}; -use crate::{Effect, fonts::FontSelector}; +use crate::fonts::FontSelector; impl FormattableText for str { + type Effect = (); + #[inline] fn as_str(&self) -> &str { self } #[inline] - fn font_tokens(&self, _: f32, _: FontSelector) -> impl Iterator { - std::iter::empty() + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + let start = 0; + std::iter::once(FontToken { start, dpem, font }) } #[inline] - fn effect_tokens(&self) -> &[Effect] { + fn effect_tokens(&self) -> &[(u32, ())] { &[] } } impl FormattableText for String { + type Effect = (); + #[inline] fn as_str(&self) -> &str { self } #[inline] - fn font_tokens(&self, _: f32, _: FontSelector) -> impl Iterator { - std::iter::empty() + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + let start = 0; + std::iter::once(FontToken { start, dpem, font }) } #[inline] - fn effect_tokens(&self) -> &[Effect] { + fn effect_tokens(&self) -> &[(u32, ())] { &[] } } diff --git a/src/lib.rs b/src/lib.rs index 41c532f..9569f8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ mod env; pub use env::*; mod conv; -pub use conv::DPU; +pub use conv::{DPU, LineMetrics}; mod data; use data::Range; diff --git a/src/text.rs b/src/text.rs index b2355d8..3bfb892 100644 --- a/src/text.rs +++ b/src/text.rs @@ -5,10 +5,11 @@ //! Text object -use crate::display::{Effect, MarkerPosIter, NotReady, TextDisplay}; +use crate::display::{MarkerPosIter, NotReady, TextDisplay}; use crate::fonts::{FontSelector, NoFontMatch}; use crate::format::FormattableText; use crate::{Align, Direction, GlyphRun, Line, Status, Vec2}; +use std::fmt::Debug; use std::num::NonZeroUsize; /// Text type-setting object (high-level API) @@ -33,7 +34,7 @@ use std::num::NonZeroUsize; /// text.set_bounds(Vec2(200.0, 50.0)); /// text.prepare().unwrap(); /// -/// for run in text.runs(Vec2::ZERO, &[]).unwrap() { +/// for run in text.runs(Vec2::ZERO).unwrap() { /// let (face, dpem) = (run.face_id(), run.dpem()); /// for glyph in run.glyphs() { /// println!("{face:?} - {dpem}px - {glyph:?}"); @@ -333,7 +334,7 @@ impl Text { /// This method simply forwards the result of /// [`FormattableText::effect_tokens`]. #[inline] - pub fn effect_tokens(&self) -> &[Effect] { + pub fn effect_tokens(&self) -> &[(u32, T::Effect)] { self.text.effect_tokens() } } @@ -389,11 +390,15 @@ impl Text { #[inline] fn prepare_runs(&mut self) -> Result<(), NoFontMatch> { match self.status { - Status::New => { - self.display - .prepare_runs(&self.text, self.direction, self.font, self.dpem)? - } - Status::ResizeLevelRuns => self.display.resize_runs(&self.text, self.font, self.dpem), + Status::New => self.display.prepare_runs( + self.text.as_str(), + self.direction, + self.text.font_tokens(self.dpem, self.font), + )?, + Status::ResizeLevelRuns => self.display.resize_runs( + self.text.as_str(), + self.text.font_tokens(self.dpem, self.font), + ), _ => (), } @@ -549,17 +554,37 @@ impl Text { /// All glyphs are translated by the given `offset` (this is practically /// free). /// - /// An [`Effect`] sequence supports underline, strikethrough and custom - /// indexing (e.g. for a color palette). Pass `&[]` if effects are not - /// required. (The default effect is always [`Effect::default()`].) + /// Uses effect tokens supplied by [`FormattableText::effect_tokens`]. /// /// Runs are yielded in undefined order. The total number of /// glyphs yielded will equal [`TextDisplay::num_glyphs`]. pub fn runs<'a>( &'a self, offset: Vec2, - effects: &'a [Effect], - ) -> Result> + 'a, NotReady> { + ) -> Result> + 'a, NotReady> { + Ok(self.display()?.runs(offset, self.text.effect_tokens())) + } + + /// Iterate over runs of positioned glyphs using a custom effects list + /// + /// All glyphs are translated by the given `offset` (this is practically + /// free). + /// + /// The `effects` sequence may be used for rendering effects: glyph color, + /// background color, strike-through, underline. Use `&[]` for no effects + /// (effectively using the default value of `E` everywhere), or use a + /// sequence such that `effects[i].0` values are strictly increasing. A + /// glyph for index `j` in the source text will use effect `effects[i].1` + /// where `i` is the largest value such that `effects[i].0 <= j`, or the + /// default value of `E` if no such `i` exists. + /// + /// Runs are yielded in undefined order. The total number of + /// glyphs yielded will equal [`TextDisplay::num_glyphs`]. + pub fn runs_with_effects<'a, E: Copy + Debug + Default>( + &'a self, + offset: Vec2, + effects: &'a [(u32, E)], + ) -> Result> + 'a, NotReady> { Ok(self.display()?.runs(offset, effects)) }