cosmic-text/src/layout.rs

219 lines
6.9 KiB
Rust
Raw Normal View History

2022-10-25 12:52:46 -06:00
// SPDX-License-Identifier: MIT OR Apache-2.0
use core::fmt::Display;
use core::ops::Range;
use crate::{math, CacheKey, CacheKeyFlags, Color, GlyphDecorationData};
2022-11-08 08:43:27 -07:00
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
2022-11-08 08:43:27 -07:00
#[cfg(not(feature = "std"))]
use core_maths::CoreFloat;
2022-10-25 12:52:46 -06:00
/// A laid out glyph
2023-11-30 12:59:49 -07:00
#[derive(Clone, Debug)]
2022-10-25 12:52:46 -06:00
pub struct LayoutGlyph {
/// Start index of cluster in original line
pub start: usize,
/// End index of cluster in original line
pub end: usize,
/// Font size of the glyph
pub font_size: f32,
/// Font weight of the glyph
pub font_weight: fontdb::Weight,
2024-06-06 15:21:44 -06:00
/// Line height of the glyph, will override buffer setting
pub line_height_opt: Option<f32>,
/// Font id of the glyph
pub font_id: fontdb::ID,
/// Font id of the glyph
pub glyph_id: u16,
2022-10-25 12:52:46 -06:00
/// X offset of hitbox
pub x: f32,
/// Y offset of hitbox
pub y: f32,
/// Width of hitbox
2022-10-25 12:52:46 -06:00
pub w: f32,
2025-01-22 16:29:02 -05:00
/// Unicode `BiDi` embedding level, character is left-to-right if `level` is divisible by 2
2022-12-16 16:49:29 -07:00
pub level: unicode_bidi::Level,
/// X offset in line
///
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
/// [`PhysicalGlyph`] for rendering.
///
/// This offset is useful when you are dealing with logical units and you do not care or
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
/// for vectorial text, apply linear transformations to the layout, etc.
pub x_offset: f32,
/// Y offset in line
///
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
/// [`PhysicalGlyph`] for rendering.
///
/// This offset is useful when you are dealing with logical units and you do not care or
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
/// for vectorial text, apply linear transformations to the layout, etc.
pub y_offset: f32,
2022-10-26 14:16:48 -06:00
/// Optional color override
pub color_opt: Option<Color>,
/// Metadata from `Attrs`
pub metadata: usize,
2024-01-02 11:36:53 -07:00
/// [`CacheKeyFlags`]
pub cache_key_flags: CacheKeyFlags,
}
/// A span of consecutive glyphs sharing the same text decoration.
#[derive(Clone, Debug, PartialEq)]
pub struct DecorationSpan {
/// Range of glyph indices in `LayoutLine::glyphs` covered by this span
pub glyph_range: Range<usize>,
/// The decoration config and metrics
pub data: GlyphDecorationData,
/// Fallback color from the first glyph's `color_opt`
pub color_opt: Option<Color>,
/// Font size from the first glyph (used to scale EM-unit metrics)
pub font_size: f32,
2022-10-25 12:52:46 -06:00
}
2023-11-30 12:59:49 -07:00
#[derive(Clone, Debug)]
pub struct PhysicalGlyph {
2025-01-22 16:29:02 -05:00
/// Cache key, see [`CacheKey`]
pub cache_key: CacheKey,
/// Integer component of X offset in line
pub x: i32,
/// Integer component of Y offset in line
pub y: i32,
}
impl LayoutGlyph {
pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph {
let x_offset = self.font_size * self.x_offset;
let y_offset = self.font_size * self.y_offset;
let (cache_key, x, y) = CacheKey::new(
self.font_id,
self.glyph_id,
self.font_size * scale,
(
(self.x + x_offset).mul_add(scale, offset.0),
math::truncf((self.y - y_offset).mul_add(scale, offset.1)), // Hinting in Y axis
),
self.font_weight,
2024-01-02 11:36:53 -07:00
self.cache_key_flags,
);
PhysicalGlyph { cache_key, x, y }
}
}
2022-10-25 12:52:46 -06:00
/// A line of laid out glyphs
2023-11-30 12:59:49 -07:00
#[derive(Clone, Debug)]
2022-10-25 12:52:46 -06:00
pub struct LayoutLine {
2022-12-15 04:59:31 +01:00
/// Width of the line
pub w: f32,
/// Maximum ascent of the glyphs in line
pub max_ascent: f32,
/// Maximum descent of the glyphs in line
pub max_descent: f32,
/// Maximum line height of any spans in line
pub line_height_opt: Option<f32>,
2022-10-25 12:52:46 -06:00
/// Glyphs in line
pub glyphs: Vec<LayoutGlyph>,
/// Text decoration spans covering ranges of glyphs
pub decorations: Vec<DecorationSpan>,
2022-10-25 12:52:46 -06:00
}
/// Wrapping mode
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum Wrap {
/// No wrapping
None,
/// Wraps at a glyph level
Glyph,
/// Wraps at the word level
Word,
/// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself
WordOrGlyph,
}
impl Display for Wrap {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::None => write!(f, "No Wrap"),
Self::Word => write!(f, "Word Wrap"),
Self::WordOrGlyph => write!(f, "Word Wrap or Character"),
Self::Glyph => write!(f, "Character"),
}
}
}
2023-02-22 18:31:49 -07:00
/// Align or justify
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum Align {
Left,
Right,
Center,
Justified,
End,
2023-02-22 18:31:49 -07:00
}
impl Display for Align {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Left => write!(f, "Left"),
Self::Right => write!(f, "Right"),
Self::Center => write!(f, "Center"),
Self::Justified => write!(f, "Justified"),
Self::End => write!(f, "End"),
2023-02-22 18:31:49 -07:00
}
}
}
Ellipsize (#467) * feat: add Ellipsize enum * chore: API changes needed for ellipsize Decided not to change "layout()" function for now to avoid breaking the interface. For now. * chore: shape ellipsis * feat: Ellipsize::Start Since it can only have 1 line, it's easier to implement. * DROPME: temporarily change rich-text for testing * test(ellipsize): Testing Ellipsize::Start Long text in small buffer should produce ellipsis glyphs * fix: do not need font_system anymore We moved ellipsis shaping elsewhere so no need to pass font_system to layout function (which also was recreating a new one in the tests every time making them take forever). * feat: Ellipsize::End * improv(ellipsize): use a single ellipsis shape * improv: Ellipsie::End && Wrap::None There is no need to layout the whole line if it's not going to fit. * fix: mixed bidi text when Ellipsize::End && Wrap::None * chore: clean up and simplify when line.RTL==span.RTL * fix(ellipsize): last word is not (word_count -1) if iter().rev() * refactor(layout): extract the layout algorithm to make it more readable * improv(ellipsize): Ellipsize::Start && Wrap::None we iterate in reverse and only layout what's going to be visible * Revert: delete the previous approach of post processing ellipsis * doc: explain the interaction between Ellipsize and Wrap * chore: lower the scope * feat: Ellipsize the last line of a paragraph For now only the number of lines is supported * fix: clear ellipsized field on visual lines This was causing ellipsis to show on random lines * chore: remove old tests will add better tests soon * chore: clean up changes from previous attempt * fix: consider the ellipsis width when doing alignment * feat(ellipsize): add `Height` limit to `Ellipsize` * fix: ellipsize the start of the last line correctly * fix: ellipsize at the start of mixed bidi lines * feat: Ellipsize::Middle * fix: consider ellipsize::middle when calculating alignment correction * refactor: improve readability * refactor: deduplicate "fit_glyphs" * refactor: combine backward and forward layout into one (wip) * fix: Backward works in the unified layout_spans * chore: clean up * fix: Ellipsize::Middle * fix: handle large words in bidi boundaries * chore: clean up and some refactoring * fix: ellipsis is now the same level as the surrounding text * fix: try to fit more when ellipsizing::middle * improv: ellipsis now have the same level as the neighbors This makes ellipsized RTL text inside a LTR line more readable. before: Hello سلام...خوبی؟ Hello خولی؟...سلام * fix: some extra words were being rendered in Ellipsize::Middle This was causing the last word (if it's not the same level as the rest) to be rendered outside the buffer. * test: a few test cases for ellipsize * fix: assign the correct byte range to ellipsis this should fix the panic when selecting or clicking on or near the ellipsis in the editor.
2026-02-19 09:11:22 -07:00
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Ellipsize {
/// No Ellipsizing
#[default]
None,
/// Ellipsizes the start of the last visual line that fits within the `EllipsizeHeightLimit`
Start(EllipsizeHeightLimit),
/// Ellipsizes the middle of the last visual line that fits within the `EllipsizeHeightLimit`.
Middle(EllipsizeHeightLimit),
/// Ellipsizes the end of the last visual line that fits within the `EllipsizeHeightLimit`.
End(EllipsizeHeightLimit),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EllipsizeHeightLimit {
/// Number of lines to show before ellipsizing the rest. Only works if `Wrap` is NOT set to
/// `Wrap::None`. Otherwise, it will be ignored and the behavior will be the same as `Lines(1)`
Lines(usize),
/// Ellipsizes the last line that fits within the given height limit. If `Wrap` is set to
/// `Wrap::None`, the behavior will be the same as `Lines(1)`
Height(f32),
}
/// Metrics hinting strategy
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)]
pub enum Hinting {
/// No metrics hinting.
///
/// Glyphs will have subpixel coordinates.
///
/// This is the default.
#[default]
Disabled,
/// Metrics hinting.
///
/// Glyphs will be snapped to integral coordinates in the X-axis during layout.
/// This can improve readability for smaller text and/or low-DPI screens.
///
/// However, in order to get the right effect, you must use physical coordinates
/// during layout and avoid further scaling when rendering. Otherwise, the rounding
/// errors can accumulate and glyph distances may look erratic.
///
/// In other words, metrics hinting makes layouting dependent of the target
/// resolution.
Enabled,
}